Merge changes from topic "pointer-icon-refactor-vdm" into main
* changes:
Pointer Icon Refactor: Add displayId parameter to getCursorPosition
Pointer Icon Refactor: Remove get/setVirtualMousePointerDisplayId
diff --git a/AconfigFlags.bp b/AconfigFlags.bp
index f5bf437..beb11fc 100644
--- a/AconfigFlags.bp
+++ b/AconfigFlags.bp
@@ -64,7 +64,9 @@
":com.android.input.flags-aconfig-java{.generated_srcjars}",
":com.android.internal.foldables.flags-aconfig-java{.generated_srcjars}",
":com.android.media.flags.bettertogether-aconfig-java{.generated_srcjars}",
+ ":com.android.media.flags.editing-aconfig-java{.generated_srcjars}",
":com.android.net.flags-aconfig-java{.generated_srcjars}",
+ ":com.android.net.thread.flags-aconfig-java{.generated_srcjars}",
":com.android.server.flags.services-aconfig-java{.generated_srcjars}",
":com.android.text.flags-aconfig-java{.generated_srcjars}",
":com.android.window.flags.window-aconfig-java{.generated_srcjars}",
@@ -133,6 +135,7 @@
"com.android.input.flags-aconfig",
"com.android.media.flags.bettertogether-aconfig",
"com.android.net.flags-aconfig",
+ "com.android.net.thread.flags-aconfig",
"com.android.server.flags.services-aconfig",
"com.android.text.flags-aconfig",
"com.android.window.flags.window-aconfig",
@@ -536,6 +539,21 @@
defaults: ["framework-minus-apex-aconfig-java-defaults"],
}
+// Media Editing
+aconfig_declarations {
+ name: "com.android.media.flags.editing-aconfig",
+ package: "com.android.media.editing.flags",
+ srcs: [
+ "media/java/android/media/flags/editing.aconfig",
+ ],
+}
+
+java_aconfig_library {
+ name: "com.android.media.flags.editing-aconfig-java",
+ aconfig_declarations: "com.android.media.flags.editing-aconfig",
+ defaults: ["framework-minus-apex-aconfig-java-defaults"],
+}
+
// Media TV
aconfig_declarations {
name: "android.media.tv.flags-aconfig",
@@ -760,12 +778,25 @@
srcs: ["core/java/android/net/flags.aconfig"],
}
+// Thread network
+aconfig_declarations {
+ name: "com.android.net.thread.flags-aconfig",
+ package: "com.android.net.thread.flags",
+ srcs: ["core/java/android/net/thread/flags.aconfig"],
+}
+
java_aconfig_library {
name: "com.android.net.flags-aconfig-java",
aconfig_declarations: "com.android.net.flags-aconfig",
defaults: ["framework-minus-apex-aconfig-java-defaults"],
}
+java_aconfig_library {
+ name: "com.android.net.thread.flags-aconfig-java",
+ aconfig_declarations: "com.android.net.thread.flags-aconfig",
+ defaults: ["framework-minus-apex-aconfig-java-defaults"],
+}
+
// Media
aconfig_declarations {
name: "android.media.playback.flags-aconfig",
diff --git a/apct-tests/perftests/core/src/android/graphics/perftests/CanvasPerfTest.java b/apct-tests/perftests/core/src/android/graphics/perftests/CanvasPerfTest.java
index 3c361d7..95730e8 100644
--- a/apct-tests/perftests/core/src/android/graphics/perftests/CanvasPerfTest.java
+++ b/apct-tests/perftests/core/src/android/graphics/perftests/CanvasPerfTest.java
@@ -122,6 +122,8 @@
Bitmap.createScaledBitmap(source, source.getWidth() / 2, source.getHeight() / 2, true)
.recycle();
}
+ source.recycle();
+ Runtime.getRuntime().gc();
}
@Test
@@ -141,6 +143,8 @@
Bitmap.createScaledBitmap(source, source.getWidth() / 2, source.getHeight() / 2, true)
.recycle();
}
+ source.recycle();
+ Runtime.getRuntime().gc();
}
@Test
@@ -158,5 +162,7 @@
Bitmap.createScaledBitmap(source, source.getWidth() / 2, source.getHeight() / 2, true)
.recycle();
}
+ source.recycle();
+ Runtime.getRuntime().gc();
}
}
diff --git a/apex/jobscheduler/service/aconfig/job.aconfig b/apex/jobscheduler/service/aconfig/job.aconfig
index de6f023..5d65d9d 100644
--- a/apex/jobscheduler/service/aconfig/job.aconfig
+++ b/apex/jobscheduler/service/aconfig/job.aconfig
@@ -1,6 +1,13 @@
package: "com.android.server.job"
flag {
+ name: "do_not_force_rush_execution_at_boot"
+ namespace: "backstage_power"
+ description: "Don't force rush job execution right after boot completion"
+ bug: "321598070"
+}
+
+flag {
name: "relax_prefetch_connectivity_constraint_only_on_charger"
namespace: "backstage_power"
description: "Only relax a prefetch job's connectivity constraint when the device is charging and battery is not low"
diff --git a/apex/jobscheduler/service/java/com/android/server/DeviceIdleController.java b/apex/jobscheduler/service/java/com/android/server/DeviceIdleController.java
index 31214cb..696c317 100644
--- a/apex/jobscheduler/service/java/com/android/server/DeviceIdleController.java
+++ b/apex/jobscheduler/service/java/com/android/server/DeviceIdleController.java
@@ -3919,6 +3919,7 @@
if (locationManager != null
&& locationManager.getProvider(LocationManager.FUSED_PROVIDER)
!= null) {
+ mHasFusedLocation = true;
locationManager.requestLocationUpdates(LocationManager.FUSED_PROVIDER,
mLocationRequest,
AppSchedulingModuleThread.getExecutor(),
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 b0f378d..7a92cca 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
@@ -2720,8 +2720,10 @@
sc.maybeStartTrackingJobLocked(job, null);
}
});
- // GO GO GO!
- mHandler.obtainMessage(MSG_CHECK_JOB).sendToTarget();
+ if (!Flags.doNotForceRushExecutionAtBoot()) {
+ // GO GO GO!
+ mHandler.obtainMessage(MSG_CHECK_JOB).sendToTarget();
+ }
}
}
}
@@ -5441,6 +5443,8 @@
pw.println("Aconfig flags:");
pw.increaseIndent();
+ pw.print(Flags.FLAG_DO_NOT_FORCE_RUSH_EXECUTION_AT_BOOT,
+ Flags.doNotForceRushExecutionAtBoot());
pw.print(Flags.FLAG_THROW_ON_UNSUPPORTED_BIAS_USAGE,
Flags.throwOnUnsupportedBiasUsage());
pw.println();
diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerShellCommand.java b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerShellCommand.java
index c14efae..6f2393a 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerShellCommand.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerShellCommand.java
@@ -344,11 +344,14 @@
final String flagName = getNextArgRequired();
switch (flagName) {
+ case android.app.job.Flags.FLAG_ENFORCE_MINIMUM_TIME_WINDOWS:
+ pw.println(android.app.job.Flags.enforceMinimumTimeWindows());
+ break;
case android.app.job.Flags.FLAG_JOB_DEBUG_INFO_APIS:
pw.println(android.app.job.Flags.jobDebugInfoApis());
break;
- case android.app.job.Flags.FLAG_ENFORCE_MINIMUM_TIME_WINDOWS:
- pw.println(android.app.job.Flags.enforceMinimumTimeWindows());
+ case com.android.server.job.Flags.FLAG_DO_NOT_FORCE_RUSH_EXECUTION_AT_BOOT:
+ pw.println(com.android.server.job.Flags.doNotForceRushExecutionAtBoot());
break;
case com.android.server.job.Flags.FLAG_THROW_ON_UNSUPPORTED_BIAS_USAGE:
pw.println(com.android.server.job.Flags.throwOnUnsupportedBiasUsage());
diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/FlexibilityController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/FlexibilityController.java
index 14cce19..6883d18 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/controllers/FlexibilityController.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/FlexibilityController.java
@@ -972,6 +972,20 @@
synchronized (mLock) {
final long earliest = getLifeCycleBeginningElapsedLocked(js);
final long latest = getLifeCycleEndElapsedLocked(js, nowElapsed, earliest);
+ if (latest <= earliest) {
+ // Something has gone horribly wrong. This has only occurred on incorrectly
+ // configured tests, but add a check here for safety.
+ Slog.wtf(TAG, "Got invalid latest when scheduling alarm."
+ + " Prefetch=" + js.getJob().isPrefetch());
+ // Since things have gone wrong, the safest and most reliable thing to do is
+ // stop applying flex policy to the job.
+ mFlexibilityTracker.setNumDroppedFlexibleConstraints(js,
+ js.getNumAppliedFlexibleConstraints());
+ mJobsToCheck.add(js);
+ mHandler.sendEmptyMessage(MSG_CHECK_JOBS);
+ return;
+ }
+
final long nextTimeElapsed =
getNextConstraintDropTimeElapsedLocked(js, earliest, latest);
diff --git a/core/api/Android.bp b/core/api/Android.bp
index 907916a..8d8a82b 100644
--- a/core/api/Android.bp
+++ b/core/api/Android.bp
@@ -96,21 +96,3 @@
name: "non-updatable-test-lint-baseline.txt",
srcs: ["test-lint-baseline.txt"],
}
-
-java_api_contribution {
- name: "api-stubs-docs-non-updatable-public-stubs",
- api_surface: "public",
- api_file: "current.txt",
- visibility: [
- "//build/orchestrator/apis",
- ],
-}
-
-java_api_contribution {
- name: "frameworks-base-core-api-module-lib-stubs",
- api_surface: "module-lib",
- api_file: "module-lib-current.txt",
- visibility: [
- "//build/orchestrator/apis",
- ],
-}
diff --git a/core/api/current.txt b/core/api/current.txt
index 0e413c4..3f0d106 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -274,6 +274,7 @@
field public static final String REQUEST_IGNORE_BATTERY_OPTIMIZATIONS = "android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS";
field public static final String REQUEST_INSTALL_PACKAGES = "android.permission.REQUEST_INSTALL_PACKAGES";
field public static final String REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE = "android.permission.REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE";
+ field @FlaggedApi("android.companion.flags.device_presence") public static final String REQUEST_OBSERVE_DEVICE_UUID_PRESENCE = "android.permission.REQUEST_OBSERVE_DEVICE_UUID_PRESENCE";
field public static final String REQUEST_PASSWORD_COMPLEXITY = "android.permission.REQUEST_PASSWORD_COMPLEXITY";
field @Deprecated public static final String RESTART_PACKAGES = "android.permission.RESTART_PACKAGES";
field public static final String RUN_USER_INITIATED_JOBS = "android.permission.RUN_USER_INITIATED_JOBS";
@@ -1603,6 +1604,7 @@
field public static final int switchTextOff = 16843628; // 0x101036c
field public static final int switchTextOn = 16843627; // 0x101036b
field public static final int syncable = 16842777; // 0x1010019
+ field @FlaggedApi("android.multiuser.enable_system_user_only_for_services_and_providers") public static final int systemUserOnly;
field public static final int tabStripEnabled = 16843453; // 0x10102bd
field public static final int tabStripLeft = 16843451; // 0x10102bb
field public static final int tabStripRight = 16843452; // 0x10102bc
@@ -7699,7 +7701,7 @@
method @Nullable public android.app.WallpaperColors getWallpaperColors(int);
method @Nullable @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_EXTERNAL_STORAGE, "android.permission.READ_WALLPAPER_INTERNAL"}) public android.os.ParcelFileDescriptor getWallpaperFile(int);
method public int getWallpaperId(int);
- method public android.app.WallpaperInfo getWallpaperInfo();
+ method @RequiresPermission(value="QUERY_ALL_PACKAGES", conditional=true) public android.app.WallpaperInfo getWallpaperInfo();
method @Nullable public android.app.WallpaperInfo getWallpaperInfo(int);
method public boolean hasResourceWallpaper(@RawRes int);
method public boolean isSetWallpaperAllowed();
@@ -9696,8 +9698,10 @@
method public void requestNotificationAccess(android.content.ComponentName);
method @FlaggedApi("android.companion.association_tag") public void setAssociationTag(int, @NonNull String);
method @RequiresPermission(android.Manifest.permission.REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE) public void startObservingDevicePresence(@NonNull String) throws android.companion.DeviceNotAssociatedException;
+ method @FlaggedApi("android.companion.device_presence") @RequiresPermission(android.Manifest.permission.REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE) public void startObservingDevicePresence(@NonNull android.companion.ObservingDevicePresenceRequest);
method public void startSystemDataTransfer(int, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Void,android.companion.CompanionException>) throws android.companion.DeviceNotAssociatedException;
method @RequiresPermission(android.Manifest.permission.REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE) public void stopObservingDevicePresence(@NonNull String) throws android.companion.DeviceNotAssociatedException;
+ method @FlaggedApi("android.companion.device_presence") @RequiresPermission(android.Manifest.permission.REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE) public void stopObservingDevicePresence(@NonNull android.companion.ObservingDevicePresenceRequest);
field public static final String EXTRA_ASSOCIATION = "android.companion.extra.ASSOCIATION";
field @Deprecated public static final String EXTRA_DEVICE = "android.companion.extra.DEVICE";
field public static final int FLAG_CALL_METADATA = 1; // 0x1
@@ -9725,13 +9729,7 @@
method @MainThread public void onDeviceAppeared(@NonNull android.companion.AssociationInfo);
method @Deprecated @MainThread public void onDeviceDisappeared(@NonNull String);
method @MainThread public void onDeviceDisappeared(@NonNull android.companion.AssociationInfo);
- method @FlaggedApi("android.companion.device_presence") @MainThread public void onDeviceEvent(@NonNull android.companion.AssociationInfo, int);
- field @FlaggedApi("android.companion.device_presence") public static final int DEVICE_EVENT_BLE_APPEARED = 0; // 0x0
- field @FlaggedApi("android.companion.device_presence") public static final int DEVICE_EVENT_BLE_DISAPPEARED = 1; // 0x1
- field @FlaggedApi("android.companion.device_presence") public static final int DEVICE_EVENT_BT_CONNECTED = 2; // 0x2
- field @FlaggedApi("android.companion.device_presence") public static final int DEVICE_EVENT_BT_DISCONNECTED = 3; // 0x3
- field @FlaggedApi("android.companion.device_presence") public static final int DEVICE_EVENT_SELF_MANAGED_APPEARED = 4; // 0x4
- field @FlaggedApi("android.companion.device_presence") public static final int DEVICE_EVENT_SELF_MANAGED_DISAPPEARED = 5; // 0x5
+ method @FlaggedApi("android.companion.device_presence") @MainThread public void onDevicePresenceEvent(@NonNull android.companion.DevicePresenceEvent);
field public static final String SERVICE_INTERFACE = "android.companion.CompanionDeviceService";
}
@@ -9744,6 +9742,38 @@
public class DeviceNotAssociatedException extends java.lang.RuntimeException {
}
+ @FlaggedApi("android.companion.device_presence") public final class DevicePresenceEvent implements android.os.Parcelable {
+ ctor public DevicePresenceEvent(int, int, @Nullable android.os.ParcelUuid);
+ method public int describeContents();
+ method public int getAssociationId();
+ method public int getEvent();
+ method @Nullable public android.os.ParcelUuid getUuid();
+ method public void writeToParcel(@NonNull android.os.Parcel, int);
+ field @NonNull public static final android.os.Parcelable.Creator<android.companion.DevicePresenceEvent> CREATOR;
+ field public static final int EVENT_BLE_APPEARED = 0; // 0x0
+ field public static final int EVENT_BLE_DISAPPEARED = 1; // 0x1
+ field public static final int EVENT_BT_CONNECTED = 2; // 0x2
+ field public static final int EVENT_BT_DISCONNECTED = 3; // 0x3
+ field public static final int EVENT_SELF_MANAGED_APPEARED = 4; // 0x4
+ field public static final int EVENT_SELF_MANAGED_DISAPPEARED = 5; // 0x5
+ field public static final int NO_ASSOCIATION = -1; // 0xffffffff
+ }
+
+ @FlaggedApi("android.companion.device_presence") public final class ObservingDevicePresenceRequest implements android.os.Parcelable {
+ method public int describeContents();
+ method public int getAssociationId();
+ method @Nullable public android.os.ParcelUuid getUuid();
+ method public void writeToParcel(@NonNull android.os.Parcel, int);
+ field @NonNull public static final android.os.Parcelable.Creator<android.companion.ObservingDevicePresenceRequest> CREATOR;
+ }
+
+ public static final class ObservingDevicePresenceRequest.Builder {
+ ctor public ObservingDevicePresenceRequest.Builder();
+ method @NonNull public android.companion.ObservingDevicePresenceRequest build();
+ method @NonNull public android.companion.ObservingDevicePresenceRequest.Builder setAssociationId(int);
+ method @NonNull @RequiresPermission(android.Manifest.permission.REQUEST_OBSERVE_DEVICE_UUID_PRESENCE) public android.companion.ObservingDevicePresenceRequest.Builder setUuid(@NonNull android.os.ParcelUuid);
+ }
+
public final class WifiDeviceFilter implements android.companion.DeviceFilter<android.net.wifi.ScanResult> {
method public int describeContents();
method public void writeToParcel(@NonNull android.os.Parcel, int);
@@ -12319,7 +12349,6 @@
method public void registerCallback(android.content.pm.LauncherApps.Callback, android.os.Handler);
method public void registerPackageInstallerSessionCallback(@NonNull java.util.concurrent.Executor, @NonNull android.content.pm.PackageInstaller.SessionCallback);
method public android.content.pm.LauncherActivityInfo resolveActivity(android.content.Intent, android.os.UserHandle);
- method @FlaggedApi("android.content.pm.archiving") public void setArchiveCompatibilityOptions(boolean, boolean);
method public boolean shouldHideFromSuggestions(@NonNull String, @NonNull android.os.UserHandle);
method public void startAppDetailsActivity(android.content.ComponentName, android.os.UserHandle, android.graphics.Rect, android.os.Bundle);
method public void startMainActivity(android.content.ComponentName, android.os.UserHandle, android.graphics.Rect, android.os.Bundle);
@@ -12448,6 +12477,7 @@
method @NonNull public android.content.pm.PackageInstaller.Session openSession(int) throws java.io.IOException;
method public void registerSessionCallback(@NonNull android.content.pm.PackageInstaller.SessionCallback);
method public void registerSessionCallback(@NonNull android.content.pm.PackageInstaller.SessionCallback, @NonNull android.os.Handler);
+ method @FlaggedApi("android.content.pm.archiving") @RequiresPermission(anyOf={android.Manifest.permission.INSTALL_PACKAGES, android.Manifest.permission.REQUEST_INSTALL_PACKAGES}) public void reportUnarchivalState(@NonNull android.content.pm.PackageInstaller.UnarchivalState) throws android.content.pm.PackageManager.NameNotFoundException;
method @FlaggedApi("android.content.pm.archiving") @RequiresPermission(anyOf={android.Manifest.permission.INSTALL_PACKAGES, android.Manifest.permission.REQUEST_INSTALL_PACKAGES}) public void reportUnarchivalStatus(int, int, long, @Nullable android.app.PendingIntent) throws android.content.pm.PackageManager.NameNotFoundException;
method @FlaggedApi("android.content.pm.archiving") @RequiresPermission(anyOf={android.Manifest.permission.DELETE_PACKAGES, android.Manifest.permission.REQUEST_DELETE_PACKAGES}) public void requestArchive(@NonNull String, @NonNull android.content.IntentSender) throws android.content.pm.PackageManager.NameNotFoundException;
method @FlaggedApi("android.content.pm.archiving") @RequiresPermission(anyOf={android.Manifest.permission.INSTALL_PACKAGES, android.Manifest.permission.REQUEST_INSTALL_PACKAGES}) public void requestUnarchive(@NonNull String, @NonNull android.content.IntentSender) throws java.io.IOException, android.content.pm.PackageManager.NameNotFoundException;
@@ -12677,6 +12707,14 @@
field public static final int USER_ACTION_UNSPECIFIED = 0; // 0x0
}
+ @FlaggedApi("android.content.pm.archiving") public static final class PackageInstaller.UnarchivalState {
+ method @NonNull public static android.content.pm.PackageInstaller.UnarchivalState createGenericErrorState(int);
+ method @NonNull public static android.content.pm.PackageInstaller.UnarchivalState createInsufficientStorageState(int, long, @Nullable android.app.PendingIntent);
+ method @NonNull public static android.content.pm.PackageInstaller.UnarchivalState createNoConnectivityState(int);
+ method @NonNull public static android.content.pm.PackageInstaller.UnarchivalState createOkState(int);
+ method @NonNull public static android.content.pm.PackageInstaller.UnarchivalState createUserActionRequiredState(int, @NonNull android.app.PendingIntent);
+ }
+
public class PackageItemInfo {
ctor public PackageItemInfo();
ctor public PackageItemInfo(android.content.pm.PackageItemInfo);
@@ -18331,8 +18369,8 @@
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 RG_1616 = 58; // 0x3a
+ field @FlaggedApi("com.android.graphics.hwui.flags.requested_formats_v") public static final int R_16 = 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
@@ -18762,14 +18800,14 @@
}
@FlaggedApi("android.hardware.biometrics.custom_biometric_prompt") public final class PromptContentItemBulletedText implements android.os.Parcelable android.hardware.biometrics.PromptContentItem {
- ctor public PromptContentItemBulletedText(@NonNull CharSequence);
+ ctor public PromptContentItemBulletedText(@NonNull String);
method public int describeContents();
method public void writeToParcel(@NonNull android.os.Parcel, int);
field @NonNull public static final android.os.Parcelable.Creator<android.hardware.biometrics.PromptContentItemBulletedText> CREATOR;
}
@FlaggedApi("android.hardware.biometrics.custom_biometric_prompt") public final class PromptContentItemPlainText implements android.os.Parcelable android.hardware.biometrics.PromptContentItem {
- ctor public PromptContentItemPlainText(@NonNull CharSequence);
+ ctor public PromptContentItemPlainText(@NonNull String);
method public int describeContents();
method public void writeToParcel(@NonNull android.os.Parcel, int);
field @NonNull public static final android.os.Parcelable.Creator<android.hardware.biometrics.PromptContentItemPlainText> CREATOR;
@@ -18780,7 +18818,7 @@
@FlaggedApi("android.hardware.biometrics.custom_biometric_prompt") public final class PromptVerticalListContentView implements android.os.Parcelable android.hardware.biometrics.PromptContentView {
method public int describeContents();
- method @Nullable public CharSequence getDescription();
+ method @Nullable public String getDescription();
method @NonNull public java.util.List<android.hardware.biometrics.PromptContentItem> getListItems();
method public static int getMaxEachItemCharacterNumber();
method public static int getMaxItemCount();
@@ -18793,7 +18831,7 @@
method @NonNull public android.hardware.biometrics.PromptVerticalListContentView.Builder addListItem(@NonNull android.hardware.biometrics.PromptContentItem);
method @NonNull public android.hardware.biometrics.PromptVerticalListContentView.Builder addListItem(@NonNull android.hardware.biometrics.PromptContentItem, int);
method @NonNull public android.hardware.biometrics.PromptVerticalListContentView build();
- method @NonNull public android.hardware.biometrics.PromptVerticalListContentView.Builder setDescription(@NonNull CharSequence);
+ method @NonNull public android.hardware.biometrics.PromptVerticalListContentView.Builder setDescription(@NonNull String);
}
}
@@ -22158,11 +22196,10 @@
@FlaggedApi("android.media.audio.loudness_configurator_api") public class LoudnessCodecController implements java.lang.AutoCloseable {
method @FlaggedApi("android.media.audio.loudness_configurator_api") public boolean addMediaCodec(@NonNull android.media.MediaCodec);
- method public void close();
+ method @FlaggedApi("android.media.audio.loudness_configurator_api") public void close();
method @FlaggedApi("android.media.audio.loudness_configurator_api") @NonNull public static android.media.LoudnessCodecController create(int);
method @FlaggedApi("android.media.audio.loudness_configurator_api") @NonNull public static android.media.LoudnessCodecController create(int, @NonNull java.util.concurrent.Executor, @NonNull android.media.LoudnessCodecController.OnLoudnessCodecUpdateListener);
method @FlaggedApi("android.media.audio.loudness_configurator_api") @NonNull public android.os.Bundle getLoudnessCodecParams(@NonNull android.media.MediaCodec);
- method @FlaggedApi("android.media.audio.loudness_configurator_api") public void release();
method @FlaggedApi("android.media.audio.loudness_configurator_api") public void removeMediaCodec(@NonNull android.media.MediaCodec);
}
@@ -24262,7 +24299,7 @@
method @Nullable public android.media.MediaRouter2.RoutingController getController(@NonNull String);
method @NonNull public java.util.List<android.media.MediaRouter2.RoutingController> getControllers();
method @NonNull public static android.media.MediaRouter2 getInstance(@NonNull android.content.Context);
- method @FlaggedApi("com.android.media.flags.enable_cross_user_routing_in_media_router2") @NonNull @RequiresPermission(anyOf={android.Manifest.permission.MEDIA_CONTENT_CONTROL, android.Manifest.permission.MEDIA_ROUTING_CONTROL}) public static android.media.MediaRouter2 getInstance(@NonNull android.content.Context, @NonNull android.os.Looper, @NonNull String, @NonNull android.os.UserHandle);
+ method @FlaggedApi("com.android.media.flags.enable_cross_user_routing_in_media_router2") @Nullable @RequiresPermission(anyOf={android.Manifest.permission.MEDIA_CONTENT_CONTROL, android.Manifest.permission.MEDIA_ROUTING_CONTROL}) public static android.media.MediaRouter2 getInstance(@NonNull android.content.Context, @NonNull String);
method @FlaggedApi("com.android.media.flags.enable_rlp_callbacks_in_media_router2") @Nullable public android.media.RouteListingPreference getRouteListingPreference();
method @NonNull public java.util.List<android.media.MediaRoute2Info> getRoutes();
method @NonNull public android.media.MediaRouter2.RoutingController getSystemController();
@@ -25478,34 +25515,34 @@
field public short preset;
}
- public class Virtualizer extends android.media.audiofx.AudioEffect {
- ctor public Virtualizer(int, int) throws java.lang.IllegalArgumentException, java.lang.IllegalStateException, java.lang.RuntimeException, java.lang.UnsupportedOperationException;
- method public boolean canVirtualize(int, int) throws java.lang.IllegalArgumentException, java.lang.IllegalStateException, java.lang.UnsupportedOperationException;
- method public boolean forceVirtualizationMode(int) throws java.lang.IllegalArgumentException, java.lang.IllegalStateException, java.lang.UnsupportedOperationException;
- method public android.media.audiofx.Virtualizer.Settings getProperties() throws java.lang.IllegalArgumentException, java.lang.IllegalStateException, java.lang.UnsupportedOperationException;
- method public short getRoundedStrength() throws java.lang.IllegalArgumentException, java.lang.IllegalStateException, java.lang.UnsupportedOperationException;
- method public boolean getSpeakerAngles(int, int, int[]) throws java.lang.IllegalArgumentException, java.lang.IllegalStateException, java.lang.UnsupportedOperationException;
- method public boolean getStrengthSupported();
- method public int getVirtualizationMode() throws java.lang.IllegalStateException, java.lang.UnsupportedOperationException;
- method public void setParameterListener(android.media.audiofx.Virtualizer.OnParameterChangeListener);
- method public void setProperties(android.media.audiofx.Virtualizer.Settings) throws java.lang.IllegalArgumentException, java.lang.IllegalStateException, java.lang.UnsupportedOperationException;
- method public void setStrength(short) throws java.lang.IllegalArgumentException, java.lang.IllegalStateException, java.lang.UnsupportedOperationException;
- field public static final int PARAM_STRENGTH = 1; // 0x1
- field public static final int PARAM_STRENGTH_SUPPORTED = 0; // 0x0
- field public static final int VIRTUALIZATION_MODE_AUTO = 1; // 0x1
- field public static final int VIRTUALIZATION_MODE_BINAURAL = 2; // 0x2
- field public static final int VIRTUALIZATION_MODE_OFF = 0; // 0x0
- field public static final int VIRTUALIZATION_MODE_TRANSAURAL = 3; // 0x3
+ @Deprecated public class Virtualizer extends android.media.audiofx.AudioEffect {
+ ctor @Deprecated public Virtualizer(int, int) throws java.lang.IllegalArgumentException, java.lang.IllegalStateException, java.lang.RuntimeException, java.lang.UnsupportedOperationException;
+ method @Deprecated public boolean canVirtualize(int, int) throws java.lang.IllegalArgumentException, java.lang.IllegalStateException, java.lang.UnsupportedOperationException;
+ method @Deprecated public boolean forceVirtualizationMode(int) throws java.lang.IllegalArgumentException, java.lang.IllegalStateException, java.lang.UnsupportedOperationException;
+ method @Deprecated public android.media.audiofx.Virtualizer.Settings getProperties() throws java.lang.IllegalArgumentException, java.lang.IllegalStateException, java.lang.UnsupportedOperationException;
+ method @Deprecated public short getRoundedStrength() throws java.lang.IllegalArgumentException, java.lang.IllegalStateException, java.lang.UnsupportedOperationException;
+ method @Deprecated public boolean getSpeakerAngles(int, int, int[]) throws java.lang.IllegalArgumentException, java.lang.IllegalStateException, java.lang.UnsupportedOperationException;
+ method @Deprecated public boolean getStrengthSupported();
+ method @Deprecated public int getVirtualizationMode() throws java.lang.IllegalStateException, java.lang.UnsupportedOperationException;
+ method @Deprecated public void setParameterListener(android.media.audiofx.Virtualizer.OnParameterChangeListener);
+ method @Deprecated public void setProperties(android.media.audiofx.Virtualizer.Settings) throws java.lang.IllegalArgumentException, java.lang.IllegalStateException, java.lang.UnsupportedOperationException;
+ method @Deprecated public void setStrength(short) throws java.lang.IllegalArgumentException, java.lang.IllegalStateException, java.lang.UnsupportedOperationException;
+ field @Deprecated public static final int PARAM_STRENGTH = 1; // 0x1
+ field @Deprecated public static final int PARAM_STRENGTH_SUPPORTED = 0; // 0x0
+ field @Deprecated public static final int VIRTUALIZATION_MODE_AUTO = 1; // 0x1
+ field @Deprecated public static final int VIRTUALIZATION_MODE_BINAURAL = 2; // 0x2
+ field @Deprecated public static final int VIRTUALIZATION_MODE_OFF = 0; // 0x0
+ field @Deprecated public static final int VIRTUALIZATION_MODE_TRANSAURAL = 3; // 0x3
}
- public static interface Virtualizer.OnParameterChangeListener {
- method public void onParameterChange(android.media.audiofx.Virtualizer, int, int, short);
+ @Deprecated public static interface Virtualizer.OnParameterChangeListener {
+ method @Deprecated public void onParameterChange(android.media.audiofx.Virtualizer, int, int, short);
}
- public static class Virtualizer.Settings {
- ctor public Virtualizer.Settings();
- ctor public Virtualizer.Settings(String);
- field public short strength;
+ @Deprecated public static class Virtualizer.Settings {
+ ctor @Deprecated public Virtualizer.Settings();
+ ctor @Deprecated public Virtualizer.Settings(String);
+ field @Deprecated public short strength;
}
public class Visualizer {
@@ -25678,9 +25715,48 @@
field public static final String KEY_STATSD_ATOM = "bundlesession-statsd-atom";
}
+ @FlaggedApi("com.android.media.editing.flags.add_media_metrics_editing") public final class EditingEndedEvent extends android.media.metrics.Event implements android.os.Parcelable {
+ method public int describeContents();
+ method public int getErrorCode();
+ method public int getFinalState();
+ method public void writeToParcel(@NonNull android.os.Parcel, int);
+ field @NonNull public static final android.os.Parcelable.Creator<android.media.metrics.EditingEndedEvent> CREATOR;
+ field public static final int ERROR_CODE_AUDIO_PROCESSING_FAILED = 18; // 0x12
+ field public static final int ERROR_CODE_DECODER_INIT_FAILED = 11; // 0xb
+ field public static final int ERROR_CODE_DECODING_FAILED = 12; // 0xc
+ field public static final int ERROR_CODE_DECODING_FORMAT_UNSUPPORTED = 13; // 0xd
+ field public static final int ERROR_CODE_ENCODER_INIT_FAILED = 14; // 0xe
+ field public static final int ERROR_CODE_ENCODING_FAILED = 15; // 0xf
+ field public static final int ERROR_CODE_ENCODING_FORMAT_UNSUPPORTED = 16; // 0x10
+ field public static final int ERROR_CODE_FAILED_RUNTIME_CHECK = 2; // 0x2
+ field public static final int ERROR_CODE_IO_BAD_HTTP_STATUS = 6; // 0x6
+ field public static final int ERROR_CODE_IO_CLEARTEXT_NOT_PERMITTED = 9; // 0x9
+ field public static final int ERROR_CODE_IO_FILE_NOT_FOUND = 7; // 0x7
+ field public static final int ERROR_CODE_IO_NETWORK_CONNECTION_FAILED = 4; // 0x4
+ field public static final int ERROR_CODE_IO_NETWORK_CONNECTION_TIMEOUT = 5; // 0x5
+ field public static final int ERROR_CODE_IO_NO_PERMISSION = 8; // 0x8
+ field public static final int ERROR_CODE_IO_READ_POSITION_OUT_OF_RANGE = 10; // 0xa
+ field public static final int ERROR_CODE_IO_UNSPECIFIED = 3; // 0x3
+ field public static final int ERROR_CODE_MUXING_FAILED = 19; // 0x13
+ field public static final int ERROR_CODE_NONE = 1; // 0x1
+ field public static final int ERROR_CODE_VIDEO_FRAME_PROCESSING_FAILED = 17; // 0x11
+ field public static final int FINAL_STATE_CANCELED = 2; // 0x2
+ field public static final int FINAL_STATE_ERROR = 3; // 0x3
+ field public static final int FINAL_STATE_SUCCEEDED = 1; // 0x1
+ }
+
+ @FlaggedApi("com.android.media.editing.flags.add_media_metrics_editing") public static final class EditingEndedEvent.Builder {
+ ctor public EditingEndedEvent.Builder(int);
+ method @NonNull public android.media.metrics.EditingEndedEvent build();
+ method @NonNull public android.media.metrics.EditingEndedEvent.Builder setErrorCode(int);
+ method @NonNull public android.media.metrics.EditingEndedEvent.Builder setMetricsBundle(@NonNull android.os.Bundle);
+ method @NonNull public android.media.metrics.EditingEndedEvent.Builder setTimeSinceCreatedMillis(@IntRange(from=0xffffffff) long);
+ }
+
public final class EditingSession implements java.lang.AutoCloseable {
method public void close();
method @NonNull public android.media.metrics.LogSessionId getSessionId();
+ method @FlaggedApi("com.android.media.editing.flags.add_media_metrics_editing") public void reportEditingEndedEvent(@NonNull android.media.metrics.EditingEndedEvent);
}
public abstract class Event {
@@ -36585,6 +36661,7 @@
field @FlaggedApi("com.android.media.flags.enable_privileged_routing_for_media_routing_control") public static final String ACTION_REQUEST_MEDIA_ROUTING_CONTROL = "android.settings.REQUEST_MEDIA_ROUTING_CONTROL";
field public static final String ACTION_REQUEST_SCHEDULE_EXACT_ALARM = "android.settings.REQUEST_SCHEDULE_EXACT_ALARM";
field public static final String ACTION_REQUEST_SET_AUTOFILL_SERVICE = "android.settings.REQUEST_SET_AUTOFILL_SERVICE";
+ field @FlaggedApi("com.android.internal.telephony.flags.carrier_enabled_satellite_flag") public static final String ACTION_SATELLITE_SETTING = "android.settings.SATELLITE_SETTING";
field public static final String ACTION_SEARCH_SETTINGS = "android.search.action.SEARCH_SETTINGS";
field public static final String ACTION_SECURITY_SETTINGS = "android.settings.SECURITY_SETTINGS";
field public static final String ACTION_SETTINGS = "android.settings.SETTINGS";
@@ -40537,7 +40614,7 @@
method public int getPriorityCategoryReminders();
method public int getPriorityCategoryRepeatCallers();
method public int getPriorityCategorySystem();
- method @FlaggedApi("android.app.modes_api") public int getPriorityChannels();
+ method @FlaggedApi("android.app.modes_api") public int getPriorityChannelsAllowed();
method public int getPriorityConversationSenders();
method public int getPriorityMessageSenders();
method public int getVisualEffectAmbient();
@@ -41665,10 +41742,10 @@
method public void disconnect(@NonNull android.telecom.DisconnectCause, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Void,android.telecom.CallException>);
method @NonNull public android.os.ParcelUuid getCallId();
method public void requestCallEndpointChange(@NonNull android.telecom.CallEndpoint, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Void,android.telecom.CallException>);
+ method @FlaggedApi("com.android.server.telecom.flags.set_mute_state") public void requestMuteState(boolean, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Void,android.telecom.CallException>);
method public void sendEvent(@NonNull String, @NonNull android.os.Bundle);
method public void setActive(@NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Void,android.telecom.CallException>);
method public void setInactive(@NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Void,android.telecom.CallException>);
- method @FlaggedApi("com.android.server.telecom.flags.set_mute_state") public void setMuteState(boolean, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Void,android.telecom.CallException>);
method public void startCallStreaming(@NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Void,android.telecom.CallException>);
}
@@ -44954,7 +45031,7 @@
method public void addOnSubscriptionsChangedListener(@NonNull java.util.concurrent.Executor, @NonNull android.telephony.SubscriptionManager.OnSubscriptionsChangedListener);
method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void addSubscriptionsIntoGroup(@NonNull java.util.List<java.lang.Integer>, @NonNull android.os.ParcelUuid);
method public boolean canManageSubscription(android.telephony.SubscriptionInfo);
- method @FlaggedApi("com.android.internal.telephony.flags.enforce_subscription_user_filter") @NonNull public android.telephony.SubscriptionManager createForAllUserProfiles();
+ method @FlaggedApi("com.android.internal.telephony.flags.enforce_subscription_user_filter") @NonNull @RequiresPermission(android.Manifest.permission.INTERACT_ACROSS_PROFILES) public android.telephony.SubscriptionManager createForAllUserProfiles();
method @NonNull @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public android.os.ParcelUuid createSubscriptionGroup(@NonNull java.util.List<java.lang.Integer>);
method @Deprecated public static android.telephony.SubscriptionManager from(android.content.Context);
method public java.util.List<android.telephony.SubscriptionInfo> getAccessibleSubscriptionInfoList();
@@ -52426,9 +52503,9 @@
field protected static final int[] PRESSED_STATE_SET;
field protected static final int[] PRESSED_WINDOW_FOCUSED_STATE_SET;
field @FlaggedApi("android.view.flags.toolkit_set_frame_rate_read_only") public static final float REQUESTED_FRAME_RATE_CATEGORY_DEFAULT = 0.0f;
- field @FlaggedApi("android.view.flags.toolkit_set_frame_rate_read_only") public static final float REQUESTED_FRAME_RATE_CATEGORY_HIGH = -120.0f;
- field @FlaggedApi("android.view.flags.toolkit_set_frame_rate_read_only") public static final float REQUESTED_FRAME_RATE_CATEGORY_LOW = -30.0f;
- field @FlaggedApi("android.view.flags.toolkit_set_frame_rate_read_only") public static final float REQUESTED_FRAME_RATE_CATEGORY_NORMAL = -60.0f;
+ field @FlaggedApi("android.view.flags.toolkit_set_frame_rate_read_only") public static final float REQUESTED_FRAME_RATE_CATEGORY_HIGH = -4.0f;
+ field @FlaggedApi("android.view.flags.toolkit_set_frame_rate_read_only") public static final float REQUESTED_FRAME_RATE_CATEGORY_LOW = -2.0f;
+ field @FlaggedApi("android.view.flags.toolkit_set_frame_rate_read_only") public static final float REQUESTED_FRAME_RATE_CATEGORY_NORMAL = -3.0f;
field @FlaggedApi("android.view.flags.toolkit_set_frame_rate_read_only") public static final float REQUESTED_FRAME_RATE_CATEGORY_NO_PREFERENCE = -1.0f;
field public static final android.util.Property<android.view.View,java.lang.Float> ROTATION;
field public static final android.util.Property<android.view.View,java.lang.Float> ROTATION_X;
@@ -57082,7 +57159,7 @@
method public abstract boolean getBuiltInZoomControls();
method public abstract int getCacheMode();
method public abstract String getCursiveFontFamily();
- method @Deprecated public abstract boolean getDatabaseEnabled();
+ method public abstract boolean getDatabaseEnabled();
method @Deprecated public abstract String getDatabasePath();
method public abstract int getDefaultFixedFontSize();
method public abstract int getDefaultFontSize();
@@ -57128,7 +57205,7 @@
method public abstract void setBuiltInZoomControls(boolean);
method public abstract void setCacheMode(int);
method public abstract void setCursiveFontFamily(String);
- method @Deprecated public abstract void setDatabaseEnabled(boolean);
+ method public abstract void setDatabaseEnabled(boolean);
method @Deprecated public abstract void setDatabasePath(String);
method public abstract void setDefaultFixedFontSize(int);
method public abstract void setDefaultFontSize(int);
@@ -59259,6 +59336,7 @@
ctor public RemoteViews(@NonNull java.util.Map<android.util.SizeF,android.widget.RemoteViews>);
ctor public RemoteViews(android.widget.RemoteViews);
ctor public RemoteViews(android.os.Parcel);
+ ctor @FlaggedApi("android.appwidget.flags.draw_data_parcel") public RemoteViews(@NonNull android.widget.RemoteViews.DrawInstructions);
method public void addStableView(@IdRes int, @NonNull android.widget.RemoteViews, int);
method public void addView(@IdRes int, android.widget.RemoteViews);
method public android.view.View apply(android.content.Context, android.view.ViewGroup);
@@ -59367,6 +59445,15 @@
ctor public RemoteViews.ActionException(String);
}
+ @FlaggedApi("android.appwidget.flags.draw_data_parcel") public static final class RemoteViews.DrawInstructions {
+ method @FlaggedApi("android.appwidget.flags.draw_data_parcel") public void appendInstructions(@NonNull byte[]);
+ }
+
+ @FlaggedApi("android.appwidget.flags.draw_data_parcel") public static final class RemoteViews.DrawInstructions.Builder {
+ ctor @FlaggedApi("android.appwidget.flags.draw_data_parcel") public RemoteViews.DrawInstructions.Builder(@NonNull java.util.List<byte[]>);
+ method @FlaggedApi("android.appwidget.flags.draw_data_parcel") @NonNull public android.widget.RemoteViews.DrawInstructions build();
+ }
+
public static final class RemoteViews.RemoteCollectionItems implements android.os.Parcelable {
method public int describeContents();
method public int getItemCount();
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index debf1bf..d18001d4 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -22,7 +22,7 @@
field public static final String ACCESS_RCS_USER_CAPABILITY_EXCHANGE = "android.permission.ACCESS_RCS_USER_CAPABILITY_EXCHANGE";
field public static final String ACCESS_SHARED_LIBRARIES = "android.permission.ACCESS_SHARED_LIBRARIES";
field public static final String ACCESS_SHORTCUTS = "android.permission.ACCESS_SHORTCUTS";
- field public static final String ACCESS_SMARTSPACE = "android.permission.ACCESS_SMARTSPACE";
+ field @FlaggedApi("android.app.smartspace.flags.access_smartspace") public static final String ACCESS_SMARTSPACE = "android.permission.ACCESS_SMARTSPACE";
field public static final String ACCESS_SURFACE_FLINGER = "android.permission.ACCESS_SURFACE_FLINGER";
field public static final String ACCESS_TUNED_INFO = "android.permission.ACCESS_TUNED_INFO";
field public static final String ACCESS_TV_DESCRAMBLER = "android.permission.ACCESS_TV_DESCRAMBLER";
@@ -56,7 +56,7 @@
field public static final String BIND_CONTENT_SUGGESTIONS_SERVICE = "android.permission.BIND_CONTENT_SUGGESTIONS_SERVICE";
field public static final String BIND_DIRECTORY_SEARCH = "android.permission.BIND_DIRECTORY_SEARCH";
field public static final String BIND_DISPLAY_HASHING_SERVICE = "android.permission.BIND_DISPLAY_HASHING_SERVICE";
- field @FlaggedApi("com.android.internal.telephony.flags.ap_domain_selection_enabled") public static final String BIND_DOMAIN_SELECTION_SERVICE = "android.permission.BIND_DOMAIN_SELECTION_SERVICE";
+ field @FlaggedApi("com.android.internal.telephony.flags.use_oem_domain_selection_service") public static final String BIND_DOMAIN_SELECTION_SERVICE = "android.permission.BIND_DOMAIN_SELECTION_SERVICE";
field public static final String BIND_DOMAIN_VERIFICATION_AGENT = "android.permission.BIND_DOMAIN_VERIFICATION_AGENT";
field public static final String BIND_EUICC_SERVICE = "android.permission.BIND_EUICC_SERVICE";
field public static final String BIND_EXTERNAL_STORAGE_SERVICE = "android.permission.BIND_EXTERNAL_STORAGE_SERVICE";
@@ -190,6 +190,7 @@
field public static final String MANAGE_DEFAULT_APPLICATIONS = "android.permission.MANAGE_DEFAULT_APPLICATIONS";
field public static final String MANAGE_DEVICE_ADMINS = "android.permission.MANAGE_DEVICE_ADMINS";
field public static final String MANAGE_DEVICE_POLICY_APP_EXEMPTIONS = "android.permission.MANAGE_DEVICE_POLICY_APP_EXEMPTIONS";
+ field @FlaggedApi("android.permission.flags.enhanced_confirmation_mode_apis_enabled") public static final String MANAGE_ENHANCED_CONFIRMATION_STATES = "android.permission.MANAGE_ENHANCED_CONFIRMATION_STATES";
field public static final String MANAGE_ETHERNET_NETWORKS = "android.permission.MANAGE_ETHERNET_NETWORKS";
field public static final String MANAGE_FACTORY_RESET_PROTECTION = "android.permission.MANAGE_FACTORY_RESET_PROTECTION";
field public static final String MANAGE_GAME_ACTIVITY = "android.permission.MANAGE_GAME_ACTIVITY";
@@ -3959,6 +3960,7 @@
method public void setInstallAsInstantApp(boolean);
method public void setInstallAsVirtualPreload();
method public void setRequestDowngrade(boolean);
+ method @FlaggedApi("android.content.pm.recoverability_detection") @RequiresPermission(android.Manifest.permission.MANAGE_ROLLBACKS) public void setRollbackImpactLevel(int);
method @FlaggedApi("android.content.pm.rollback_lifetime") @RequiresPermission(android.Manifest.permission.MANAGE_ROLLBACKS) public void setRollbackLifetimeMillis(long);
method @RequiresPermission(android.Manifest.permission.INSTALL_PACKAGES) public void setStaged();
}
@@ -4127,6 +4129,9 @@
field public static final int ROLLBACK_DATA_POLICY_RESTORE = 0; // 0x0
field public static final int ROLLBACK_DATA_POLICY_RETAIN = 2; // 0x2
field public static final int ROLLBACK_DATA_POLICY_WIPE = 1; // 0x1
+ field @FlaggedApi("android.content.pm.recoverability_detection") public static final int ROLLBACK_USER_IMPACT_HIGH = 1; // 0x1
+ field @FlaggedApi("android.content.pm.recoverability_detection") public static final int ROLLBACK_USER_IMPACT_LOW = 0; // 0x0
+ field @FlaggedApi("android.content.pm.recoverability_detection") public static final int ROLLBACK_USER_IMPACT_ONLY_MANUAL = 2; // 0x2
field public static final int SYSTEM_APP_STATE_HIDDEN_UNTIL_INSTALLED_HIDDEN = 0; // 0x0
field public static final int SYSTEM_APP_STATE_HIDDEN_UNTIL_INSTALLED_VISIBLE = 1; // 0x1
field public static final int SYSTEM_APP_STATE_INSTALLED = 2; // 0x2
@@ -6881,7 +6886,6 @@
public final class MediaRouter2 {
method @NonNull public java.util.List<android.media.MediaRoute2Info> getAllRoutes();
method @Nullable public String getClientPackageName();
- method @Nullable @RequiresPermission(android.Manifest.permission.MEDIA_CONTENT_CONTROL) public static android.media.MediaRouter2 getInstance(@NonNull android.content.Context, @NonNull String);
method @RequiresPermission(android.Manifest.permission.MEDIA_CONTENT_CONTROL) public void setRouteVolume(@NonNull android.media.MediaRoute2Info, int);
method @RequiresPermission(android.Manifest.permission.MEDIA_CONTENT_CONTROL) public void startScan();
method @RequiresPermission(android.Manifest.permission.MEDIA_CONTENT_CONTROL) public void stopScan();
@@ -14712,7 +14716,7 @@
method @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public int getEmergencyNumberDbVersion();
method @Nullable @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public String getIsimDomain();
method @Nullable @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public String getIsimIst();
- method @FlaggedApi("com.android.server.telecom.flags.telecom_resolve_hidden_dependencies") @Nullable @RequiresPermission(allOf={android.Manifest.permission.ACCESS_FINE_LOCATION, "com.android.phone.permission.ACCESS_LAST_KNOWN_CELL_ID"}) public android.telephony.CellIdentity getLastKnownCellIdentity();
+ method @FlaggedApi("com.android.server.telecom.flags.telecom_resolve_hidden_dependencies") @Nullable @RequiresPermission(allOf={android.Manifest.permission.ACCESS_FINE_LOCATION, android.Manifest.permission.ACCESS_LAST_KNOWN_CELL_ID}) public android.telephony.CellIdentity getLastKnownCellIdentity();
method @Deprecated @NonNull @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public java.util.Map<java.lang.Integer,java.lang.Integer> getLogicalToPhysicalSlotMapping();
method public int getMaxNumberOfSimultaneouslyActiveSims();
method public static long getMaxNumberVerificationTimeoutMillis();
@@ -17154,7 +17158,7 @@
method @FlaggedApi("com.android.internal.telephony.flags.carrier_enabled_satellite_flag") @NonNull @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public java.util.Set<java.lang.Integer> getSatelliteAttachRestrictionReasonsForCarrier(int);
method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void pollPendingSatelliteDatagrams(@NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Integer>);
method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void provisionSatelliteService(@NonNull String, @NonNull byte[], @Nullable android.os.CancellationSignal, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Integer>);
- method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void registerForNtnSignalStrengthChanged(@NonNull java.util.concurrent.Executor, @NonNull android.telephony.satellite.NtnSignalStrengthCallback) throws android.telephony.satellite.SatelliteManager.SatelliteException;
+ method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void registerForNtnSignalStrengthChanged(@NonNull java.util.concurrent.Executor, @NonNull android.telephony.satellite.NtnSignalStrengthCallback);
method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public int registerForSatelliteCapabilitiesChanged(@NonNull java.util.concurrent.Executor, @NonNull android.telephony.satellite.SatelliteCapabilitiesCallback);
method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public int registerForSatelliteDatagram(@NonNull java.util.concurrent.Executor, @NonNull android.telephony.satellite.SatelliteDatagramCallback);
method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public int registerForSatelliteModemStateChanged(@NonNull java.util.concurrent.Executor, @NonNull android.telephony.satellite.SatelliteModemStateCallback);
@@ -17221,6 +17225,7 @@
field @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public static final int SATELLITE_MODEM_STATE_UNKNOWN = -1; // 0xffffffff
field @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public static final int SATELLITE_RESULT_ACCESS_BARRED = 16; // 0x10
field @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public static final int SATELLITE_RESULT_ERROR = 1; // 0x1
+ field @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public static final int SATELLITE_RESULT_ILLEGAL_STATE = 23; // 0x17
field @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public static final int SATELLITE_RESULT_INVALID_ARGUMENTS = 8; // 0x8
field @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public static final int SATELLITE_RESULT_INVALID_MODEM_STATE = 7; // 0x7
field @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public static final int SATELLITE_RESULT_INVALID_TELEPHONY_STATE = 6; // 0x6
diff --git a/core/api/test-current.txt b/core/api/test-current.txt
index 5d271cc..b8b98a3 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -377,6 +377,7 @@
public class NotificationManager {
method @FlaggedApi("android.app.modes_api") @NonNull public String addAutomaticZenRule(@NonNull android.app.AutomaticZenRule, boolean);
method public void cleanUpCallersAfter(long);
+ method @FlaggedApi("android.app.modes_api") @NonNull public android.service.notification.ZenPolicy getDefaultZenPolicy();
method public android.content.ComponentName getEffectsSuppressor();
method public boolean isNotificationPolicyAccessGrantedForPackage(@NonNull String);
method @FlaggedApi("android.app.modes_api") public boolean removeAutomaticZenRule(@NonNull String, boolean);
@@ -1210,6 +1211,10 @@
package android.content.rollback {
+ public final class RollbackInfo implements android.os.Parcelable {
+ method @FlaggedApi("android.content.pm.recoverability_detection") public int getRollbackImpactLevel();
+ }
+
public final class RollbackManager {
method @RequiresPermission(android.Manifest.permission.TEST_MANAGE_ROLLBACKS) public void blockRollbackManager(long);
method @RequiresPermission(android.Manifest.permission.TEST_MANAGE_ROLLBACKS) public void expireRollbackForPackage(@NonNull String);
@@ -3023,6 +3028,10 @@
method @Deprecated public boolean isBound();
}
+ public final class ZenPolicy implements android.os.Parcelable {
+ method @FlaggedApi("android.app.modes_api") @NonNull public android.service.notification.ZenPolicy overwrittenWith(@Nullable android.service.notification.ZenPolicy);
+ }
+
public static final class ZenPolicy.Builder {
ctor public ZenPolicy.Builder(@Nullable android.service.notification.ZenPolicy);
}
diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java
index 949e2ba..1bdbd4c5 100644
--- a/core/java/android/app/ActivityThread.java
+++ b/core/java/android/app/ActivityThread.java
@@ -6792,6 +6792,7 @@
}
}
if (killApp) {
+ // Keep in sync with "perhaps it was removed" case below.
mPackages.remove(packages[i]);
mResourcePackages.remove(packages[i]);
}
@@ -6834,23 +6835,24 @@
PackageManager.GET_SHARED_LIBRARY_FILES,
UserHandle.myUserId());
- if (mActivities.size() > 0) {
- for (ActivityClientRecord ar : mActivities.values()) {
- if (ar.activityInfo.applicationInfo.packageName
- .equals(packageName)) {
- ar.activityInfo.applicationInfo = aInfo;
- ar.packageInfo = pkgInfo;
+ if (aInfo != null) {
+ if (mActivities.size() > 0) {
+ for (ActivityClientRecord ar : mActivities.values()) {
+ if (ar.activityInfo.applicationInfo.packageName
+ .equals(packageName)) {
+ ar.activityInfo.applicationInfo = aInfo;
+ ar.packageInfo = pkgInfo;
+ }
}
}
- }
- final String[] oldResDirs = { pkgInfo.getResDir() };
+ final String[] oldResDirs = {pkgInfo.getResDir()};
- final ArrayList<String> oldPaths = new ArrayList<>();
- LoadedApk.makePaths(this, pkgInfo.getApplicationInfo(), oldPaths);
- pkgInfo.updateApplicationInfo(aInfo, oldPaths);
+ final ArrayList<String> oldPaths = new ArrayList<>();
+ LoadedApk.makePaths(
+ this, pkgInfo.getApplicationInfo(), oldPaths);
+ pkgInfo.updateApplicationInfo(aInfo, oldPaths);
- synchronized (mResourcesManager) {
// Update affected Resources objects to use new ResourcesImpl
mResourcesManager.appendPendingAppInfoUpdate(oldResDirs,
aInfo);
@@ -6858,6 +6860,12 @@
}
} catch (RemoteException e) {
}
+ } else {
+ // No package, perhaps it was removed?
+ Slog.e(TAG, "Package [" + packages[i] + "] reported as REPLACED,"
+ + " but missing application info. Assuming REMOVED.");
+ mPackages.remove(packages[i]);
+ mResourcePackages.remove(packages[i]);
}
}
}
diff --git a/core/java/android/app/AppOpsManager.java b/core/java/android/app/AppOpsManager.java
index ea37e7f..ccd8456 100644
--- a/core/java/android/app/AppOpsManager.java
+++ b/core/java/android/app/AppOpsManager.java
@@ -1548,24 +1548,9 @@
public static final int OP_READ_SYSTEM_GRAMMATICAL_GENDER =
AppProtoEnums.APP_OP_READ_SYSTEM_GRAMMATICAL_GENDER;
- /**
- * Whether the app has enabled to receive the icon overlay for fetching archived apps.
- *
- * @hide
- */
- public static final int OP_ARCHIVE_ICON_OVERLAY = AppProtoEnums.APP_OP_ARCHIVE_ICON_OVERLAY;
-
- /**
- * Whether the app has enabled compatibility support for unarchival.
- *
- * @hide
- */
- public static final int OP_UNARCHIVAL_CONFIRMATION =
- AppProtoEnums.APP_OP_UNARCHIVAL_CONFIRMATION;
-
/** @hide */
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
- public static final int _NUM_OP = 146;
+ public static final int _NUM_OP = 144;
/**
* All app ops represented as strings.
@@ -1715,8 +1700,6 @@
OPSTR_ENABLE_MOBILE_DATA_BY_USER,
OPSTR_RESERVED_FOR_TESTING,
OPSTR_RAPID_CLEAR_NOTIFICATIONS_BY_LISTENER,
- OPSTR_ARCHIVE_ICON_OVERLAY,
- OPSTR_UNARCHIVAL_CONFIRMATION,
})
public @interface AppOpString {}
@@ -2057,20 +2040,6 @@
public static final String OPSTR_MEDIA_ROUTING_CONTROL = "android:media_routing_control";
/**
- * Whether the app has enabled to receive the icon overlay for fetching archived apps.
- *
- * @hide
- */
- public static final String OPSTR_ARCHIVE_ICON_OVERLAY = "android:archive_icon_overlay";
-
- /**
- * Whether the app has enabled compatibility support for unarchival.
- *
- * @hide
- */
- public static final String OPSTR_UNARCHIVAL_CONFIRMATION = "android:unarchival_support";
-
- /**
* AppOp granted to apps that we are started via {@code am instrument -e --no-isolated-storage}
*
* <p>MediaProvider is the only component (outside of system server) that should care about this
@@ -2535,8 +2504,6 @@
OP_RECEIVE_SANDBOXED_DETECTION_TRAINING_DATA,
OP_MEDIA_ROUTING_CONTROL,
OP_READ_SYSTEM_GRAMMATICAL_GENDER,
- OP_ARCHIVE_ICON_OVERLAY,
- OP_UNARCHIVAL_CONFIRMATION,
};
static final AppOpInfo[] sAppOpInfos = new AppOpInfo[]{
@@ -2991,14 +2958,9 @@
.setDefaultMode(AppOpsManager.MODE_ALLOWED).build(),
new AppOpInfo.Builder(OP_READ_SYSTEM_GRAMMATICAL_GENDER,
OPSTR_READ_SYSTEM_GRAMMATICAL_GENDER, "READ_SYSTEM_GRAMMATICAL_GENDER")
- .setPermission(Manifest.permission.READ_SYSTEM_GRAMMATICAL_GENDER)
+ // will make it an app-op permission in the future.
+ // .setPermission(Manifest.permission.READ_SYSTEM_GRAMMATICAL_GENDER)
.build(),
- new AppOpInfo.Builder(OP_ARCHIVE_ICON_OVERLAY, OPSTR_ARCHIVE_ICON_OVERLAY,
- "ARCHIVE_ICON_OVERLAY")
- .setDefaultMode(MODE_ALLOWED).build(),
- new AppOpInfo.Builder(OP_UNARCHIVAL_CONFIRMATION, OPSTR_UNARCHIVAL_CONFIRMATION,
- "UNARCHIVAL_CONFIRMATION")
- .setDefaultMode(MODE_ALLOWED).build(),
};
// The number of longs needed to form a full bitmask of app ops
@@ -3133,7 +3095,7 @@
/**
* Retrieve the permission associated with an operation, or null if there is not one.
-
+ *
* @param op The operation name.
*
* @hide
diff --git a/core/java/android/app/ApplicationPackageManager.java b/core/java/android/app/ApplicationPackageManager.java
index 4f1db7d..34c44f9 100644
--- a/core/java/android/app/ApplicationPackageManager.java
+++ b/core/java/android/app/ApplicationPackageManager.java
@@ -4032,8 +4032,7 @@
private Drawable getArchivedAppIcon(String packageName) {
try {
return new BitmapDrawable(null,
- mPM.getArchivedAppIcon(packageName, new UserHandle(getUserId()),
- mContext.getPackageName()));
+ mPM.getArchivedAppIcon(packageName, new UserHandle(getUserId())));
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
diff --git a/core/java/android/app/ApplicationStartInfo.java b/core/java/android/app/ApplicationStartInfo.java
index afa513d..c6712c0 100644
--- a/core/java/android/app/ApplicationStartInfo.java
+++ b/core/java/android/app/ApplicationStartInfo.java
@@ -413,7 +413,9 @@
* @hide
*/
public void setIntent(Intent startIntent) {
- mStartIntent = startIntent;
+ if (startIntent != null) {
+ mStartIntent = startIntent.maybeStripForHistory();
+ }
}
/**
@@ -548,6 +550,8 @@
/**
* The intent used to launch the application.
*
+ * <p class="note"> Note: Intent is stripped and does not include extras.</p>
+ *
* <p class="note"> Note: field will be set for any {@link #getStartupState} value.</p>
*/
@SuppressLint("IntentBuilderName")
@@ -662,6 +666,7 @@
private static final String PROTO_SERIALIZER_ATTRIBUTE_TIMESTAMP = "timestamp";
private static final String PROTO_SERIALIZER_ATTRIBUTE_KEY = "key";
private static final String PROTO_SERIALIZER_ATTRIBUTE_TS = "ts";
+ private static final String PROTO_SERIALIZER_ATTRIBUTE_INTENT = "intent";
/**
* Write to a protocol buffer output stream. Protocol buffer message definition at {@link
@@ -702,10 +707,17 @@
}
proto.write(ApplicationStartInfoProto.START_TYPE, mStartType);
if (mStartIntent != null) {
- Parcel parcel = Parcel.obtain();
- mStartIntent.writeToParcel(parcel, 0);
- proto.write(ApplicationStartInfoProto.START_INTENT, parcel.marshall());
- parcel.recycle();
+ ByteArrayOutputStream intentBytes = new ByteArrayOutputStream();
+ ObjectOutputStream intentOut = new ObjectOutputStream(intentBytes);
+ TypedXmlSerializer serializer = Xml.resolveSerializer(intentOut);
+ serializer.startDocument(null, true);
+ serializer.startTag(null, PROTO_SERIALIZER_ATTRIBUTE_INTENT);
+ mStartIntent.saveToXml(serializer);
+ serializer.endTag(null, PROTO_SERIALIZER_ATTRIBUTE_INTENT);
+ serializer.endDocument();
+ proto.write(ApplicationStartInfoProto.START_INTENT,
+ intentBytes.toByteArray());
+ intentOut.close();
}
proto.write(ApplicationStartInfoProto.LAUNCH_MODE, mLaunchMode);
proto.end(token);
@@ -772,15 +784,17 @@
mStartType = proto.readInt(ApplicationStartInfoProto.START_TYPE);
break;
case (int) ApplicationStartInfoProto.START_INTENT:
- byte[] startIntentBytes = proto.readBytes(
- ApplicationStartInfoProto.START_INTENT);
- if (startIntentBytes.length > 0) {
- Parcel parcel = Parcel.obtain();
- parcel.unmarshall(startIntentBytes, 0, startIntentBytes.length);
- parcel.setDataPosition(0);
- mStartIntent = Intent.CREATOR.createFromParcel(parcel);
- parcel.recycle();
+ ByteArrayInputStream intentBytes = new ByteArrayInputStream(proto.readBytes(
+ ApplicationStartInfoProto.START_INTENT));
+ ObjectInputStream intentIn = new ObjectInputStream(intentBytes);
+ try {
+ TypedXmlPullParser parser = Xml.resolvePullParser(intentIn);
+ XmlUtils.beginDocument(parser, PROTO_SERIALIZER_ATTRIBUTE_INTENT);
+ mStartIntent = Intent.restoreFromXml(parser);
+ } catch (XmlPullParserException e) {
+ // Intent lost
}
+ intentIn.close();
break;
case (int) ApplicationStartInfoProto.LAUNCH_MODE:
mLaunchMode = proto.readInt(ApplicationStartInfoProto.LAUNCH_MODE);
diff --git a/core/java/android/app/IActivityClientController.aidl b/core/java/android/app/IActivityClientController.aidl
index 7370fc3..5b044f6 100644
--- a/core/java/android/app/IActivityClientController.aidl
+++ b/core/java/android/app/IActivityClientController.aidl
@@ -119,7 +119,7 @@
oneway void setShowWhenLocked(in IBinder token, boolean showWhenLocked);
oneway void setInheritShowWhenLocked(in IBinder token, boolean setInheritShownWhenLocked);
- oneway void setTurnScreenOn(in IBinder token, boolean turnScreenOn);
+ void setTurnScreenOn(in IBinder token, boolean turnScreenOn);
oneway void setAllowCrossUidActivitySwitchFromBelow(in IBinder token, boolean allowed);
oneway void reportActivityFullyDrawn(in IBinder token, boolean restoredFromBundle);
oneway void overrideActivityTransition(IBinder token, boolean open, int enterAnim, int exitAnim,
diff --git a/core/java/android/app/INotificationManager.aidl b/core/java/android/app/INotificationManager.aidl
index c3adbc3..578105f 100644
--- a/core/java/android/app/INotificationManager.aidl
+++ b/core/java/android/app/INotificationManager.aidl
@@ -38,6 +38,7 @@
import android.service.notification.INotificationListener;
import android.service.notification.NotificationListenerFilter;
import android.service.notification.StatusBarNotification;
+import android.service.notification.ZenPolicy;
import android.app.AutomaticZenRule;
import android.service.notification.ZenModeConfig;
@@ -213,6 +214,7 @@
boolean isNotificationPolicyAccessGrantedForPackage(String pkg);
void setNotificationPolicyAccessGranted(String pkg, boolean granted);
void setNotificationPolicyAccessGrantedForUser(String pkg, int userId, boolean granted);
+ ZenPolicy getDefaultZenPolicy();
AutomaticZenRule getAutomaticZenRule(String id);
Map<String, AutomaticZenRule> getAutomaticZenRules();
// TODO: b/310620812 - Remove getZenRules() when MODES_API is inlined.
diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java
index ed0cfbe..a81ad3c 100644
--- a/core/java/android/app/Notification.java
+++ b/core/java/android/app/Notification.java
@@ -5487,6 +5487,15 @@
return mColors;
}
+ /**
+ * @param isHeader If the notification is a notification header
+ * @return An instance of mColors after resolving the palette
+ */
+ private Colors getColors(boolean isHeader) {
+ mColors.resolvePalette(mContext, mN.color, !isHeader && mN.isColorized(), mInNightMode);
+ return mColors;
+ }
+
private void updateBackgroundColor(RemoteViews contentView,
StandardTemplateParams p) {
if (isBackgroundColorized(p)) {
@@ -6618,6 +6627,23 @@
return getColors(p).getContrastColor();
}
+ /**
+ * Gets the foreground color of the small icon. If the notification is colorized, this
+ * is the primary text color, otherwise it's the contrast-adjusted app-provided color.
+ * @hide
+ */
+ public @ColorInt int getSmallIconColor(boolean isHeader) {
+ return getColors(/* isHeader = */ isHeader).getContrastColor();
+ }
+
+ /**
+ * Gets the background color of the notification.
+ * @hide
+ */
+ public @ColorInt int getBackgroundColor(boolean isHeader) {
+ return getColors(/* isHeader = */ isHeader).getBackgroundColor();
+ }
+
/** @return the theme's accent color for colored UI elements. */
private @ColorInt int getPrimaryAccentColor(StandardTemplateParams p) {
return getColors(p).getPrimaryAccentColor();
@@ -8532,6 +8558,8 @@
boolean isImportantConversation = mConversationType == CONVERSATION_TYPE_IMPORTANT;
boolean isHeaderless = !isConversationLayout && isCollapsed;
+ //TODO (b/217799515): ensure mConversationTitle always returns the correct
+ // conversationTitle, probably set mConversationTitle = conversationTitle after this
CharSequence conversationTitle = !TextUtils.isEmpty(super.mBigContentTitle)
? super.mBigContentTitle
: mConversationTitle;
diff --git a/core/java/android/app/NotificationManager.java b/core/java/android/app/NotificationManager.java
index 0b6e24c..366b45b 100644
--- a/core/java/android/app/NotificationManager.java
+++ b/core/java/android/app/NotificationManager.java
@@ -1750,6 +1750,20 @@
@NonNull ComponentName listener, boolean granted) {
setNotificationListenerAccessGranted(listener, granted, true);
}
+ /**
+ * Gets the device-default notification policy as a ZenPolicy.
+ * @hide
+ */
+ @TestApi
+ @FlaggedApi(Flags.FLAG_MODES_API)
+ public @NonNull ZenPolicy getDefaultZenPolicy() {
+ INotificationManager service = getService();
+ try {
+ return service.getDefaultZenPolicy();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
/**
* For apps targeting {@link Build.VERSION_CODES#VANILLA_ICE_CREAM} and above, the
diff --git a/core/java/android/app/WallpaperManager.java b/core/java/android/app/WallpaperManager.java
index 63f37f1..0116ca2 100644
--- a/core/java/android/app/WallpaperManager.java
+++ b/core/java/android/app/WallpaperManager.java
@@ -261,6 +261,13 @@
public static final String COMMAND_GOING_TO_SLEEP = "android.wallpaper.goingtosleep";
/**
+ * Command for {@link #sendWallpaperCommand}: reported when a physical display switch event
+ * happens, e.g. fold and unfold.
+ * @hide
+ */
+ public static final String COMMAND_DISPLAY_SWITCH = "android.wallpaper.displayswitch";
+
+ /**
* Command for {@link #sendWallpaperCommand}: reported when the wallpaper that was already
* set is re-applied by the user.
* @hide
@@ -1892,15 +1899,22 @@
/**
* Returns the information about the home screen wallpaper if its current wallpaper is a live
- * wallpaper component. Otherwise, if the wallpaper is a static image, this returns null.
+ * wallpaper component. Otherwise, if the wallpaper is a static image or is not set, or if the
+ * caller doesn't have the appropriate permissions, this returns {@code null}.
*
* <p>
- * In order to use this, apps should declare a {@code <queries>} tag with the action
- * {@code "android.service.wallpaper.WallpaperService"}. Otherwise,
+ * Before Android U, this method requires the
+ * {@link android.Manifest.permission#QUERY_ALL_PACKAGES} permission.
+ * </p>
+ *
+ * <p>
+ * Starting from Android U, in order to use this, apps should declare a {@code <queries>} tag
+ * with the action {@code "android.service.wallpaper.WallpaperService"}. Otherwise,
* this method will return {@code null} if the caller doesn't otherwise have
* <a href="{@docRoot}training/package-visibility">visibility</a> of the wallpaper package.
* </p>
*/
+ @RequiresPermission(value = "QUERY_ALL_PACKAGES", conditional = true)
public WallpaperInfo getWallpaperInfo() {
return getWallpaperInfoForUser(mContext.getUserId());
}
@@ -1917,19 +1931,14 @@
}
/**
- * Returns the information about the home screen wallpaper if its current wallpaper is a live
- * wallpaper component. Otherwise, if the wallpaper is a static image or is not set, or if the
+ * Returns the information about the designated wallpaper if its current wallpaper is a live
+ * wallpaper component. Otherwise, if the wallpaper is a static image or is not set, or if
* the caller doesn't have the appropriate permissions, this returns {@code null}.
*
* <p>
- * Before Android U, this method requires the
- * {@link android.Manifest.permission#QUERY_ALL_PACKAGES} permission.
- * </p>
- *
- * <p>
- * Starting from Android U, In order to use this, apps should declare a {@code <queries>} tag
- * with the action {@code "android.service.wallpaper.WallpaperService"}. Otherwise,
- * this method will return {@code null} if the caller doesn't otherwise have
+ * In order to use this, apps should declare a {@code <queries>} tag with the action
+ * {@code "android.service.wallpaper.WallpaperService"}. Otherwise, this method will return
+ * {@code null} if the caller doesn't otherwise have
* <a href="{@docRoot}training/package-visibility">visibility</a> of the wallpaper package.
* </p>
*
@@ -1945,7 +1954,7 @@
/**
* Returns the information about the designated wallpaper if its current wallpaper is a live
* wallpaper component. Otherwise, if the wallpaper is a static image or is not set, or if the
- * the caller doesn't have the appropriate permissions, this returns {@code null}.
+ * caller doesn't have the appropriate permissions, this returns {@code null}.
*
* <p>
* In order to use this, apps should declare a {@code <queries>} tag
diff --git a/core/java/android/app/admin/flags/flags.aconfig b/core/java/android/app/admin/flags/flags.aconfig
index 35ce102..b3ecd92 100644
--- a/core/java/android/app/admin/flags/flags.aconfig
+++ b/core/java/android/app/admin/flags/flags.aconfig
@@ -55,3 +55,10 @@
description: "Guards a bugfix that ends the credential input flow if the managed user has not stopped."
bug: "293441361"
}
+
+flag {
+ name: "default_sms_personal_app_suspension_fix_enabled"
+ namespace: "enterprise"
+ description: "Exempt the default sms app of the context user for suspension when calling setPersonalAppsSuspended"
+ bug: "309183330"
+}
diff --git a/core/java/android/app/backup/BackupHelperWithLogger.java b/core/java/android/app/backup/BackupHelperWithLogger.java
new file mode 100644
index 0000000..1a59a53
--- /dev/null
+++ b/core/java/android/app/backup/BackupHelperWithLogger.java
@@ -0,0 +1,59 @@
+/*
+ * 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.app.backup;
+
+import android.os.ParcelFileDescriptor;
+
+/**
+ * Utility class for writing BackupHelpers with added logging capabilities.
+ * Used for passing a logger object to Helper in key shared backup agents
+ *
+ * @hide
+ */
+public abstract class BackupHelperWithLogger implements BackupHelper {
+ private BackupRestoreEventLogger mLogger;
+ private boolean mIsLoggerSet = false;
+
+ public abstract void writeNewStateDescription(ParcelFileDescriptor newState);
+
+ public abstract void restoreEntity(BackupDataInputStream data);
+
+ public abstract void performBackup(ParcelFileDescriptor oldState, BackupDataOutput data,
+ ParcelFileDescriptor newState);
+
+ /**
+ * Gets the logger so that the backuphelper can log success/error for each datatype handled
+ */
+ public BackupRestoreEventLogger getLogger() {
+ return mLogger;
+ }
+
+ /**
+ * Allow the shared backup agent to pass a logger to each of its backup helper
+ */
+ public void setLogger(BackupRestoreEventLogger logger) {
+ mLogger = logger;
+ mIsLoggerSet = true;
+ }
+
+ /**
+ * Allow the helper to check if its shared backup agent has passed a logger
+ */
+ public boolean isLoggerSet() {
+ return mIsLoggerSet;
+ }
+}
diff --git a/core/java/android/app/backup/BlobBackupHelper.java b/core/java/android/app/backup/BlobBackupHelper.java
index 82d0a94c..a55ff48 100644
--- a/core/java/android/app/backup/BlobBackupHelper.java
+++ b/core/java/android/app/backup/BlobBackupHelper.java
@@ -39,7 +39,7 @@
*
* @hide
*/
-public abstract class BlobBackupHelper implements BackupHelper {
+public abstract class BlobBackupHelper extends BackupHelperWithLogger {
private static final String TAG = "BlobBackupHelper";
private static final boolean DEBUG = false;
diff --git a/core/java/android/companion/CompanionDeviceManager.java b/core/java/android/companion/CompanionDeviceManager.java
index 672e3439..d743992 100644
--- a/core/java/android/companion/CompanionDeviceManager.java
+++ b/core/java/android/companion/CompanionDeviceManager.java
@@ -1038,6 +1038,7 @@
}
}
+ // TODO(b/315163162) Add @Deprecated keyword after 24Q2 cut.
/**
* Register to receive callbacks whenever the associated device comes in and out of range.
*
@@ -1094,7 +1095,7 @@
callingUid, callingPid);
}
}
-
+ // TODO(b/315163162) Add @Deprecated keyword after 24Q2 cut.
/**
* Unregister for receiving callbacks whenever the associated device comes in and out of range.
*
@@ -1137,6 +1138,64 @@
}
/**
+ * Register to receive callbacks whenever the associated device comes in and out of range.
+ *
+ * <p>The app doesn't need to remain running in order to receive its callbacks.</p>
+ *
+ * <p>Calling app must check for feature presence of
+ * {@link PackageManager#FEATURE_COMPANION_DEVICE_SETUP} before calling this API.</p>
+ *
+ * <p>For Bluetooth LE devices, this is based on scanning for device with the given address.
+ * The system will scan for the device when Bluetooth is ON or Bluetooth scanning is ON.</p>
+ *
+ * <p>For Bluetooth classic devices this is triggered when the device connects/disconnects.</p>
+ *
+ * <p>WiFi devices are not supported.</p>
+ *
+ * <p>If a Bluetooth LE device wants to use a rotating mac address, it is recommended to use
+ * Resolvable Private Address, and ensure the device is bonded to the phone so that android OS
+ * is able to resolve the address.</p>
+ *
+ * @param request A request for setting the types of device for observing device presence.
+ *
+ * @see ObservingDevicePresenceRequest.Builder
+ * @see CompanionDeviceService#onDevicePresenceEvent(DevicePresenceEvent)
+ */
+ @FlaggedApi(Flags.FLAG_DEVICE_PRESENCE)
+ @RequiresPermission(android.Manifest.permission.REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE)
+ public void startObservingDevicePresence(@NonNull ObservingDevicePresenceRequest request) {
+ Objects.requireNonNull(request, "request cannot be null");
+
+ try {
+ mService.startObservingDevicePresence(
+ request, mContext.getOpPackageName(), mContext.getUserId());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Unregister for receiving callbacks whenever the associated device comes in and out of range.
+ *
+ * Calling app must check for feature presence of
+ * {@link PackageManager#FEATURE_COMPANION_DEVICE_SETUP} before calling this API.
+ *
+ * @param request A request for setting the types of device for observing device presence.
+ */
+ @FlaggedApi(Flags.FLAG_DEVICE_PRESENCE)
+ @RequiresPermission(android.Manifest.permission.REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE)
+ public void stopObservingDevicePresence(@NonNull ObservingDevicePresenceRequest request) {
+ Objects.requireNonNull(request, "request cannot be null");
+
+ try {
+ mService.stopObservingDevicePresence(
+ request, mContext.getOpPackageName(), mContext.getUserId());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
* Dispatch a message to system for processing. It should only be called by
* {@link CompanionDeviceService#dispatchMessageToSystem(int, int, byte[])}
*
diff --git a/core/java/android/companion/CompanionDeviceService.java b/core/java/android/companion/CompanionDeviceService.java
index 4d0267c..5ad2348 100644
--- a/core/java/android/companion/CompanionDeviceService.java
+++ b/core/java/android/companion/CompanionDeviceService.java
@@ -18,7 +18,6 @@
package android.companion;
import android.annotation.FlaggedApi;
-import android.annotation.IntDef;
import android.annotation.MainThread;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -33,8 +32,6 @@
import java.io.InputStream;
import java.io.OutputStream;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
import java.util.Objects;
import java.util.concurrent.Executor;
@@ -123,62 +120,6 @@
*/
public static final String SERVICE_INTERFACE = "android.companion.CompanionDeviceService";
- /** @hide */
- @IntDef(prefix = {"DEVICE_EVENT"}, value = {
- DEVICE_EVENT_BLE_APPEARED,
- DEVICE_EVENT_BLE_DISAPPEARED,
- DEVICE_EVENT_BT_CONNECTED,
- DEVICE_EVENT_BT_DISCONNECTED,
- DEVICE_EVENT_SELF_MANAGED_APPEARED,
- DEVICE_EVENT_SELF_MANAGED_DISAPPEARED
- })
-
- @Retention(RetentionPolicy.SOURCE)
- public @interface DeviceEvent {}
-
- /**
- * Companion app receives {@link #onDeviceEvent(AssociationInfo, int)} callback
- * with this event if the device comes into BLE range.
- */
- @FlaggedApi(Flags.FLAG_DEVICE_PRESENCE)
- public static final int DEVICE_EVENT_BLE_APPEARED = 0;
-
- /**
- * Companion app receives {@link #onDeviceEvent(AssociationInfo, int)} callback
- * with this event if the device is no longer in BLE range.
- */
- @FlaggedApi(Flags.FLAG_DEVICE_PRESENCE)
- public static final int DEVICE_EVENT_BLE_DISAPPEARED = 1;
-
- /**
- * Companion app receives {@link #onDeviceEvent(AssociationInfo, int)} callback
- * with this event when the bluetooth device is connected.
- */
- @FlaggedApi(Flags.FLAG_DEVICE_PRESENCE)
- public static final int DEVICE_EVENT_BT_CONNECTED = 2;
-
- /**
- * Companion app receives {@link #onDeviceEvent(AssociationInfo, int)} callback
- * with this event if the bluetooth device is disconnected.
- */
- @FlaggedApi(Flags.FLAG_DEVICE_PRESENCE)
- public static final int DEVICE_EVENT_BT_DISCONNECTED = 3;
-
- /**
- * A companion app for a self-managed device will receive the callback
- * {@link #onDeviceEvent(AssociationInfo, int)} if it reports that a device has appeared on its
- * own.
- */
- @FlaggedApi(Flags.FLAG_DEVICE_PRESENCE)
- public static final int DEVICE_EVENT_SELF_MANAGED_APPEARED = 4;
-
- /**
- * A companion app for a self-managed device will receive the callback
- * {@link #onDeviceEvent(AssociationInfo, int)} if it reports that a device has disappeared on
- * its own.
- */
- @FlaggedApi(Flags.FLAG_DEVICE_PRESENCE)
- public static final int DEVICE_EVENT_SELF_MANAGED_DISAPPEARED = 5;
private final Stub mRemote = new Stub();
@@ -306,6 +247,7 @@
.detachSystemDataTransport(associationId);
}
+ // TODO(b/315163162) Add @Deprecated keyword after 24Q2 cut.
/**
* Called by system whenever a device associated with this app is connected.
*
@@ -318,6 +260,7 @@
}
}
+ // TODO(b/315163162) Add @Deprecated keyword after 24Q2 cut.
/**
* Called by system whenever a device associated with this app is disconnected.
*
@@ -331,27 +274,13 @@
}
/**
- * Called by the system during device events.
+ * Called by the system during device events.
*
- * <p>E.g. Event {@link #DEVICE_EVENT_BLE_APPEARED} will be called when the associated
- * companion device comes into BLE range.
- * <p>Event {@link #DEVICE_EVENT_BLE_DISAPPEARED} will be called when the associated
- * companion device is no longer in BLE range.
- * <p> Event {@link #DEVICE_EVENT_BT_CONNECTED} will be called when the associated
- * companion device is connected.
- * <p>Event {@link #DEVICE_EVENT_BT_DISCONNECTED} will be called when the associated
- * companion device is disconnected.
- * Note that app must receive {@link #DEVICE_EVENT_BLE_APPEARED} first before
- * {@link #DEVICE_EVENT_BLE_DISAPPEARED} and {@link #DEVICE_EVENT_BT_CONNECTED}
- * before {@link #DEVICE_EVENT_BT_DISCONNECTED}.
- *
- * @param associationInfo A record for the companion device.
- * @param event Associated companion device's event.
+ * @see CompanionDeviceManager#startObservingDevicePresence(ObservingDevicePresenceRequest)
*/
@FlaggedApi(Flags.FLAG_DEVICE_PRESENCE)
@MainThread
- public void onDeviceEvent(@NonNull AssociationInfo associationInfo,
- @DeviceEvent int event) {
+ public void onDevicePresenceEvent(@NonNull DevicePresenceEvent event) {
// Do nothing. Companion apps can override this function.
}
@@ -390,9 +319,10 @@
}
@Override
- public void onDeviceEvent(AssociationInfo associationInfo, int event) {
- mMainHandler.postAtFrontOfQueue(
- () -> mService.onDeviceEvent(associationInfo, event));
+ public void onDevicePresenceEvent(DevicePresenceEvent event) {
+ if (Flags.devicePresence()) {
+ mMainHandler.postAtFrontOfQueue(() -> mService.onDevicePresenceEvent(event));
+ }
}
}
}
diff --git a/core/java/android/companion/DevicePresenceEvent.aidl b/core/java/android/companion/DevicePresenceEvent.aidl
new file mode 100644
index 0000000..1521574
--- /dev/null
+++ b/core/java/android/companion/DevicePresenceEvent.aidl
@@ -0,0 +1,19 @@
+ /*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+ package android.companion;
+
+ parcelable DevicePresenceEvent;
diff --git a/core/java/android/companion/DevicePresenceEvent.java b/core/java/android/companion/DevicePresenceEvent.java
new file mode 100644
index 0000000..30439a5
--- /dev/null
+++ b/core/java/android/companion/DevicePresenceEvent.java
@@ -0,0 +1,218 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.companion;
+
+import android.annotation.FlaggedApi;
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Parcel;
+import android.os.ParcelUuid;
+import android.os.Parcelable;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Objects;
+
+/**
+ * Event for observing device presence.
+ *
+ * @see CompanionDeviceManager#startObservingDevicePresence(ObservingDevicePresenceRequest)
+ * @see ObservingDevicePresenceRequest.Builder#setUuid(ParcelUuid)
+ * @see ObservingDevicePresenceRequest.Builder#setAssociationId(int)
+ */
+@FlaggedApi(Flags.FLAG_DEVICE_PRESENCE)
+public final class DevicePresenceEvent implements Parcelable {
+
+ /** @hide */
+ @IntDef(prefix = {"EVENT"}, value = {
+ EVENT_BLE_APPEARED,
+ EVENT_BLE_DISAPPEARED,
+ EVENT_BT_CONNECTED,
+ EVENT_BT_DISCONNECTED,
+ EVENT_SELF_MANAGED_APPEARED,
+ EVENT_SELF_MANAGED_DISAPPEARED
+ })
+
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface Event {}
+
+ /**
+ * Indicate observing device presence base on the ParcelUuid but not association id.
+ */
+ public static final int NO_ASSOCIATION = -1;
+
+ /**
+ * Companion app receives
+ * {@link CompanionDeviceService#onDevicePresenceEvent(DevicePresenceEvent)} callback
+ * with this event if the device comes into BLE range.
+ */
+ public static final int EVENT_BLE_APPEARED = 0;
+
+ /**
+ * Companion app receives
+ * {@link CompanionDeviceService#onDevicePresenceEvent(DevicePresenceEvent)} callback
+ * with this event if the device is no longer in BLE range.
+ */
+ public static final int EVENT_BLE_DISAPPEARED = 1;
+
+ /**
+ * Companion app receives
+ * {@link CompanionDeviceService#onDevicePresenceEvent(DevicePresenceEvent)} callback
+ * with this event when the bluetooth device is connected.
+ */
+ public static final int EVENT_BT_CONNECTED = 2;
+
+ /**
+ * Companion app receives
+ * {@link CompanionDeviceService#onDevicePresenceEvent(DevicePresenceEvent)} callback
+ * with this event if the bluetooth device is disconnected.
+ */
+ public static final int EVENT_BT_DISCONNECTED = 3;
+
+ /**
+ * A companion app for a self-managed device will receive the callback
+ * {@link CompanionDeviceService#onDevicePresenceEvent(DevicePresenceEvent)}
+ * if it reports that a device has appeared on its
+ * own.
+ */
+ public static final int EVENT_SELF_MANAGED_APPEARED = 4;
+
+ /**
+ * A companion app for a self-managed device will receive the callback
+ * {@link CompanionDeviceService#onDevicePresenceEvent(DevicePresenceEvent)} if it reports
+ * that a device has disappeared on its own.
+ */
+ public static final int EVENT_SELF_MANAGED_DISAPPEARED = 5;
+ private final int mAssociationId;
+ private final int mEvent;
+ @Nullable
+ private final ParcelUuid mUuid;
+
+ private static final int PARCEL_UUID_NULL = 0;
+
+ private static final int PARCEL_UUID_NOT_NULL = 1;
+
+ /**
+ * Create a new DevicePresenceEvent.
+ */
+ public DevicePresenceEvent(
+ int associationId, @Event int event, @Nullable ParcelUuid uuid) {
+ mAssociationId = associationId;
+ mEvent = event;
+ mUuid = uuid;
+ }
+
+ /**
+ * @return The association id has been used to observe device presence.
+ *
+ * Caller will receive the valid association id if only if using
+ * {@link ObservingDevicePresenceRequest.Builder#setAssociationId(int)}, otherwise
+ * return {@link #NO_ASSOCIATION}.
+ *
+ * @see ObservingDevicePresenceRequest.Builder#setAssociationId(int)
+ */
+ public int getAssociationId() {
+ return mAssociationId;
+ }
+
+ /**
+ * @return Associated companion device's event.
+ */
+ public int getEvent() {
+ return mEvent;
+ }
+
+ /**
+ * @return The ParcelUuid has been used to observe device presence.
+ *
+ * Caller will receive the ParcelUuid if only if using
+ * {@link ObservingDevicePresenceRequest.Builder#setUuid(ParcelUuid)}, otherwise return null.
+ *
+ * @see ObservingDevicePresenceRequest.Builder#setUuid(ParcelUuid)
+ */
+
+ @Nullable
+ public ParcelUuid getUuid() {
+ return mUuid;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ dest.writeInt(mAssociationId);
+ dest.writeInt(mEvent);
+ if (mUuid == null) {
+ // Write 0 to the parcel to indicate the ParcelUuid is null.
+ dest.writeInt(PARCEL_UUID_NULL);
+ } else {
+ dest.writeInt(PARCEL_UUID_NOT_NULL);
+ mUuid.writeToParcel(dest, flags);
+ }
+ }
+
+ @Override
+ public boolean equals(@Nullable Object o) {
+ if (this == o) return true;
+ if (!(o instanceof DevicePresenceEvent that)) return false;
+
+ return Objects.equals(mUuid, that.mUuid)
+ && mAssociationId == that.mAssociationId
+ && mEvent == that.mEvent;
+ }
+
+ @Override
+ public String toString() {
+ return "ObservingDevicePresenceResult { "
+ + "Association Id= " + mAssociationId + ","
+ + "ParcelUuid= " + mUuid + ","
+ + "Event= " + mEvent + "}";
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mAssociationId, mEvent, mUuid);
+ }
+
+ @NonNull
+ public static final Parcelable.Creator<DevicePresenceEvent> CREATOR =
+ new Parcelable.Creator<DevicePresenceEvent>() {
+ @Override
+ public DevicePresenceEvent[] newArray(int size) {
+ return new DevicePresenceEvent[size];
+ }
+
+ @Override
+ public DevicePresenceEvent createFromParcel(@NonNull Parcel in) {
+ return new DevicePresenceEvent(in);
+ }
+ };
+
+ private DevicePresenceEvent(@NonNull Parcel in) {
+ mAssociationId = in.readInt();
+ mEvent = in.readInt();
+ if (in.readInt() == PARCEL_UUID_NULL) {
+ mUuid = null;
+ } else {
+ mUuid = ParcelUuid.CREATOR.createFromParcel(in);
+ }
+ }
+}
diff --git a/core/java/android/companion/ICompanionDeviceManager.aidl b/core/java/android/companion/ICompanionDeviceManager.aidl
index 22689f3..57d59e5 100644
--- a/core/java/android/companion/ICompanionDeviceManager.aidl
+++ b/core/java/android/companion/ICompanionDeviceManager.aidl
@@ -24,8 +24,11 @@
import android.companion.ISystemDataTransferCallback;
import android.companion.AssociationInfo;
import android.companion.AssociationRequest;
+import android.companion.ObservingDevicePresenceRequest;
import android.companion.datatransfer.PermissionSyncRequest;
import android.content.ComponentName;
+import android.os.ParcelUuid;
+
/**
* Interface for communication with the core companion device manager service.
@@ -132,4 +135,10 @@
byte[] getBackupPayload(int userId);
void applyRestoredPayload(in byte[] payload, int userId);
+
+ @EnforcePermission("REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE")
+ void startObservingDevicePresence(in ObservingDevicePresenceRequest request, in String packageName, int userId);
+
+ @EnforcePermission("REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE")
+ void stopObservingDevicePresence(in ObservingDevicePresenceRequest request, in String packageName, int userId);
}
diff --git a/core/java/android/companion/ICompanionDeviceService.aidl b/core/java/android/companion/ICompanionDeviceService.aidl
index 2a311bf..f5401d2 100644
--- a/core/java/android/companion/ICompanionDeviceService.aidl
+++ b/core/java/android/companion/ICompanionDeviceService.aidl
@@ -17,10 +17,12 @@
package android.companion;
import android.companion.AssociationInfo;
+import android.companion.DevicePresenceEvent;
+import android.os.ParcelUuid;
/** @hide */
oneway interface ICompanionDeviceService {
void onDeviceAppeared(in AssociationInfo associationInfo);
void onDeviceDisappeared(in AssociationInfo associationInfo);
- void onDeviceEvent(in AssociationInfo associationInfo, int state);
+ void onDevicePresenceEvent(in DevicePresenceEvent event);
}
diff --git a/core/java/android/companion/ObservingDevicePresenceRequest.aidl b/core/java/android/companion/ObservingDevicePresenceRequest.aidl
new file mode 100644
index 0000000..fed0607
--- /dev/null
+++ b/core/java/android/companion/ObservingDevicePresenceRequest.aidl
@@ -0,0 +1,19 @@
+ /*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+ package android.companion;
+
+ parcelable ObservingDevicePresenceRequest;
\ No newline at end of file
diff --git a/core/java/android/companion/ObservingDevicePresenceRequest.java b/core/java/android/companion/ObservingDevicePresenceRequest.java
new file mode 100644
index 0000000..f1d594e
--- /dev/null
+++ b/core/java/android/companion/ObservingDevicePresenceRequest.java
@@ -0,0 +1,208 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.companion;
+
+import android.annotation.FlaggedApi;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
+import android.os.Parcel;
+import android.os.ParcelUuid;
+import android.os.Parcelable;
+import android.provider.OneTimeUseBuilder;
+
+import java.util.Objects;
+
+/**
+ * A request for setting the types of device for observing device presence.
+ *
+ * <p>Only supports association id or ParcelUuid and calling app must declare uses-permission
+ * {@link android.Manifest.permission#REQUEST_OBSERVE_DEVICE_UUID_PRESENCE} if using
+ * {@link Builder#setUuid(ParcelUuid)}.</p>
+ *
+ * Calling apps must use either ObservingDevicePresenceRequest.Builder#setUuid(ParcelUuid) or
+ * ObservingDevicePresenceRequest.Builder#setAssociationId(int), but not both.
+ *
+ * @see Builder#setUuid(ParcelUuid)
+ * @see Builder#setAssociationId(int)
+ * @see CompanionDeviceManager#startObservingDevicePresence(ObservingDevicePresenceRequest)
+ */
+@FlaggedApi(Flags.FLAG_DEVICE_PRESENCE)
+public final class ObservingDevicePresenceRequest implements Parcelable {
+ private final int mAssociationId;
+ @Nullable private final ParcelUuid mUuid;
+
+ private static final int PARCEL_UUID_NULL = 0;
+
+ private static final int PARCEL_UUID_NOT_NULL = 1;
+
+ private ObservingDevicePresenceRequest(int associationId, ParcelUuid uuid) {
+ mAssociationId = associationId;
+ mUuid = uuid;
+ }
+
+ private ObservingDevicePresenceRequest(@NonNull Parcel in) {
+ mAssociationId = in.readInt();
+ if (in.readInt() == PARCEL_UUID_NULL) {
+ mUuid = null;
+ } else {
+ mUuid = ParcelUuid.CREATOR.createFromParcel(in);
+ }
+ }
+
+ /**
+ * @return the association id for observing device presence. It will return
+ * {@link DevicePresenceEvent#NO_ASSOCIATION} if using
+ * {@link Builder#setUuid(ParcelUuid)}.
+ */
+ public int getAssociationId() {
+ return mAssociationId;
+ }
+
+ /**
+ * @return the ParcelUuid for observing device presence.
+ */
+ @Nullable
+ public ParcelUuid getUuid() {
+ return mUuid;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ dest.writeInt(mAssociationId);
+ if (mUuid == null) {
+ // Write 0 to the parcel to indicate the ParcelUuid is null.
+ dest.writeInt(PARCEL_UUID_NULL);
+ } else {
+ dest.writeInt(PARCEL_UUID_NOT_NULL);
+ mUuid.writeToParcel(dest, flags);
+ }
+
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @NonNull
+ public static final Parcelable.Creator<ObservingDevicePresenceRequest> CREATOR =
+ new Parcelable.Creator<ObservingDevicePresenceRequest>() {
+ @Override
+ public ObservingDevicePresenceRequest[] newArray(int size) {
+ return new ObservingDevicePresenceRequest[size];
+ }
+
+ @Override
+ public ObservingDevicePresenceRequest createFromParcel(@NonNull Parcel in) {
+ return new ObservingDevicePresenceRequest(in);
+ }
+ };
+
+ @Override
+ public String toString() {
+ return "ObservingDevicePresenceRequest { "
+ + "Association Id= " + mAssociationId + ","
+ + "ParcelUuid= " + mUuid + "}";
+ }
+
+ @Override
+ public boolean equals(@Nullable Object o) {
+ if (this == o) return true;
+ if (!(o instanceof ObservingDevicePresenceRequest that)) return false;
+
+ return Objects.equals(mUuid, that.mUuid) && mAssociationId == that.mAssociationId;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mAssociationId, mUuid);
+ }
+
+ /**
+ * A builder for {@link ObservingDevicePresenceRequest}
+ */
+ public static final class Builder extends OneTimeUseBuilder<ObservingDevicePresenceRequest> {
+ // Initial the association id to {@link DevicePresenceEvent.NO_ASSOCIATION}
+ // to indicate the value is not set yet.
+ private int mAssociationId = DevicePresenceEvent.NO_ASSOCIATION;
+ private ParcelUuid mUuid;
+
+ public Builder() {}
+
+ /**
+ * Set the association id to be observed for device presence.
+ *
+ * <p>The provided device must be {@link CompanionDeviceManager#associate associated}
+ * with the calling app before calling this method if using this API.
+ *
+ * Caller must implement a single {@link CompanionDeviceService} which will be bound to and
+ * receive callbacks to
+ * {@link CompanionDeviceService#onDevicePresenceEvent(DevicePresenceEvent)}.</p>
+ *
+ * <p>Calling apps must use either {@link #setUuid(ParcelUuid)}
+ * or this API, but not both.</p>
+ *
+ * @param associationId The association id for observing device presence.
+ */
+ @NonNull
+ public Builder setAssociationId(int associationId) {
+ checkNotUsed();
+ this.mAssociationId = associationId;
+ return this;
+ }
+
+ /**
+ * Set the ParcelUuid to be observed for device presence.
+ *
+ * <p>It does not require to create the association before calling this API.
+ * This only supports classic Bluetooth scan and caller must implement
+ * a single {@link CompanionDeviceService} which will be bound to and receive callbacks to
+ * {@link CompanionDeviceService#onDevicePresenceEvent(DevicePresenceEvent)}.</p>
+ *
+ * <p>The Uuid should be matching one of the ParcelUuid form
+ * {@link android.bluetooth.BluetoothDevice#getUuids()}</p>
+ *
+ * <p>Calling apps must use either this API or {@link #setAssociationId(int)},
+ * but not both.</p>
+ *
+ * @param uuid The ParcelUuid for observing device presence.
+ */
+ @NonNull
+ @RequiresPermission(android.Manifest.permission.REQUEST_OBSERVE_DEVICE_UUID_PRESENCE)
+ public Builder setUuid(@NonNull ParcelUuid uuid) {
+ checkNotUsed();
+ this.mUuid = uuid;
+ return this;
+ }
+
+ @NonNull
+ @Override
+ public ObservingDevicePresenceRequest build() {
+ markUsed();
+ if (mUuid != null && mAssociationId != DevicePresenceEvent.NO_ASSOCIATION) {
+ throw new IllegalStateException("Cannot observe device presence based on "
+ + "both ParcelUuid and association ID. Choose one or the other.");
+ } else if (mUuid == null && mAssociationId <= 0) {
+ throw new IllegalStateException("Must provide either a ParcelUuid or "
+ + "a valid association ID to observe device presence.");
+ }
+
+ return new ObservingDevicePresenceRequest(mAssociationId, mUuid);
+ }
+ }
+}
diff --git a/core/java/android/companion/flags.aconfig b/core/java/android/companion/flags.aconfig
index 9e410b8..d634b64 100644
--- a/core/java/android/companion/flags.aconfig
+++ b/core/java/android/companion/flags.aconfig
@@ -33,4 +33,4 @@
namespace: "companion"
description: "Expose perm sync user consent API"
bug: "309528663"
-}
\ No newline at end of file
+}
diff --git a/core/java/android/companion/virtual/IVirtualDeviceManager.aidl b/core/java/android/companion/virtual/IVirtualDeviceManager.aidl
index 325aa28f..83e18ec 100644
--- a/core/java/android/companion/virtual/IVirtualDeviceManager.aidl
+++ b/core/java/android/companion/virtual/IVirtualDeviceManager.aidl
@@ -133,4 +133,10 @@
* device.
*/
boolean isVirtualDeviceOwnedMirrorDisplay(int displayId);
+
+ /**
+ * Returns all current persistent device IDs, including the ones for which no virtual device
+ * exists, as long as one may have existed or can be created.
+ */
+ List<String> getAllPersistentDeviceIds();
}
diff --git a/core/java/android/content/ContentProvider.java b/core/java/android/content/ContentProvider.java
index c7a75ed..e9b94c9 100644
--- a/core/java/android/content/ContentProvider.java
+++ b/core/java/android/content/ContentProvider.java
@@ -41,6 +41,7 @@
import android.database.Cursor;
import android.database.MatrixCursor;
import android.database.SQLException;
+import android.multiuser.Flags;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Binder;
@@ -146,6 +147,7 @@
private boolean mExported;
private boolean mNoPerms;
private boolean mSingleUser;
+ private boolean mSystemUserOnly;
private SparseBooleanArray mUsersRedirectedToOwnerForMedia = new SparseBooleanArray();
private ThreadLocal<AttributionSource> mCallingAttributionSource;
@@ -377,7 +379,9 @@
!= PermissionChecker.PERMISSION_GRANTED
&& getContext().checkUriPermission(userUri, Binder.getCallingPid(),
callingUid, Intent.FLAG_GRANT_READ_URI_PERMISSION)
- != PackageManager.PERMISSION_GRANTED) {
+ != PackageManager.PERMISSION_GRANTED
+ && !deniedAccessSystemUserOnlyProvider(callingUserId,
+ mSystemUserOnly)) {
FrameworkStatsLog.write(GET_TYPE_ACCESSED_WITHOUT_PERMISSION,
enumCheckUriPermission,
callingUid, uri.getAuthority(), type);
@@ -865,6 +869,10 @@
boolean checkUser(int pid, int uid, Context context) {
final int callingUserId = UserHandle.getUserId(uid);
+ if (deniedAccessSystemUserOnlyProvider(callingUserId, mSystemUserOnly)) {
+ return false;
+ }
+
if (callingUserId == context.getUserId() || mSingleUser) {
return true;
}
@@ -987,6 +995,9 @@
// last chance, check against any uri grants
final int callingUserId = UserHandle.getUserId(uid);
+ if (deniedAccessSystemUserOnlyProvider(callingUserId, mSystemUserOnly)) {
+ return PermissionChecker.PERMISSION_HARD_DENIED;
+ }
final Uri userUri = (mSingleUser && !UserHandle.isSameUser(mMyUid, uid))
? maybeAddUserId(uri, callingUserId) : uri;
if (context.checkUriPermission(userUri, pid, uid, Intent.FLAG_GRANT_READ_URI_PERMISSION)
@@ -2623,6 +2634,7 @@
setPathPermissions(info.pathPermissions);
mExported = info.exported;
mSingleUser = (info.flags & ProviderInfo.FLAG_SINGLE_USER) != 0;
+ mSystemUserOnly = (info.flags & ProviderInfo.FLAG_SYSTEM_USER_ONLY) != 0;
setAuthorities(info.authority);
}
if (Build.IS_DEBUGGABLE) {
@@ -2756,6 +2768,11 @@
String auth = uri.getAuthority();
if (!mSingleUser) {
int userId = getUserIdFromAuthority(auth, UserHandle.USER_CURRENT);
+ if (deniedAccessSystemUserOnlyProvider(mContext.getUserId(),
+ mSystemUserOnly)) {
+ throw new SecurityException("Trying to query a SYSTEM user only content"
+ + " provider from user:" + mContext.getUserId());
+ }
if (userId != UserHandle.USER_CURRENT
&& userId != mContext.getUserId()
// Since userId specified in content uri, the provider userId would be
@@ -2929,4 +2946,16 @@
Trace.traceBegin(traceTag, methodName + subInfo);
}
}
+ /**
+ * Return true if access to content provider is denied because it's a SYSTEM user only
+ * provider and the calling user is not the SYSTEM user.
+ *
+ * @param callingUserId UserId of the caller accessing the content provider.
+ * @param systemUserOnly true when the content provider is only available for the SYSTEM user.
+ */
+ private static boolean deniedAccessSystemUserOnlyProvider(int callingUserId,
+ boolean systemUserOnly) {
+ return Flags.enableSystemUserOnlyForServicesAndProviders()
+ && (callingUserId != UserHandle.USER_SYSTEM && systemUserOnly);
+ }
}
diff --git a/core/java/android/content/pm/ILauncherApps.aidl b/core/java/android/content/pm/ILauncherApps.aidl
index 62db65f..a97de63 100644
--- a/core/java/android/content/pm/ILauncherApps.aidl
+++ b/core/java/android/content/pm/ILauncherApps.aidl
@@ -128,6 +128,4 @@
/** Unregister a callback, so that it won't be called when LauncherApps dumps. */
void unRegisterDumpCallback(IDumpCallback cb);
-
- void setArchiveCompatibilityOptions(boolean enableIconOverlay, boolean enableUnarchivalConfirmation);
}
diff --git a/core/java/android/content/pm/IPackageManager.aidl b/core/java/android/content/pm/IPackageManager.aidl
index 380de96..6dc8d47 100644
--- a/core/java/android/content/pm/IPackageManager.aidl
+++ b/core/java/android/content/pm/IPackageManager.aidl
@@ -840,7 +840,7 @@
ArchivedPackageParcel getArchivedPackage(in String packageName, int userId);
- Bitmap getArchivedAppIcon(String packageName, in UserHandle user, String callingPackageName);
+ Bitmap getArchivedAppIcon(String packageName, in UserHandle user);
boolean isAppArchivable(String packageName, in UserHandle user);
}
diff --git a/core/java/android/content/pm/LauncherApps.java b/core/java/android/content/pm/LauncherApps.java
index 50be983..1d2b1af 100644
--- a/core/java/android/content/pm/LauncherApps.java
+++ b/core/java/android/content/pm/LauncherApps.java
@@ -1801,31 +1801,6 @@
}
}
- /**
- * Enable or disable different archive compatibility options of the launcher.
- *
- * @param enableIconOverlay Provides a cloud overlay for archived apps to ensure users are aware
- * that a certain app is archived. True by default.
- * Launchers might want to disable this operation if they want to provide custom user experience
- * to differentiate archived apps.
- * @param enableUnarchivalConfirmation If true, the user is shown a confirmation dialog when
- * they click an archived app, which explains that the app will be downloaded and restored in
- * the background. True by default.
- * Launchers might want to disable this operation if they provide sufficient, alternative user
- * guidance to highlight that an unarchival is starting and ongoing once an archived app is
- * tapped. E.g., this could be achieved by showing the unarchival progress around the icon.
- */
- @FlaggedApi(android.content.pm.Flags.FLAG_ARCHIVING)
- public void setArchiveCompatibilityOptions(boolean enableIconOverlay,
- boolean enableUnarchivalConfirmation) {
- try {
- mService.setArchiveCompatibilityOptions(enableIconOverlay,
- enableUnarchivalConfirmation);
- } catch (RemoteException re) {
- throw re.rethrowFromSystemServer();
- }
- }
-
/** @return position in mCallbacks for callback or -1 if not present. */
private int findCallbackLocked(Callback callback) {
if (callback == null) {
diff --git a/core/java/android/content/pm/PackageInstaller.java b/core/java/android/content/pm/PackageInstaller.java
index f0efed9..c4bf18d 100644
--- a/core/java/android/content/pm/PackageInstaller.java
+++ b/core/java/android/content/pm/PackageInstaller.java
@@ -738,7 +738,7 @@
/**
* The set of error types that can be set for
- * {@link #reportUnarchivalStatus(int, int, PendingIntent)}.
+ * {@link #reportUnarchivalState}.
*
* @hide
*/
@@ -2421,6 +2421,7 @@
* facilitate the unarchival flow (e.g. user needs to log in).
* @throws PackageManager.NameNotFoundException if no unarchival with {@code unarchiveId} exists
*/
+ // TODO(b/314960798) Remove old API once it's unused
@RequiresPermission(anyOf = {
Manifest.permission.INSTALL_PACKAGES,
Manifest.permission.REQUEST_INSTALL_PACKAGES})
@@ -2438,6 +2439,30 @@
}
}
+ /**
+ * Reports the state of an unarchival to the system.
+ *
+ * @see UnarchivalState for the different state options.
+ * @throws PackageManager.NameNotFoundException if no unarchival with {@code unarchiveId} exists
+ */
+ @RequiresPermission(anyOf = {
+ Manifest.permission.INSTALL_PACKAGES,
+ Manifest.permission.REQUEST_INSTALL_PACKAGES})
+ @FlaggedApi(Flags.FLAG_ARCHIVING)
+ public void reportUnarchivalState(@NonNull UnarchivalState unarchivalState)
+ throws PackageManager.NameNotFoundException {
+ Objects.requireNonNull(unarchivalState);
+ try {
+ mInstaller.reportUnarchivalStatus(unarchivalState.getUnarchiveId(),
+ unarchivalState.getStatus(), unarchivalState.getRequiredStorageBytes(),
+ unarchivalState.getUserActionIntent(), new UserHandle(mUserId));
+ } catch (ParcelableException e) {
+ e.maybeRethrow(PackageManager.NameNotFoundException.class);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
// (b/239722738) This class serves as a bridge between the PackageLite class, which
// is a hidden class, and the consumers of this class. (e.g. InstallInstalling.java)
// This is a part of an effort to remove dependency on hidden APIs and use SystemAPIs or
@@ -2693,6 +2718,8 @@
/** @hide */
public long rollbackLifetimeMillis = 0;
/** {@hide} */
+ public int rollbackImpactLevel = PackageManager.ROLLBACK_USER_IMPACT_LOW;
+ /** {@hide} */
public boolean forceQueryableOverride;
/** {@hide} */
public int requireUserAction = USER_ACTION_UNSPECIFIED;
@@ -2749,6 +2776,7 @@
}
rollbackDataPolicy = source.readInt();
rollbackLifetimeMillis = source.readLong();
+ rollbackImpactLevel = source.readInt();
requireUserAction = source.readInt();
packageSource = source.readInt();
applicationEnabledSettingPersistent = source.readBoolean();
@@ -2783,6 +2811,7 @@
ret.dataLoaderParams = dataLoaderParams;
ret.rollbackDataPolicy = rollbackDataPolicy;
ret.rollbackLifetimeMillis = rollbackLifetimeMillis;
+ ret.rollbackImpactLevel = rollbackImpactLevel;
ret.requireUserAction = requireUserAction;
ret.packageSource = packageSource;
ret.applicationEnabledSettingPersistent = applicationEnabledSettingPersistent;
@@ -3121,6 +3150,28 @@
}
/**
+ * rollbackImpactLevel is a measure of impact a rollback has on the user. This can take one
+ * of 3 values:
+ * <ul>
+ * <li>{@link PackageManager#ROLLBACK_USER_IMPACT_LOW} (default)</li>
+ * <li>{@link PackageManager#ROLLBACK_USER_IMPACT_HIGH} (1)</li>
+ * <li>{@link PackageManager#ROLLBACK_USER_IMPACT_ONLY_MANUAL} (2)</li>
+ * </ul>
+ *
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(android.Manifest.permission.MANAGE_ROLLBACKS)
+ @FlaggedApi(Flags.FLAG_RECOVERABILITY_DETECTION)
+ public void setRollbackImpactLevel(@PackageManager.RollbackImpactLevel int impactLevel) {
+ if ((installFlags & PackageManager.INSTALL_ENABLE_ROLLBACK) == 0) {
+ throw new IllegalArgumentException(
+ "Can't set rollbackImpactLevel when rollback is not enabled");
+ }
+ rollbackImpactLevel = impactLevel;
+ }
+
+ /**
* @deprecated use {@link #setRequestDowngrade(boolean)}.
* {@hide}
*/
@@ -3493,6 +3544,7 @@
pw.printPair("dataLoaderParams", dataLoaderParams);
pw.printPair("rollbackDataPolicy", rollbackDataPolicy);
pw.printPair("rollbackLifetimeMillis", rollbackLifetimeMillis);
+ pw.printPair("rollbackImpactLevel", rollbackImpactLevel);
pw.printPair("applicationEnabledSettingPersistent",
applicationEnabledSettingPersistent);
pw.printHexPair("developmentInstallFlags", developmentInstallFlags);
@@ -3536,6 +3588,7 @@
}
dest.writeInt(rollbackDataPolicy);
dest.writeLong(rollbackLifetimeMillis);
+ dest.writeInt(rollbackImpactLevel);
dest.writeInt(requireUserAction);
dest.writeInt(packageSource);
dest.writeBoolean(applicationEnabledSettingPersistent);
@@ -3734,6 +3787,9 @@
public long rollbackLifetimeMillis;
/** {@hide} */
+ public int rollbackImpactLevel;
+
+ /** {@hide} */
public int requireUserAction;
/** {@hide} */
@@ -3801,6 +3857,7 @@
isPreapprovalRequested = source.readBoolean();
rollbackDataPolicy = source.readInt();
rollbackLifetimeMillis = source.readLong();
+ rollbackImpactLevel = source.readInt();
createdMillis = source.readLong();
requireUserAction = source.readInt();
installerUid = source.readInt();
@@ -4438,6 +4495,7 @@
dest.writeBoolean(isPreapprovalRequested);
dest.writeInt(rollbackDataPolicy);
dest.writeLong(rollbackLifetimeMillis);
+ dest.writeInt(rollbackImpactLevel);
dest.writeLong(createdMillis);
dest.writeInt(requireUserAction);
dest.writeInt(installerUid);
@@ -4708,10 +4766,10 @@
codegenVersion = "1.0.23",
sourceFile = "frameworks/base/core/java/android/content/pm/PackageInstaller.java",
inputSignatures = "private final @android.annotation.Nullable android.graphics.Bitmap mIcon\nprivate final @android.annotation.NonNull java.lang.CharSequence mLabel\nprivate final @android.annotation.NonNull android.icu.util.ULocale mLocale\nprivate final @android.annotation.NonNull java.lang.String mPackageName\npublic static final @android.annotation.NonNull android.os.Parcelable.Creator<android.content.pm.PackageInstaller.PreapprovalDetails> CREATOR\npublic @java.lang.Override void writeToParcel(android.os.Parcel,int)\npublic @java.lang.Override int describeContents()\nclass PreapprovalDetails extends java.lang.Object implements [android.os.Parcelable]\nprivate @android.annotation.Nullable android.graphics.Bitmap mIcon\nprivate @android.annotation.NonNull java.lang.CharSequence mLabel\nprivate @android.annotation.NonNull android.icu.util.ULocale mLocale\nprivate @android.annotation.NonNull java.lang.String mPackageName\nprivate long mBuilderFieldsSet\npublic @android.annotation.NonNull android.content.pm.PackageInstaller.PreapprovalDetails.Builder setIcon(android.graphics.Bitmap)\npublic @android.annotation.NonNull android.content.pm.PackageInstaller.PreapprovalDetails.Builder setLabel(java.lang.CharSequence)\npublic @android.annotation.NonNull android.content.pm.PackageInstaller.PreapprovalDetails.Builder setLocale(android.icu.util.ULocale)\npublic @android.annotation.NonNull android.content.pm.PackageInstaller.PreapprovalDetails.Builder setPackageName(java.lang.String)\npublic @android.annotation.NonNull android.content.pm.PackageInstaller.PreapprovalDetails build()\nprivate void checkNotUsed()\nclass Builder extends java.lang.Object implements []\n@com.android.internal.util.DataClass(genConstructor=false, genToString=true)")
+
@Deprecated
private void __metadata() {}
-
//@formatter:on
// End of generated code
@@ -5102,13 +5160,188 @@
codegenVersion = "1.0.23",
sourceFile = "frameworks/base/core/java/android/content/pm/PackageInstaller.java",
inputSignatures = "public static final @android.annotation.NonNull android.content.pm.PackageInstaller.InstallConstraints GENTLE_UPDATE\nprivate final boolean mDeviceIdleRequired\nprivate final boolean mAppNotForegroundRequired\nprivate final boolean mAppNotInteractingRequired\nprivate final boolean mAppNotTopVisibleRequired\nprivate final boolean mNotInCallRequired\nclass InstallConstraints extends java.lang.Object implements [android.os.Parcelable]\nprivate boolean mDeviceIdleRequired\nprivate boolean mAppNotForegroundRequired\nprivate boolean mAppNotInteractingRequired\nprivate boolean mAppNotTopVisibleRequired\nprivate boolean mNotInCallRequired\npublic @android.annotation.SuppressLint @android.annotation.NonNull android.content.pm.PackageInstaller.InstallConstraints.Builder setDeviceIdleRequired()\npublic @android.annotation.SuppressLint @android.annotation.NonNull android.content.pm.PackageInstaller.InstallConstraints.Builder setAppNotForegroundRequired()\npublic @android.annotation.SuppressLint @android.annotation.NonNull android.content.pm.PackageInstaller.InstallConstraints.Builder setAppNotInteractingRequired()\npublic @android.annotation.SuppressLint @android.annotation.NonNull android.content.pm.PackageInstaller.InstallConstraints.Builder setAppNotTopVisibleRequired()\npublic @android.annotation.SuppressLint @android.annotation.NonNull android.content.pm.PackageInstaller.InstallConstraints.Builder setNotInCallRequired()\npublic @android.annotation.NonNull android.content.pm.PackageInstaller.InstallConstraints build()\nclass Builder extends java.lang.Object implements []\n@com.android.internal.util.DataClass(genParcelable=true, genHiddenConstructor=true, genEqualsHashCode=true)")
+
@Deprecated
private void __metadata() {}
-
//@formatter:on
// End of generated code
}
+ /**
+ * Used to communicate the unarchival state in {@link #reportUnarchivalState}.
+ */
+ @FlaggedApi(Flags.FLAG_ARCHIVING)
+ public static final class UnarchivalState {
+
+ /**
+ * The caller is able to facilitate the unarchival for the given {@code unarchiveId}.
+ *
+ * @param unarchiveId the ID provided by the system as part of the intent.action.UNARCHIVE
+ * broadcast with EXTRA_UNARCHIVE_ID.
+ */
+ @NonNull
+ public static UnarchivalState createOkState(int unarchiveId) {
+ return new UnarchivalState(unarchiveId, UNARCHIVAL_OK, /* requiredStorageBytes= */ -1,
+ /* userActionIntent= */ null);
+ }
+
+ /**
+ * User action is required before commencing with the unarchival for the given
+ * {@code unarchiveId}. E.g., this could be used if it's necessary for the user to sign-in
+ * first.
+ *
+ * @param unarchiveId the ID provided by the system as part of the
+ * intent.action.UNARCHIVE
+ * broadcast with EXTRA_UNARCHIVE_ID.
+ * @param userActionIntent optional intent to start a follow up action required to
+ * facilitate the unarchival flow (e.g. user needs to log in).
+ */
+ @NonNull
+ public static UnarchivalState createUserActionRequiredState(int unarchiveId,
+ @NonNull PendingIntent userActionIntent) {
+ Objects.requireNonNull(userActionIntent);
+ return new UnarchivalState(unarchiveId, UNARCHIVAL_ERROR_USER_ACTION_NEEDED,
+ /* requiredStorageBytes= */ -1, userActionIntent);
+ }
+
+ /**
+ * There is not enough storage to start the unarchival for the given {@code unarchiveId}.
+ *
+ * @param unarchiveId the ID provided by the system as part of the
+ * intent.action.UNARCHIVE
+ * broadcast with EXTRA_UNARCHIVE_ID.
+ * @param requiredStorageBytes ff the error is UNARCHIVAL_ERROR_INSUFFICIENT_STORAGE this
+ * field should be set to specify how many additional bytes of
+ * storage are required to unarchive the app.
+ * @param userActionIntent can optionally be set to provide a custom storage-clearing
+ * action.
+ */
+ @NonNull
+ public static UnarchivalState createInsufficientStorageState(int unarchiveId,
+ long requiredStorageBytes, @Nullable PendingIntent userActionIntent) {
+ return new UnarchivalState(unarchiveId, UNARCHIVAL_ERROR_INSUFFICIENT_STORAGE,
+ requiredStorageBytes, userActionIntent);
+ }
+
+ /**
+ * The device has no data connectivity and unarchival cannot be started for the given
+ * {@code unarchiveId}.
+ *
+ * @param unarchiveId the ID provided by the system as part of the intent.action.UNARCHIVE
+ * broadcast with EXTRA_UNARCHIVE_ID.
+ */
+ @NonNull
+ public static UnarchivalState createNoConnectivityState(int unarchiveId) {
+ return new UnarchivalState(unarchiveId, UNARCHIVAL_ERROR_NO_CONNECTIVITY,
+ /* requiredStorageBytes= */ -1,/* userActionIntent= */ null);
+ }
+
+ /**
+ * Generic error state for all cases that are not covered by other methods in this class.
+ *
+ * @param unarchiveId the ID provided by the system as part of the intent.action.UNARCHIVE
+ * broadcast with EXTRA_UNARCHIVE_ID.
+ */
+ @NonNull
+ public static UnarchivalState createGenericErrorState(int unarchiveId) {
+ return new UnarchivalState(unarchiveId, UNARCHIVAL_GENERIC_ERROR,
+ /* requiredStorageBytes= */ -1,/* userActionIntent= */ null);
+ }
+
+
+ /**
+ * The ID provided by the system as part of the intent.action.UNARCHIVE broadcast with
+ * EXTRA_UNARCHIVE_ID.
+ */
+ private final int mUnarchiveId;
+
+ /** Used for the system to provide the user with necessary follow-up steps or errors. */
+ @UnarchivalStatus
+ private final int mStatus;
+
+ /**
+ * If the error is UNARCHIVAL_ERROR_INSUFFICIENT_STORAGE this field should be set to specify
+ * how many additional bytes of storage are required to unarchive the app.
+ */
+ private final long mRequiredStorageBytes;
+
+ /**
+ * Optional intent to start a follow up action required to facilitate the unarchival flow
+ * (e.g., user needs to log in).
+ */
+ @Nullable
+ private final PendingIntent mUserActionIntent;
+
+ /**
+ * Creates a new UnarchivalState.
+ *
+ * @param unarchiveId The ID provided by the system as part of the
+ * intent.action.UNARCHIVE broadcast with
+ * EXTRA_UNARCHIVE_ID.
+ * @param status Used for the system to provide the user with necessary
+ * follow-up steps or errors.
+ * @param requiredStorageBytes If the error is UNARCHIVAL_ERROR_INSUFFICIENT_STORAGE this
+ * field should be set to specify
+ * how many additional bytes of storage are required to
+ * unarchive the app.
+ * @param userActionIntent Optional intent to start a follow up action required to
+ * facilitate the unarchival flow
+ * (e.g,. user needs to log in).
+ * @hide
+ */
+ private UnarchivalState(
+ int unarchiveId,
+ @UnarchivalStatus int status,
+ long requiredStorageBytes,
+ @Nullable PendingIntent userActionIntent) {
+ this.mUnarchiveId = unarchiveId;
+ this.mStatus = status;
+ com.android.internal.util.AnnotationValidations.validate(
+ UnarchivalStatus.class, null, mStatus);
+ this.mRequiredStorageBytes = requiredStorageBytes;
+ this.mUserActionIntent = userActionIntent;
+ }
+
+ /**
+ * The ID provided by the system as part of the intent.action.UNARCHIVE broadcast with
+ * EXTRA_UNARCHIVE_ID.
+ *
+ * @hide
+ */
+ int getUnarchiveId() {
+ return mUnarchiveId;
+ }
+
+ /**
+ * Used for the system to provide the user with necessary follow-up steps or errors.
+ *
+ * @hide
+ */
+ @UnarchivalStatus int getStatus() {
+ return mStatus;
+ }
+
+ /**
+ * If the error is UNARCHIVAL_ERROR_INSUFFICIENT_STORAGE this field should be set to specify
+ * how many additional bytes of storage are required to unarchive the app.
+ *
+ * @hide
+ */
+ long getRequiredStorageBytes() {
+ return mRequiredStorageBytes;
+ }
+
+ /**
+ * Optional intent to start a follow up action required to facilitate the unarchival flow
+ * (e.g. user needs to log in).
+ *
+ * @hide
+ */
+ @Nullable PendingIntent getUserActionIntent() {
+ return mUserActionIntent;
+ }
+ }
+
}
diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java
index aabbe69..8744eae 100644
--- a/core/java/android/content/pm/PackageManager.java
+++ b/core/java/android/content/pm/PackageManager.java
@@ -128,6 +128,7 @@
* <a href="/training/basics/intents/package-visibility">manage package visibility</a>.
* </p>
*/
+@android.ravenwood.annotation.RavenwoodKeepPartialClass
public abstract class PackageManager {
private static final String TAG = "PackageManager";
@@ -1501,6 +1502,44 @@
public static final int ROLLBACK_DATA_POLICY_RETAIN = 2;
/** @hide */
+ @IntDef(prefix = {"ROLLBACK_USER_IMPACT_"}, value = {
+ ROLLBACK_USER_IMPACT_LOW,
+ ROLLBACK_USER_IMPACT_HIGH,
+ ROLLBACK_USER_IMPACT_ONLY_MANUAL,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface RollbackImpactLevel {}
+
+ /**
+ * Rollback will be performed automatically in response to native crashes on startup or
+ * persistent service crashes. More suitable for apps that do not store any user data.
+ *
+ * @hide
+ */
+ @SystemApi
+ @FlaggedApi(android.content.pm.Flags.FLAG_RECOVERABILITY_DETECTION)
+ public static final int ROLLBACK_USER_IMPACT_LOW = 0;
+
+ /**
+ * Rollback will be performed automatically only when the device is found to be unrecoverable.
+ * More suitable for apps that store user data and have higher impact on user.
+ *
+ * @hide
+ */
+ @SystemApi
+ @FlaggedApi(android.content.pm.Flags.FLAG_RECOVERABILITY_DETECTION)
+ public static final int ROLLBACK_USER_IMPACT_HIGH = 1;
+
+ /**
+ * Rollback will not be performed automatically. It can be triggered externally.
+ *
+ * @hide
+ */
+ @SystemApi
+ @FlaggedApi(android.content.pm.Flags.FLAG_RECOVERABILITY_DETECTION)
+ public static final int ROLLBACK_USER_IMPACT_ONLY_MANUAL = 2;
+
+ /** @hide */
@IntDef(flag = true, prefix = { "INSTALL_" }, value = {
INSTALL_REPLACE_EXISTING,
INSTALL_ALLOW_TEST,
@@ -5454,6 +5493,7 @@
* application info.
* @hide
*/
+ @android.ravenwood.annotation.RavenwoodKeepWholeClass
public static class Flags {
final long mValue;
protected Flags(long value) {
@@ -5468,6 +5508,7 @@
* Specific flags used for retrieving package info. Example:
* {@code PackageManager.getPackageInfo(packageName, PackageInfoFlags.of(0)}
*/
+ @android.ravenwood.annotation.RavenwoodKeepWholeClass
public final static class PackageInfoFlags extends Flags {
private PackageInfoFlags(@PackageInfoFlagsBits long value) {
super(value);
@@ -5481,6 +5522,7 @@
/**
* Specific flags used for retrieving application info.
*/
+ @android.ravenwood.annotation.RavenwoodKeepWholeClass
public final static class ApplicationInfoFlags extends Flags {
private ApplicationInfoFlags(@ApplicationInfoFlagsBits long value) {
super(value);
@@ -5494,6 +5536,7 @@
/**
* Specific flags used for retrieving component info.
*/
+ @android.ravenwood.annotation.RavenwoodKeepWholeClass
public final static class ComponentInfoFlags extends Flags {
private ComponentInfoFlags(@ComponentInfoFlagsBits long value) {
super(value);
@@ -5507,6 +5550,7 @@
/**
* Specific flags used for retrieving resolve info.
*/
+ @android.ravenwood.annotation.RavenwoodKeepWholeClass
public final static class ResolveInfoFlags extends Flags {
private ResolveInfoFlags(@ResolveInfoFlagsBits long value) {
super(value);
diff --git a/core/java/android/content/pm/ProviderInfo.java b/core/java/android/content/pm/ProviderInfo.java
index 9e553db..de33fa8 100644
--- a/core/java/android/content/pm/ProviderInfo.java
+++ b/core/java/android/content/pm/ProviderInfo.java
@@ -89,6 +89,15 @@
public static final int FLAG_VISIBLE_TO_INSTANT_APP = 0x100000;
/**
+ * Bit in {@link #flags}: If set, this provider will only be available
+ * for the system user.
+ * Set from the android.R.attr#systemUserOnly attribute.
+ * In Sync with {@link ActivityInfo#FLAG_SYSTEM_USER_ONLY}
+ * @hide
+ */
+ public static final int FLAG_SYSTEM_USER_ONLY = ActivityInfo.FLAG_SYSTEM_USER_ONLY;
+
+ /**
* Bit in {@link #flags}: If set, a single instance of the provider will
* run for all users on the device. Set from the
* {@link android.R.attr#singleUser} attribute.
diff --git a/core/java/android/content/pm/ServiceInfo.java b/core/java/android/content/pm/ServiceInfo.java
index ae46c027..2b378b1 100644
--- a/core/java/android/content/pm/ServiceInfo.java
+++ b/core/java/android/content/pm/ServiceInfo.java
@@ -101,6 +101,14 @@
public static final int FLAG_VISIBLE_TO_INSTANT_APP = 0x100000;
/**
+ * @hide Bit in {@link #flags}: If set, this service will only be available
+ * for the system user.
+ * Set from the android.R.attr#systemUserOnly attribute.
+ * In Sync with {@link ActivityInfo#FLAG_SYSTEM_USER_ONLY}
+ */
+ public static final int FLAG_SYSTEM_USER_ONLY = ActivityInfo.FLAG_SYSTEM_USER_ONLY;
+
+ /**
* Bit in {@link #flags}: If set, a single instance of the service will
* run for all users on the device. Set from the
* {@link android.R.attr#singleUser} attribute.
diff --git a/core/java/android/content/pm/UserInfo.java b/core/java/android/content/pm/UserInfo.java
index 8fd78bd..3e9f260 100644
--- a/core/java/android/content/pm/UserInfo.java
+++ b/core/java/android/content/pm/UserInfo.java
@@ -52,6 +52,7 @@
* @hide
*/
@TestApi
+@android.ravenwood.annotation.RavenwoodKeepWholeClass
public class UserInfo implements Parcelable {
/**
@@ -438,6 +439,7 @@
/**
* @return true if this user can be switched to.
**/
+ @android.ravenwood.annotation.RavenwoodThrow
public boolean supportsSwitchTo() {
if (partial || !isEnabled()) {
// Don't support switching to disabled or partial users, which includes users with
@@ -455,6 +457,7 @@
* @return true if user is of type {@link UserManager#USER_TYPE_SYSTEM_HEADLESS} and
* {@link com.android.internal.R.bool.config_canSwitchToHeadlessSystemUser} is true.
*/
+ @android.ravenwood.annotation.RavenwoodThrow
private boolean canSwitchToHeadlessSystemUser() {
return UserManager.USER_TYPE_SYSTEM_HEADLESS.equals(userType) && Resources.getSystem()
.getBoolean(com.android.internal.R.bool.config_canSwitchToHeadlessSystemUser);
@@ -465,6 +468,7 @@
* @deprecated Use {@link UserInfo#supportsSwitchTo} instead.
*/
@Deprecated
+ @android.ravenwood.annotation.RavenwoodThrow
public boolean supportsSwitchToByUser() {
return supportsSwitchTo();
}
diff --git a/core/java/android/content/pm/flags.aconfig b/core/java/android/content/pm/flags.aconfig
index a2cd3e1..fd87290 100644
--- a/core/java/android/content/pm/flags.aconfig
+++ b/core/java/android/content/pm/flags.aconfig
@@ -146,3 +146,25 @@
bug: "281848623"
}
+flag {
+ name: "recoverability_detection"
+ namespace: "package_manager_service"
+ description: "Feature flag to enable recoverability detection feature. It includes GMS core rollback and improvements to rescue party."
+ bug: "291135724"
+ is_fixed_read_only: true
+}
+
+flag {
+ name: "fix_system_apps_first_install_time"
+ namespace: "package_manager_service"
+ description: "Feature flag to fix the first-install timestamps for system apps."
+ bug: "321258605"
+ is_fixed_read_only: true
+}
+
+flag {
+ name: "allow_sdk_sandbox_query_intent_activities"
+ namespace: "package_manager_service"
+ description: "Feature flag to allow the sandbox SDK to query intent activities of the client app."
+ bug: "295842134"
+}
diff --git a/core/java/android/content/pm/multiuser.aconfig b/core/java/android/content/pm/multiuser.aconfig
index 5bfc012..c083437 100644
--- a/core/java/android/content/pm/multiuser.aconfig
+++ b/core/java/android/content/pm/multiuser.aconfig
@@ -84,4 +84,19 @@
namespace: "profile_experiences"
description: "Enable auto-locking private space on device restarts"
bug: "296993385"
-}
\ No newline at end of file
+}
+
+flag {
+ name: "enable_system_user_only_for_services_and_providers"
+ namespace: "multiuser"
+ description: "Enable systemUserOnly manifest attribute for services and providers."
+ bug: "302354856"
+ is_fixed_read_only: true
+}
+
+flag {
+ name: "allow_private_profile_apis"
+ namespace: "profile_experiences"
+ description: "Enable only the API changes to support private space"
+ bug: "299069460"
+}
diff --git a/core/java/android/content/pm/overlay/OverlayPaths.java b/core/java/android/content/pm/overlay/OverlayPaths.java
index a4db733..bd74b0b 100644
--- a/core/java/android/content/pm/overlay/OverlayPaths.java
+++ b/core/java/android/content/pm/overlay/OverlayPaths.java
@@ -49,6 +49,13 @@
public static class Builder {
final OverlayPaths mPaths = new OverlayPaths();
+ public Builder() {}
+
+ public Builder(@NonNull OverlayPaths base) {
+ mPaths.mResourceDirs.addAll(base.getResourceDirs());
+ mPaths.mOverlayPaths.addAll(base.getOverlayPaths());
+ }
+
/**
* Adds a non-APK path to the contents of {@link OverlayPaths#getOverlayPaths()}.
*/
diff --git a/core/java/android/content/res/FontScaleConverterFactory.java b/core/java/android/content/res/FontScaleConverterFactory.java
index cbe4c62..625d7cb 100644
--- a/core/java/android/content/res/FontScaleConverterFactory.java
+++ b/core/java/android/content/res/FontScaleConverterFactory.java
@@ -58,6 +58,16 @@
synchronized (LOOKUP_TABLES_WRITE_LOCK) {
putInto(
sLookupTables,
+ /* scaleKey= */ 1.1f,
+ new FontScaleConverterImpl(
+ /* fromSp= */
+ new float[] { 8f, 10f, 12f, 14f, 18f, 20f, 24f, 30f, 100},
+ /* toDp= */
+ new float[] { 8.8f, 11f, 13.2f, 15.6f, 19.2f, 21.2f, 24.8f, 30f, 100})
+ );
+
+ putInto(
+ sLookupTables,
/* scaleKey= */ 1.15f,
new FontScaleConverterImpl(
/* fromSp= */
diff --git a/core/java/android/content/rollback/RollbackInfo.java b/core/java/android/content/rollback/RollbackInfo.java
index a363718..d128055 100644
--- a/core/java/android/content/rollback/RollbackInfo.java
+++ b/core/java/android/content/rollback/RollbackInfo.java
@@ -16,8 +16,12 @@
package android.content.rollback;
+import android.annotation.FlaggedApi;
import android.annotation.NonNull;
import android.annotation.SystemApi;
+import android.annotation.TestApi;
+import android.content.pm.Flags;
+import android.content.pm.PackageManager;
import android.content.pm.VersionedPackage;
import android.os.Parcel;
import android.os.Parcelable;
@@ -25,17 +29,14 @@
import java.util.List;
/**
- * Information about a set of packages that can be, or already have been
- * rolled back together.
+ * Information about a set of packages that can be, or already have been rolled back together.
*
* @hide
*/
@SystemApi
public final class RollbackInfo implements Parcelable {
- /**
- * A unique identifier for the rollback.
- */
+ /** A unique identifier for the rollback. */
private final int mRollbackId;
private final List<PackageRollbackInfo> mPackages;
@@ -44,15 +45,39 @@
private final boolean mIsStaged;
private int mCommittedSessionId;
+ private int mRollbackImpactLevel;
/** @hide */
- public RollbackInfo(int rollbackId, List<PackageRollbackInfo> packages, boolean isStaged,
- List<VersionedPackage> causePackages, int committedSessionId) {
+ public RollbackInfo(
+ int rollbackId,
+ List<PackageRollbackInfo> packages,
+ boolean isStaged,
+ List<VersionedPackage> causePackages,
+ int committedSessionId,
+ @PackageManager.RollbackImpactLevel int rollbackImpactLevel) {
this.mRollbackId = rollbackId;
this.mPackages = packages;
this.mIsStaged = isStaged;
this.mCausePackages = causePackages;
this.mCommittedSessionId = committedSessionId;
+ this.mRollbackImpactLevel = rollbackImpactLevel;
+ }
+
+ /** @hide */
+ public RollbackInfo(
+ int rollbackId,
+ List<PackageRollbackInfo> packages,
+ boolean isStaged,
+ List<VersionedPackage> causePackages,
+ int committedSessionId) {
+ // If impact level is not set default to 0
+ this(
+ rollbackId,
+ packages,
+ isStaged,
+ causePackages,
+ committedSessionId,
+ PackageManager.ROLLBACK_USER_IMPACT_LOW);
}
private RollbackInfo(Parcel in) {
@@ -61,34 +86,28 @@
mIsStaged = in.readBoolean();
mCausePackages = in.createTypedArrayList(VersionedPackage.CREATOR);
mCommittedSessionId = in.readInt();
+ mRollbackImpactLevel = in.readInt();
}
- /**
- * Returns a unique identifier for this rollback.
- */
+ /** Returns a unique identifier for this rollback. */
public int getRollbackId() {
return mRollbackId;
}
- /**
- * Returns the list of package that are rolled back.
- */
+ /** Returns the list of package that are rolled back. */
@NonNull
public List<PackageRollbackInfo> getPackages() {
return mPackages;
}
- /**
- * Returns true if this rollback requires reboot to take effect after
- * being committed.
- */
+ /** Returns true if this rollback requires reboot to take effect after being committed. */
public boolean isStaged() {
return mIsStaged;
}
/**
- * Returns the session ID for the committed rollback for staged rollbacks.
- * Only applicable for rollbacks that have been committed.
+ * Returns the session ID for the committed rollback for staged rollbacks. Only applicable for
+ * rollbacks that have been committed.
*/
public int getCommittedSessionId() {
return mCommittedSessionId;
@@ -96,6 +115,7 @@
/**
* Sets the session ID for the committed rollback for staged rollbacks.
+ *
* @hide
*/
public void setCommittedSessionId(int sessionId) {
@@ -103,15 +123,40 @@
}
/**
- * Gets the list of package versions that motivated this rollback.
- * As provided to {@link #commitRollback} when the rollback was committed.
- * This is only applicable for rollbacks that have been committed.
+ * Gets the list of package versions that motivated this rollback. As provided to {@link
+ * #commitRollback} when the rollback was committed. This is only applicable for rollbacks that
+ * have been committed.
*/
@NonNull
public List<VersionedPackage> getCausePackages() {
return mCausePackages;
}
+ /**
+ * Get rollback impact level. Refer {@link
+ * android.content.pm.PackageInstaller.SessionParams#setRollbackImpactLevel(int)} for more info
+ * on impact level.
+ *
+ * @hide
+ */
+ @TestApi
+ @FlaggedApi(Flags.FLAG_RECOVERABILITY_DETECTION)
+ public @PackageManager.RollbackImpactLevel int getRollbackImpactLevel() {
+ return mRollbackImpactLevel;
+ }
+
+ /**
+ * Set rollback impact level. Refer {@link
+ * android.content.pm.PackageInstaller.SessionParams#setRollbackImpactLevel(int)} for more info
+ * on impact level.
+ *
+ * @hide
+ */
+ public void setRollbackImpactLevel(
+ @PackageManager.RollbackImpactLevel int rollbackImpactLevel) {
+ mRollbackImpactLevel = rollbackImpactLevel;
+ }
+
@Override
public int describeContents() {
return 0;
@@ -124,16 +169,17 @@
out.writeBoolean(mIsStaged);
out.writeTypedList(mCausePackages);
out.writeInt(mCommittedSessionId);
+ out.writeInt(mRollbackImpactLevel);
}
public static final @android.annotation.NonNull Parcelable.Creator<RollbackInfo> CREATOR =
new Parcelable.Creator<RollbackInfo>() {
- public RollbackInfo createFromParcel(Parcel in) {
- return new RollbackInfo(in);
- }
+ public RollbackInfo createFromParcel(Parcel in) {
+ return new RollbackInfo(in);
+ }
- public RollbackInfo[] newArray(int size) {
- return new RollbackInfo[size];
- }
- };
+ public RollbackInfo[] newArray(int size) {
+ return new RollbackInfo[size];
+ }
+ };
}
diff --git a/core/java/android/credentials/CredentialManager.java b/core/java/android/credentials/CredentialManager.java
index 796a57b..2e63664 100644
--- a/core/java/android/credentials/CredentialManager.java
+++ b/core/java/android/credentials/CredentialManager.java
@@ -32,6 +32,7 @@
import android.content.Context;
import android.content.IntentSender;
import android.os.Binder;
+import android.os.Bundle;
import android.os.CancellationSignal;
import android.os.IBinder;
import android.os.ICancellationSignal;
@@ -58,6 +59,9 @@
@SystemService(Context.CREDENTIAL_SERVICE)
public final class CredentialManager {
private static final String TAG = "CredentialManager";
+ private static final Bundle OPTIONS_SENDER_BAL_OPTIN = ActivityOptions.makeBasic()
+ .setPendingIntentBackgroundActivityStartMode(
+ ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED).toBundle();
/** @hide */
@IntDef(
@@ -757,9 +761,7 @@
public void onPendingIntent(PendingIntent pendingIntent) {
try {
mContext.startIntentSender(pendingIntent.getIntentSender(), null, 0, 0, 0,
- ActivityOptions.makeBasic()
- .setPendingIntentBackgroundActivityStartMode(
- ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED).toBundle());
+ OPTIONS_SENDER_BAL_OPTIN);
} catch (IntentSender.SendIntentException e) {
Log.e(
TAG,
@@ -817,7 +819,8 @@
@Override
public void onPendingIntent(PendingIntent pendingIntent) {
try {
- mContext.startIntentSender(pendingIntent.getIntentSender(), null, 0, 0, 0);
+ mContext.startIntentSender(pendingIntent.getIntentSender(), null, 0, 0, 0,
+ OPTIONS_SENDER_BAL_OPTIN);
} catch (IntentSender.SendIntentException e) {
Log.e(
TAG,
diff --git a/core/java/android/credentials/PrepareGetCredentialResponse.java b/core/java/android/credentials/PrepareGetCredentialResponse.java
index 212f571..75d671b 100644
--- a/core/java/android/credentials/PrepareGetCredentialResponse.java
+++ b/core/java/android/credentials/PrepareGetCredentialResponse.java
@@ -22,9 +22,11 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.RequiresPermission;
+import android.app.ActivityOptions;
import android.app.PendingIntent;
import android.content.Context;
import android.content.IntentSender;
+import android.os.Bundle;
import android.os.CancellationSignal;
import android.os.OutcomeReceiver;
import android.util.Log;
@@ -41,6 +43,10 @@
*/
public final class PrepareGetCredentialResponse {
+ private static final Bundle OPTIONS_SENDER_BAL_OPTIN = ActivityOptions.makeBasic()
+ .setPendingIntentBackgroundActivityStartMode(
+ ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED).toBundle();
+
/**
* A handle that represents a pending get-credential operation. Pass this handle to {@link
* CredentialManager#getCredential(Context, PendingGetCredentialHandle, CancellationSignal,
@@ -80,7 +86,8 @@
@Override
public void onPendingIntent(PendingIntent pendingIntent) {
try {
- context.startIntentSender(pendingIntent.getIntentSender(), null, 0, 0, 0);
+ context.startIntentSender(pendingIntent.getIntentSender(), null, 0, 0, 0,
+ OPTIONS_SENDER_BAL_OPTIN);
} catch (IntentSender.SendIntentException e) {
Log.e(TAG, "startIntentSender() failed for intent for show()", e);
executor.execute(() -> callback.onError(
@@ -101,7 +108,8 @@
});
try {
- context.startIntentSender(mPendingIntent.getIntentSender(), null, 0, 0, 0);
+ context.startIntentSender(mPendingIntent.getIntentSender(), null, 0, 0, 0,
+ OPTIONS_SENDER_BAL_OPTIN);
} catch (IntentSender.SendIntentException e) {
Log.e(TAG, "startIntentSender() failed for intent for show()", e);
executor.execute(() -> callback.onError(
diff --git a/core/java/android/credentials/ui/AuthenticationEntry.java b/core/java/android/credentials/ui/AuthenticationEntry.java
index b1a382c..9bd0871 100644
--- a/core/java/android/credentials/ui/AuthenticationEntry.java
+++ b/core/java/android/credentials/ui/AuthenticationEntry.java
@@ -34,15 +34,24 @@
/**
* An authentication entry.
*
+ * Applicable only for credential retrieval flow, authentication entries are a special type of
+ * entries that require the user to unlock the given provider before its credential options can
+ * be fully rendered.
+ *
* @hide
*/
@TestApi
public final class AuthenticationEntry implements Parcelable {
- @NonNull private final String mKey;
- @NonNull private final String mSubkey;
- @NonNull private final @Status int mStatus;
- @Nullable private Intent mFrameworkExtrasIntent;
- @NonNull private final Slice mSlice;
+ @NonNull
+ private final String mKey;
+ @NonNull
+ private final String mSubkey;
+ @NonNull
+ private final @Status int mStatus;
+ @Nullable
+ private Intent mFrameworkExtrasIntent;
+ @NonNull
+ private final Slice mSlice;
/** @hide **/
@IntDef(prefix = {"STATUS_"}, value = {
@@ -51,15 +60,21 @@
STATUS_UNLOCKED_BUT_EMPTY_MOST_RECENT,
})
@Retention(RetentionPolicy.SOURCE)
- public @interface Status {}
+ public @interface Status {
+ }
/** This entry is still locked, as initially supplied by the provider. */
public static final int STATUS_LOCKED = 0;
- /** This entry was unlocked but didn't contain any credential. Meanwhile, "less recent" means
- * there is another such entry that was unlocked more recently. */
+ /**
+ * This entry was unlocked but didn't contain any credential. Meanwhile, "less recent" means
+ * there is another such entry that was unlocked more recently.
+ */
public static final int STATUS_UNLOCKED_BUT_EMPTY_LESS_RECENT = 1;
- /** This is the most recent entry that was unlocked but didn't contain any credential.
- * There should be at most one authentication entry with this status. */
+ /**
+ * This is the most recent entry that was unlocked but didn't contain any credential.
+ *
+ * There will be at most one authentication entry with this status.
+ */
public static final int STATUS_UNLOCKED_BUT_EMPTY_MOST_RECENT = 2;
private AuthenticationEntry(@NonNull Parcel in) {
@@ -74,9 +89,11 @@
AnnotationValidations.validate(NonNull.class, null, mSlice);
}
- /** Constructor to be used for an entry that does not require further activities
+ /**
+ * Constructor to be used for an entry that does not require further activities
* to be invoked when selected.
*/
+ // TODO(b/322065508): remove this constructor.
public AuthenticationEntry(@NonNull String key, @NonNull String subkey, @NonNull Slice slice,
@Status int status) {
mKey = key;
@@ -95,9 +112,9 @@
}
/**
- * Returns the identifier of this entry that's unique within the context of the CredentialManager
- * request.
- */
+ * Returns the identifier of this entry that's unique within the context of the
+ * CredentialManager request.
+ */
@NonNull
public String getKey() {
return mKey;
@@ -111,23 +128,23 @@
return mSubkey;
}
- /**
- * Returns the Slice to be rendered.
- */
+ /** Returns the Slice to be rendered. */
@NonNull
public Slice getSlice() {
return mSlice;
}
- /**
- * Returns the entry status.
- */
+ /** Returns the entry status, depending on which the entry will be rendered differently. */
@NonNull
@Status
public int getStatus() {
return mStatus;
}
+ /**
+ * Returns the framework intent to be filled in when launching this entry's provider
+ * PendingIntent.
+ */
@Nullable
@SuppressLint("IntentBuilderName") // Not building a new intent.
public Intent getFrameworkExtrasIntent() {
diff --git a/core/java/android/credentials/ui/BaseDialogResult.java b/core/java/android/credentials/ui/BaseDialogResult.java
index e8cf5ab..e985a46 100644
--- a/core/java/android/credentials/ui/BaseDialogResult.java
+++ b/core/java/android/credentials/ui/BaseDialogResult.java
@@ -24,8 +24,6 @@
import android.os.Parcel;
import android.os.Parcelable;
-import com.android.internal.util.AnnotationValidations;
-
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -46,7 +44,7 @@
/**
* Used for the UX to construct the {@code resultData Bundle} to send via the {@code
- * ResultReceiver}.
+ * ResultReceiver}.
*/
public static void addToBundle(@NonNull BaseDialogResult result, @NonNull Bundle bundle) {
bundle.putParcelable(EXTRA_BASE_RESULT, result);
@@ -66,13 +64,14 @@
RESULT_CODE_DATA_PARSING_FAILURE,
})
@Retention(RetentionPolicy.SOURCE)
- public @interface ResultCode {}
+ public @interface ResultCode {
+ }
/** User intentionally canceled the dialog. */
public static final int RESULT_CODE_DIALOG_USER_CANCELED = 0;
/**
- * The user has consented to switching to a new default provider. The provider info is in the
- * {@code resultData}.
+ * The UI was stopped since the user has chosen to navigate to the Settings UI to reconfigure
+ * their providers.
*/
public static final int RESULT_CODE_CANCELED_AND_LAUNCHED_SETTINGS = 1;
/**
@@ -86,6 +85,7 @@
public static final int RESULT_CODE_DATA_PARSING_FAILURE = 3;
@Nullable
+ @Deprecated
private final IBinder mRequestToken;
public BaseDialogResult(@Nullable IBinder requestToken) {
@@ -94,6 +94,7 @@
/** Returns the unique identifier for the request that launched the operation. */
@Nullable
+ @Deprecated
public IBinder getRequestToken() {
return mRequestToken;
}
@@ -115,14 +116,14 @@
public static final @NonNull Creator<BaseDialogResult> CREATOR =
new Creator<BaseDialogResult>() {
- @Override
- public BaseDialogResult createFromParcel(@NonNull Parcel in) {
- return new BaseDialogResult(in);
- }
+ @Override
+ public BaseDialogResult createFromParcel(@NonNull Parcel in) {
+ return new BaseDialogResult(in);
+ }
- @Override
- public BaseDialogResult[] newArray(int size) {
- return new BaseDialogResult[size];
- }
- };
+ @Override
+ public BaseDialogResult[] newArray(int size) {
+ return new BaseDialogResult[size];
+ }
+ };
}
diff --git a/core/java/android/credentials/ui/CancelUiRequest.java b/core/java/android/credentials/ui/CancelUiRequest.java
index d4c249e..712424ce 100644
--- a/core/java/android/credentials/ui/CancelUiRequest.java
+++ b/core/java/android/credentials/ui/CancelUiRequest.java
@@ -24,7 +24,7 @@
import com.android.internal.util.AnnotationValidations;
/**
- * A request to cancel any ongoing UI matching this request.
+ * A request to cancel the ongoing UI matching the identifier token in this request.
*
* @hide
*/
@@ -33,9 +33,12 @@
/**
* The intent extra key for the {@code CancelUiRequest} object when launching the UX
* activities.
+ *
+ * @hide
*/
- @NonNull public static final String EXTRA_CANCEL_UI_REQUEST =
- "android.credentials.ui.extra.EXTRA_CANCEL_UI_REQUEST";
+ @NonNull
+ public static final String EXTRA_CANCEL_UI_REQUEST =
+ "android.credentials.ui.extra.CANCEL_UI_REQUEST";
@NonNull
private final IBinder mToken;
@@ -51,6 +54,10 @@
return mToken;
}
+ /**
+ * Returns the app package name invoking this request, that can be used to derive display
+ * metadata (e.g. "Cancelled by `App Name`").
+ */
@NonNull
public String getAppPackageName() {
return mAppPackageName;
@@ -64,6 +71,7 @@
return mShouldShowCancellationUi;
}
+ /** Constructs a {@link CancelUiRequest}. */
public CancelUiRequest(@NonNull IBinder token, boolean shouldShowCancellationUi,
@NonNull String appPackageName) {
mToken = token;
@@ -91,7 +99,8 @@
return 0;
}
- @NonNull public static final Creator<CancelUiRequest> CREATOR = new Creator<>() {
+ @NonNull
+ public static final Creator<CancelUiRequest> CREATOR = new Creator<>() {
@Override
public CancelUiRequest createFromParcel(@NonNull Parcel in) {
return new CancelUiRequest(in);
diff --git a/core/java/android/credentials/ui/Constants.java b/core/java/android/credentials/ui/Constants.java
index 37f850b..68f28e7 100644
--- a/core/java/android/credentials/ui/Constants.java
+++ b/core/java/android/credentials/ui/Constants.java
@@ -36,7 +36,5 @@
public static final String EXTRA_REQ_FOR_ALL_OPTIONS =
"android.credentials.ui.extra.REQ_FOR_ALL_OPTIONS";
- /** The intent action for when the enabled Credential Manager providers has been updated. */
- public static final String CREDMAN_ENABLED_PROVIDERS_UPDATED =
- "android.credentials.ui.action.CREDMAN_ENABLED_PROVIDERS_UPDATED";
+ private Constants() {}
}
diff --git a/core/java/android/credentials/ui/CreateCredentialProviderData.java b/core/java/android/credentials/ui/CreateCredentialProviderData.java
index 2508d8e..d7a4f5b 100644
--- a/core/java/android/credentials/ui/CreateCredentialProviderData.java
+++ b/core/java/android/credentials/ui/CreateCredentialProviderData.java
@@ -47,6 +47,17 @@
mRemoteEntry = remoteEntry;
}
+ /**
+ * Converts the instance to a {@link CreateCredentialProviderInfo}.
+ *
+ * @hide
+ */
+ @NonNull
+ public CreateCredentialProviderInfo toCreateCredentialProviderInfo() {
+ return new CreateCredentialProviderInfo(
+ getProviderFlattenedComponentName(), mSaveEntries, mRemoteEntry);
+ }
+
@NonNull
public List<Entry> getSaveEntries() {
return mSaveEntries;
diff --git a/core/java/android/credentials/ui/CreateCredentialProviderInfo.java b/core/java/android/credentials/ui/CreateCredentialProviderInfo.java
new file mode 100644
index 0000000..41ca852
--- /dev/null
+++ b/core/java/android/credentials/ui/CreateCredentialProviderInfo.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright 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 android.credentials.ui;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+
+import com.android.internal.util.Preconditions;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Information pertaining to a specific provider during the given create-credential flow.
+ *
+ * This includes provider metadata and its credential creation options for display purposes.
+ *
+ * @hide
+ */
+public final class CreateCredentialProviderInfo {
+
+ @NonNull
+ private final String mProviderName;
+
+ @NonNull
+ private final List<Entry> mSaveEntries;
+ @Nullable
+ private final Entry mRemoteEntry;
+
+ CreateCredentialProviderInfo(
+ @NonNull String providerName, @NonNull List<Entry> saveEntries,
+ @Nullable Entry remoteEntry) {
+ mProviderName = Preconditions.checkStringNotEmpty(providerName);
+ mSaveEntries = new ArrayList<>(saveEntries);
+ mRemoteEntry = remoteEntry;
+ }
+
+ /** Returns the fully-qualified provider (component or package) name. */
+ @NonNull
+ public String getProviderName() {
+ return mProviderName;
+ }
+
+ /** Returns all the options this provider has, to which the credential can be saved. */
+ @NonNull
+ public List<Entry> getSaveEntries() {
+ return mSaveEntries;
+ }
+
+ /**
+ * Returns the remote credential saving option, if any.
+ *
+ * Notice that only one system configured provider can set this option, and when set, it means
+ * that the system service has already validated the provider's eligibility.
+ */
+ @Nullable
+ public Entry getRemoteEntry() {
+ return mRemoteEntry;
+ }
+
+ /**
+ * Builder for {@link CreateCredentialProviderInfo}.
+ *
+ * @hide
+ */
+ public static final class Builder {
+ @NonNull
+ private String mProviderName;
+ @NonNull
+ private List<Entry> mSaveEntries = new ArrayList<>();
+ @Nullable
+ private Entry mRemoteEntry = null;
+
+ /** Constructor with required properties. */
+ public Builder(@NonNull String providerName) {
+ mProviderName = Preconditions.checkStringNotEmpty(providerName);
+ }
+
+ /** Sets the list of options for credential saving to be displayed to the user. */
+ @NonNull
+ public Builder setSaveEntries(@NonNull List<Entry> credentialEntries) {
+ mSaveEntries = credentialEntries;
+ return this;
+ }
+
+ /** Sets the remote entry of the provider. */
+ @NonNull
+ public Builder setRemoteEntry(@Nullable Entry remoteEntry) {
+ mRemoteEntry = remoteEntry;
+ return this;
+ }
+
+ /** Builds a {@link CreateCredentialProviderInfo}. */
+ @NonNull
+ public CreateCredentialProviderInfo build() {
+ return new CreateCredentialProviderInfo(mProviderName, mSaveEntries, mRemoteEntry);
+ }
+ }
+}
diff --git a/core/java/android/credentials/ui/DisabledProviderData.java b/core/java/android/credentials/ui/DisabledProviderData.java
index c266fd5..8bccdc9 100644
--- a/core/java/android/credentials/ui/DisabledProviderData.java
+++ b/core/java/android/credentials/ui/DisabledProviderData.java
@@ -34,6 +34,16 @@
super(providerFlattenedComponentName);
}
+ /**
+ * Converts the instance to a {@link DisabledProviderInfo}.
+ *
+ * @hide
+ */
+ @NonNull
+ public DisabledProviderInfo toDisabledProviderInfo() {
+ return new DisabledProviderInfo(getProviderFlattenedComponentName());
+ }
+
private DisabledProviderData(@NonNull Parcel in) {
super(in);
}
diff --git a/core/java/android/credentials/ui/DisabledProviderInfo.java b/core/java/android/credentials/ui/DisabledProviderInfo.java
new file mode 100644
index 0000000..7ce6368
--- /dev/null
+++ b/core/java/android/credentials/ui/DisabledProviderInfo.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 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 android.credentials.ui;
+
+import android.annotation.NonNull;
+
+import com.android.internal.util.Preconditions;
+
+/**
+ * Information pertaining to a specific provider that is disabled from the user settings.
+ *
+ * Currently, disabled provider data is only propagated in the create-credential flow.
+ *
+ * @hide
+ */
+public final class DisabledProviderInfo {
+
+ @NonNull
+ private final String mProviderName;
+
+ /**
+ * Constructs a {@link DisabledProviderInfo}.
+ *
+ * @throws IllegalArgumentException if {@code providerName} is empty
+ */
+ public DisabledProviderInfo(
+ @NonNull String providerName) {
+ mProviderName = Preconditions.checkStringNotEmpty(providerName);
+ }
+
+ /** Returns the fully-qualified provider (component or package) name. */
+ @NonNull
+ public String getProviderName() {
+ return mProviderName;
+ }
+}
diff --git a/core/java/android/credentials/ui/Entry.java b/core/java/android/credentials/ui/Entry.java
index 55f2a3e..8469447 100644
--- a/core/java/android/credentials/ui/Entry.java
+++ b/core/java/android/credentials/ui/Entry.java
@@ -35,10 +35,14 @@
*/
@TestApi
public final class Entry implements Parcelable {
- @NonNull private final String mKey;
- @NonNull private final String mSubkey;
- @Nullable private PendingIntent mPendingIntent;
- @Nullable private Intent mFrameworkExtrasIntent;
+ @NonNull
+ private final String mKey;
+ @NonNull
+ private final String mSubkey;
+ @Nullable
+ private PendingIntent mPendingIntent;
+ @Nullable
+ private Intent mFrameworkExtrasIntent;
@NonNull
private final Slice mSlice;
@@ -58,16 +62,19 @@
mFrameworkExtrasIntent = in.readTypedObject(Intent.CREATOR);
}
- /** Constructor to be used for an entry that does not require further activities
+ /**
+ * Constructor to be used for an entry that does not require further activities
* to be invoked when selected.
*/
+ // TODO(b/322065508): deprecate this constructor.
public Entry(@NonNull String key, @NonNull String subkey, @NonNull Slice slice) {
mKey = key;
mSubkey = subkey;
mSlice = slice;
}
- /** Constructor to be used for an entry that requires a pending intent to be invoked
+ /**
+ * Constructor to be used for an entry that requires a pending intent to be invoked
* when clicked.
*/
public Entry(@NonNull String key, @NonNull String subkey, @NonNull Slice slice,
@@ -77,9 +84,12 @@
}
/**
- * Returns the identifier of this entry that's unique within the context of the CredentialManager
- * request.
- */
+ * Returns the identifier of this entry that's unique within the context of the
+ * CredentialManager
+ * request.
+ *
+ * Generally used when sending the user selection result back to the system service.
+ */
@NonNull
public String getKey() {
return mKey;
@@ -87,25 +97,33 @@
/**
* Returns the sub-identifier of this entry that's unique within the context of the {@code key}.
+ *
+ * Generally used when sending the user selection result back to the system service.
*/
@NonNull
public String getSubkey() {
return mSubkey;
}
- /**
- * Returns the Slice to be rendered.
- */
+ /** Returns the Slice to be rendered. */
@NonNull
public Slice getSlice() {
return mSlice;
}
+ /**
+ * Returns the provider PendingIntent to launch once this entry is selected.
+ */
+ // TODO(b/322065508): deprecate this bit.
@Nullable
public PendingIntent getPendingIntent() {
return mPendingIntent;
}
+ /**
+ * Returns the framework fill in intent to add to the provider PendingIntent to launch, once
+ * this entry is selected.
+ */
@Nullable
@SuppressLint("IntentBuilderName") // Not building a new intent.
public Intent getFrameworkExtrasIntent() {
@@ -126,7 +144,7 @@
return 0;
}
- public static final @NonNull Creator<Entry> CREATOR = new Creator<Entry>() {
+ public static final @NonNull Creator<Entry> CREATOR = new Creator<>() {
@Override
public Entry createFromParcel(@NonNull Parcel in) {
return new Entry(in);
diff --git a/core/java/android/credentials/ui/FailureDialogResult.java b/core/java/android/credentials/ui/FailureDialogResult.java
new file mode 100644
index 0000000..abd5a92
--- /dev/null
+++ b/core/java/android/credentials/ui/FailureDialogResult.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright 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 android.credentials.ui;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * Result data when the selector UI has encountered a failure.
+ *
+ * @hide
+ */
+public final class FailureDialogResult extends BaseDialogResult implements Parcelable {
+ /** Parses and returns a UserSelectionDialogResult from the given resultData. */
+ @Nullable
+ public static FailureDialogResult fromResultData(@NonNull Bundle resultData) {
+ return resultData.getParcelable(
+ EXTRA_FAILURE_RESULT, FailureDialogResult.class);
+ }
+
+ /**
+ * Used for the UX to construct the {@code resultData Bundle} to send via the {@code
+ * ResultReceiver}.
+ */
+ public static void addToBundle(
+ @NonNull FailureDialogResult result, @NonNull Bundle bundle) {
+ bundle.putParcelable(EXTRA_FAILURE_RESULT, result);
+ }
+
+ /**
+ * The intent extra key for the {@code UserSelectionDialogResult} object when the credential
+ * selector activity finishes.
+ */
+ private static final String EXTRA_FAILURE_RESULT =
+ "android.credentials.ui.extra.FAILURE_RESULT";
+
+ @Nullable
+ private final String mErrorMessage;
+
+ public FailureDialogResult(@Nullable IBinder requestToken, @Nullable String errorMessage) {
+ super(requestToken);
+ mErrorMessage = errorMessage;
+ }
+
+ /** Returns provider package name whose entry was selected by the user. */
+ @Nullable
+ public String getErrorMessage() {
+ return mErrorMessage;
+ }
+
+ protected FailureDialogResult(@NonNull Parcel in) {
+ super(in);
+ mErrorMessage = in.readString8();
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ super.writeToParcel(dest, flags);
+ dest.writeString8(mErrorMessage);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ public static final @NonNull Creator<FailureDialogResult> CREATOR =
+ new Creator<>() {
+ @Override
+ public FailureDialogResult createFromParcel(@NonNull Parcel in) {
+ return new FailureDialogResult(in);
+ }
+
+ @Override
+ public FailureDialogResult[] newArray(int size) {
+ return new FailureDialogResult[size];
+ }
+ };
+}
diff --git a/core/java/android/credentials/ui/FailureResult.java b/core/java/android/credentials/ui/FailureResult.java
new file mode 100644
index 0000000..ec58417
--- /dev/null
+++ b/core/java/android/credentials/ui/FailureResult.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright 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 android.credentials.ui;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Failure or cancellation result encountered during a UI flow.
+ *
+ * @hide
+ */
+public final class FailureResult implements UiResult {
+ @Nullable
+ private final String mErrorMessage;
+ @NonNull
+ private final int mErrorCode;
+
+ /** @hide **/
+ @IntDef(prefix = {"ERROR_CODE_"}, value = {
+ ERROR_CODE_DIALOG_CANCELED_BY_USER,
+ ERROR_CODE_CANCELED_AND_LAUNCHED_SETTINGS,
+ ERROR_CODE_UI_FAILURE,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface ErrorCode {
+ }
+
+ /**
+ * The UI was stopped due to a failure, e.g. because it failed to parse the incoming data,
+ * or it encountered an irrecoverable internal issue.
+ */
+ public static final int ERROR_CODE_UI_FAILURE = 0;
+ /** The user intentionally canceled the dialog. */
+ public static final int ERROR_CODE_DIALOG_CANCELED_BY_USER = 1;
+ /**
+ * The UI was stopped since the user has chosen to navigate to the Settings UI to reconfigure
+ * their providers.
+ */
+ public static final int ERROR_CODE_CANCELED_AND_LAUNCHED_SETTINGS = 2;
+
+ /**
+ * Constructs a {@link FailureResult}.
+ *
+ * @throws IllegalArgumentException if {@code providerId} is empty
+ */
+ public FailureResult(@ErrorCode int errorCode, @Nullable String errorMessage) {
+ mErrorCode = errorCode;
+ mErrorMessage = errorMessage;
+ }
+
+ /** Returns the error code. */
+ @ErrorCode
+ public int getErrorCode() {
+ return mErrorCode;
+ }
+
+ /** Returns the error message. */
+ @Nullable
+ public String getErrorMessage() {
+ return mErrorMessage;
+ }
+
+ FailureDialogResult toFailureDialogResult() {
+ return new FailureDialogResult(/*requestToken=*/null, mErrorMessage);
+ }
+
+ int errorCodeToResultCode() {
+ switch (mErrorCode) {
+ case ERROR_CODE_DIALOG_CANCELED_BY_USER:
+ return BaseDialogResult.RESULT_CODE_DIALOG_USER_CANCELED;
+ case ERROR_CODE_CANCELED_AND_LAUNCHED_SETTINGS:
+ return BaseDialogResult.RESULT_CODE_CANCELED_AND_LAUNCHED_SETTINGS;
+ default:
+ return BaseDialogResult.RESULT_CODE_DATA_PARSING_FAILURE;
+ }
+ }
+}
diff --git a/core/java/android/credentials/ui/GetCredentialProviderData.java b/core/java/android/credentials/ui/GetCredentialProviderData.java
index 181475c..481419b 100644
--- a/core/java/android/credentials/ui/GetCredentialProviderData.java
+++ b/core/java/android/credentials/ui/GetCredentialProviderData.java
@@ -55,6 +55,17 @@
mRemoteEntry = remoteEntry;
}
+ /**
+ * Converts the instance to a {@link GetCredentialProviderInfo}.
+ *
+ * @hide
+ */
+ @NonNull
+ public GetCredentialProviderInfo toGetCredentialProviderInfo() {
+ return new GetCredentialProviderInfo(getProviderFlattenedComponentName(),
+ mCredentialEntries, mActionChips, mAuthenticationEntries, mRemoteEntry);
+ }
+
@NonNull
public List<Entry> getCredentialEntries() {
return mCredentialEntries;
@@ -83,12 +94,12 @@
mCredentialEntries = credentialEntries;
AnnotationValidations.validate(NonNull.class, null, mCredentialEntries);
- List<Entry> actionChips = new ArrayList<>();
+ List<Entry> actionChips = new ArrayList<>();
in.readTypedList(actionChips, Entry.CREATOR);
mActionChips = actionChips;
AnnotationValidations.validate(NonNull.class, null, mActionChips);
- List<AuthenticationEntry> authenticationEntries = new ArrayList<>();
+ List<AuthenticationEntry> authenticationEntries = new ArrayList<>();
in.readTypedList(authenticationEntries, AuthenticationEntry.CREATOR);
mAuthenticationEntries = authenticationEntries;
AnnotationValidations.validate(NonNull.class, null, mAuthenticationEntries);
@@ -113,16 +124,16 @@
public static final @NonNull Creator<GetCredentialProviderData> CREATOR =
new Creator<GetCredentialProviderData>() {
- @Override
- public GetCredentialProviderData createFromParcel(@NonNull Parcel in) {
- return new GetCredentialProviderData(in);
- }
+ @Override
+ public GetCredentialProviderData createFromParcel(@NonNull Parcel in) {
+ return new GetCredentialProviderData(in);
+ }
- @Override
- public GetCredentialProviderData[] newArray(int size) {
- return new GetCredentialProviderData[size];
- }
- };
+ @Override
+ public GetCredentialProviderData[] newArray(int size) {
+ return new GetCredentialProviderData[size];
+ }
+ };
/**
* Builder for {@link GetCredentialProviderData}.
@@ -131,11 +142,16 @@
*/
@TestApi
public static final class Builder {
- @NonNull private String mProviderFlattenedComponentName;
- @NonNull private List<Entry> mCredentialEntries = new ArrayList<>();
- @NonNull private List<Entry> mActionChips = new ArrayList<>();
- @NonNull private List<AuthenticationEntry> mAuthenticationEntries = new ArrayList<>();
- @Nullable private Entry mRemoteEntry = null;
+ @NonNull
+ private String mProviderFlattenedComponentName;
+ @NonNull
+ private List<Entry> mCredentialEntries = new ArrayList<>();
+ @NonNull
+ private List<Entry> mActionChips = new ArrayList<>();
+ @NonNull
+ private List<AuthenticationEntry> mAuthenticationEntries = new ArrayList<>();
+ @Nullable
+ private Entry mRemoteEntry = null;
/** Constructor with required properties. */
public Builder(@NonNull String providerFlattenedComponentName) {
diff --git a/core/java/android/credentials/ui/GetCredentialProviderInfo.java b/core/java/android/credentials/ui/GetCredentialProviderInfo.java
new file mode 100644
index 0000000..bac7147
--- /dev/null
+++ b/core/java/android/credentials/ui/GetCredentialProviderInfo.java
@@ -0,0 +1,168 @@
+/*
+ * Copyright 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 android.credentials.ui;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+
+import com.android.internal.util.Preconditions;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Information pertaining to a specific provider during the given create-credential flow.
+ *
+ * This includes provider metadata and its credential creation options for display purposes.
+ *
+ * @hide
+ */
+public final class GetCredentialProviderInfo {
+
+ @NonNull
+ private final String mProviderName;
+
+ @NonNull
+ private final List<Entry> mCredentialEntries;
+ @NonNull
+ private final List<Entry> mActionChips;
+ @NonNull
+ private final List<AuthenticationEntry> mAuthenticationEntries;
+ @Nullable
+ private final Entry mRemoteEntry;
+
+ GetCredentialProviderInfo(
+ @NonNull String providerName, @NonNull List<Entry> credentialEntries,
+ @NonNull List<Entry> actionChips,
+ @NonNull List<AuthenticationEntry> authenticationEntries,
+ @Nullable Entry remoteEntry) {
+ mProviderName = Preconditions.checkStringNotEmpty(providerName);
+ mCredentialEntries = new ArrayList<>(credentialEntries);
+ mActionChips = new ArrayList<>(actionChips);
+ mAuthenticationEntries = new ArrayList<>(authenticationEntries);
+ mRemoteEntry = remoteEntry;
+ }
+
+ /** Returns the fully-qualified provider (component or package) name. */
+ @NonNull
+ public String getProviderName() {
+ return mProviderName;
+ }
+
+ /** Returns the display information for all the candidate credentials this provider has. */
+ @NonNull
+ public List<Entry> getCredentialEntries() {
+ return mCredentialEntries;
+ }
+
+ /**
+ * Returns a list of actions defined by the provider that intent into the provider's app for
+ * specific user actions, each of which should eventually lead to an actual credential.
+ */
+ @NonNull
+ public List<Entry> getActionChips() {
+ return mActionChips;
+ }
+
+ /**
+ * Returns a list of authentication actions that each intents into a provider authentication
+ * activity.
+ *
+ * When the authentication activity succeeds, the provider will return a list of actual
+ * credential candidates to render. However, the UI should not attempt to parse the result
+ * itself, but rather send the result back to the system service, which will then process the
+ * new candidates and relaunch the UI with updated display data.
+ */
+ @NonNull
+ public List<AuthenticationEntry> getAuthenticationEntries() {
+ return mAuthenticationEntries;
+ }
+
+ /**
+ * Returns the remote credential retrieval option, if any.
+ *
+ * Notice that only one system configured provider can set this option, and when set, it means
+ * that the system service has already validated the provider's eligibility.
+ */
+ @Nullable
+ public Entry getRemoteEntry() {
+ return mRemoteEntry;
+ }
+
+ /**
+ * Builder for {@link GetCredentialProviderInfo}.
+ *
+ * @hide
+ */
+ public static final class Builder {
+ @NonNull
+ private String mProviderName;
+ @NonNull
+ private List<Entry> mCredentialEntries = new ArrayList<>();
+ @NonNull
+ private List<Entry> mActionChips = new ArrayList<>();
+ @NonNull
+ private List<AuthenticationEntry> mAuthenticationEntries = new ArrayList<>();
+ @Nullable
+ private Entry mRemoteEntry = null;
+
+ /**
+ * Constructs a {@link GetCredentialProviderInfo.Builder}.
+ *
+ * @throws IllegalArgumentException if {@code providerName} is null or empty
+ */
+ public Builder(@NonNull String providerName) {
+ mProviderName = Preconditions.checkStringNotEmpty(providerName);
+ }
+
+ /** Sets the list of credential candidates to be displayed to the user. */
+ @NonNull
+ public Builder setCredentialEntries(@NonNull List<Entry> credentialEntries) {
+ mCredentialEntries = credentialEntries;
+ return this;
+ }
+
+ /** Sets the list of action chips to be displayed to the user. */
+ @NonNull
+ public Builder setActionChips(@NonNull List<Entry> actionChips) {
+ mActionChips = actionChips;
+ return this;
+ }
+
+ /** Sets the authentication entry to be displayed to the user. */
+ @NonNull
+ public Builder setAuthenticationEntries(
+ @NonNull List<AuthenticationEntry> authenticationEntry) {
+ mAuthenticationEntries = authenticationEntry;
+ return this;
+ }
+
+ /** Sets the remote entry to be displayed to the user. */
+ @NonNull
+ public Builder setRemoteEntry(@Nullable Entry remoteEntry) {
+ mRemoteEntry = remoteEntry;
+ return this;
+ }
+
+ /** Builds a {@link GetCredentialProviderInfo}. */
+ @NonNull
+ public GetCredentialProviderInfo build() {
+ return new GetCredentialProviderInfo(mProviderName,
+ mCredentialEntries, mActionChips, mAuthenticationEntries, mRemoteEntry);
+ }
+ }
+}
diff --git a/core/java/android/credentials/ui/IntentFactory.java b/core/java/android/credentials/ui/IntentFactory.java
index 49321d5..5e1e0ef 100644
--- a/core/java/android/credentials/ui/IntentFactory.java
+++ b/core/java/android/credentials/ui/IntentFactory.java
@@ -113,25 +113,6 @@
}
/**
- * Notify the UI that providers have been enabled/disabled.
- *
- * @hide
- */
- @NonNull
- public static Intent createProviderUpdateIntent() {
- Intent intent = new Intent();
- ComponentName componentName =
- ComponentName.unflattenFromString(
- Resources.getSystem()
- .getString(
- com.android.internal.R.string
- .config_credentialManagerReceiverComponent));
- intent.setComponent(componentName);
- intent.setAction(Constants.CREDMAN_ENABLED_PROVIDERS_UPDATED);
- return intent;
- }
-
- /**
* Convert an instance of a "locally-defined" ResultReceiver to an instance of {@link
* android.os.ResultReceiver} itself, which the receiving process will be able to unmarshall.
*/
diff --git a/core/java/android/credentials/ui/IntentHelper.java b/core/java/android/credentials/ui/IntentHelper.java
new file mode 100644
index 0000000..c5f34c1
--- /dev/null
+++ b/core/java/android/credentials/ui/IntentHelper.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.credentials.ui;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SuppressLint;
+import android.content.Intent;
+import android.os.ResultReceiver;
+
+import java.util.List;
+
+/**
+ * Utilities for parsing the intent data used to launch the UI activity.
+ *
+ * @hide
+ */
+public final class IntentHelper {
+ /**
+ * Attempts to extract a {@link CancelUiRequest} from the given intent; returns null
+ * if not found.
+ */
+ @Nullable
+ public static CancelUiRequest extractCancelUiRequest(@NonNull Intent intent) {
+ return intent.getParcelableExtra(CancelUiRequest.EXTRA_CANCEL_UI_REQUEST,
+ CancelUiRequest.class);
+ }
+
+ /**
+ * Attempts to extract a {@link RequestInfo} from the given intent; returns null
+ * if not found.
+ */
+ @Nullable
+ public static RequestInfo extractRequestInfo(@NonNull Intent intent) {
+ return intent.getParcelableExtra(RequestInfo.EXTRA_REQUEST_INFO,
+ RequestInfo.class);
+ }
+
+ /**
+ * Attempts to extract the list of {@link GetCredentialProviderInfo} from the given intent;
+ * returns null if not found.
+ */
+ @Nullable
+ @SuppressLint("NullableCollection") // To be consistent with the nullable Intent extra APIs
+ // and the other APIs in this class.
+ public static List<GetCredentialProviderInfo> extractGetCredentialProviderDataList(
+ @NonNull Intent intent) {
+ List<GetCredentialProviderData> providerList = intent.getParcelableArrayListExtra(
+ ProviderData.EXTRA_ENABLED_PROVIDER_DATA_LIST,
+ GetCredentialProviderData.class);
+ return providerList == null ? null : providerList.stream().map(
+ GetCredentialProviderData::toGetCredentialProviderInfo).toList();
+ }
+
+ /**
+ * Attempts to extract the list of {@link CreateCredentialProviderInfo} from the given intent;
+ * returns null if not found.
+ */
+ @Nullable
+ @SuppressLint("NullableCollection") // To be consistent with the nullable Intent extra APIs
+ // and the other APIs in this class.
+ public static List<CreateCredentialProviderInfo> extractCreateCredentialProviderDataList(
+ @NonNull Intent intent) {
+ List<CreateCredentialProviderData> providerList = intent.getParcelableArrayListExtra(
+ ProviderData.EXTRA_ENABLED_PROVIDER_DATA_LIST,
+ CreateCredentialProviderData.class);
+ return providerList == null ? null : providerList.stream().map(
+ CreateCredentialProviderData::toCreateCredentialProviderInfo).toList();
+ }
+
+ /**
+ * Attempts to extract a {@link android.os.ResultReceiver} from the given intent, which should
+ * be used to send back UI results; returns null if not found.
+ */
+ @Nullable
+ public static ResultReceiver extractResultReceiver(@NonNull Intent intent) {
+ return intent.getParcelableExtra(Constants.EXTRA_RESULT_RECEIVER,
+ ResultReceiver.class);
+ }
+
+ private IntentHelper() {
+ }
+}
diff --git a/core/java/android/credentials/ui/ProviderDialogResult.java b/core/java/android/credentials/ui/ProviderDialogResult.java
deleted file mode 100644
index 53f1864..0000000
--- a/core/java/android/credentials/ui/ProviderDialogResult.java
+++ /dev/null
@@ -1,100 +0,0 @@
-/*
- * Copyright 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 android.credentials.ui;
-
-import android.annotation.NonNull;
-import android.annotation.Nullable;
-import android.os.Bundle;
-import android.os.IBinder;
-import android.os.Parcel;
-import android.os.Parcelable;
-
-import com.android.internal.util.AnnotationValidations;
-
-/**
- * Result data matching {@link BaseDialogResult#RESULT_CODE_PROVIDER_ENABLED}, or {@link
- * BaseDialogResult#RESULT_CODE_DEFAULT_PROVIDER_CHANGED}.
- *
- * @hide
- */
-public final class ProviderDialogResult extends BaseDialogResult implements Parcelable {
- /** Parses and returns a ProviderDialogResult from the given resultData. */
- @Nullable
- public static ProviderDialogResult fromResultData(@NonNull Bundle resultData) {
- return resultData.getParcelable(EXTRA_PROVIDER_RESULT, ProviderDialogResult.class);
- }
-
- /**
- * Used for the UX to construct the {@code resultData Bundle} to send via the {@code
- * ResultReceiver}.
- */
- public static void addToBundle(
- @NonNull ProviderDialogResult result, @NonNull Bundle bundle) {
- bundle.putParcelable(EXTRA_PROVIDER_RESULT, result);
- }
-
- /**
- * The intent extra key for the {@code ProviderDialogResult} object when the credential
- * selector activity finishes.
- */
- private static final String EXTRA_PROVIDER_RESULT =
- "android.credentials.ui.extra.PROVIDER_RESULT";
-
- @NonNull
- private final String mProviderId;
-
- public ProviderDialogResult(@NonNull IBinder requestToken, @NonNull String providerId) {
- super(requestToken);
- mProviderId = providerId;
- }
-
- @NonNull
- public String getProviderId() {
- return mProviderId;
- }
-
- protected ProviderDialogResult(@NonNull Parcel in) {
- super(in);
- String providerId = in.readString8();
- mProviderId = providerId;
- AnnotationValidations.validate(NonNull.class, null, mProviderId);
- }
-
- @Override
- public void writeToParcel(@NonNull Parcel dest, int flags) {
- super.writeToParcel(dest, flags);
- dest.writeString8(mProviderId);
- }
-
- @Override
- public int describeContents() {
- return 0;
- }
-
- public static final @NonNull Creator<ProviderDialogResult> CREATOR =
- new Creator<ProviderDialogResult>() {
- @Override
- public ProviderDialogResult createFromParcel(@NonNull Parcel in) {
- return new ProviderDialogResult(in);
- }
-
- @Override
- public ProviderDialogResult[] newArray(int size) {
- return new ProviderDialogResult[size];
- }
- };
-}
diff --git a/core/java/android/credentials/ui/ProviderPendingIntentResponse.java b/core/java/android/credentials/ui/ProviderPendingIntentResponse.java
index 47936c4..11cc21f9 100644
--- a/core/java/android/credentials/ui/ProviderPendingIntentResponse.java
+++ b/core/java/android/credentials/ui/ProviderPendingIntentResponse.java
@@ -16,15 +16,20 @@
package android.credentials.ui;
+import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.annotation.SuppressLint;
import android.content.Intent;
import android.os.Parcel;
import android.os.Parcelable;
-import androidx.annotation.NonNull;
-
/**
- * Response from a provider's pending intent
+ * Result of launching a provider's PendingIntent associated with an {@link Entry} after it is
+ * selected by the user.
+ *
+ * The provider sets the credential creation / retrieval result through
+ * {@link android.app.Activity#setResult(int, Intent)}, which is then directly propagated back
+ * through this data structure.
*
* @hide
*/
@@ -33,20 +38,21 @@
@Nullable
private final Intent mResultData;
+ /** Constructs a {@link ProviderPendingIntentResponse}. */
public ProviderPendingIntentResponse(int resultCode, @Nullable Intent resultData) {
mResultCode = resultCode;
mResultData = resultData;
}
- protected ProviderPendingIntentResponse(Parcel in) {
+ private ProviderPendingIntentResponse(@NonNull Parcel in) {
mResultCode = in.readInt();
mResultData = in.readTypedObject(Intent.CREATOR);
}
- public static final Creator<ProviderPendingIntentResponse> CREATOR =
- new Creator<ProviderPendingIntentResponse>() {
+ public static final @NonNull Creator<ProviderPendingIntentResponse> CREATOR =
+ new Creator<>() {
@Override
- public ProviderPendingIntentResponse createFromParcel(Parcel in) {
+ public ProviderPendingIntentResponse createFromParcel(@NonNull Parcel in) {
return new ProviderPendingIntentResponse(in);
}
@@ -67,13 +73,15 @@
dest.writeTypedObject(mResultData, flags);
}
- /** Returns the result code associated with this pending intent activity result. */
+ /** Returns the result code associated with this provider PendingIntent activity result. */
public int getResultCode() {
return mResultCode;
}
- /** Returns the result data associated with this pending intent activity result. */
- @NonNull public Intent getResultData() {
+ /** Returns the result data associated with this provider PendingIntent activity result. */
+ @SuppressLint("IntentBuilderName") // Not building a new intent.
+ @NonNull
+ public Intent getResultData() {
return mResultData;
}
}
diff --git a/core/java/android/credentials/ui/RequestInfo.java b/core/java/android/credentials/ui/RequestInfo.java
index 4fedc83..f651584 100644
--- a/core/java/android/credentials/ui/RequestInfo.java
+++ b/core/java/android/credentials/ui/RequestInfo.java
@@ -48,17 +48,24 @@
@NonNull public static final String EXTRA_REQUEST_INFO =
"android.credentials.ui.extra.REQUEST_INFO";
- /** Type value for any request that does not require UI. */
+ /**
+ * Type value for any request that does not require UI.
+ */
@NonNull public static final String TYPE_UNDEFINED = "android.credentials.ui.TYPE_UNDEFINED";
- /** Type value for a getCredential request. */
+ /**
+ * Type value for a getCredential request.
+ */
@NonNull public static final String TYPE_GET = "android.credentials.ui.TYPE_GET";
- /** Type value for a getCredential request that utilizes the credential registry.
+ /**
+ * Type value for a getCredential request that utilizes the credential registry.
*
* @hide
- **/
+ */
@NonNull public static final String TYPE_GET_VIA_REGISTRY =
"android.credentials.ui.TYPE_GET_VIA_REGISTRY";
- /** Type value for a createCredential request. */
+ /**
+ * Type value for a createCredential request.
+ */
@NonNull public static final String TYPE_CREATE = "android.credentials.ui.TYPE_CREATE";
/** @hide */
diff --git a/core/java/android/credentials/ui/ResultHelper.java b/core/java/android/credentials/ui/ResultHelper.java
new file mode 100644
index 0000000..7b9d5e8
--- /dev/null
+++ b/core/java/android/credentials/ui/ResultHelper.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.credentials.ui;
+
+import android.annotation.NonNull;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.ResultReceiver;
+
+/**
+ * Utilities for sending the UI results back to the system service.
+ *
+ * @hide
+ */
+public final class ResultHelper {
+ /**
+ * Sends the {@code failureResult} that caused the UI to stop back to the CredentialManager
+ * service.
+ *
+ * The {code resultReceiver} for a UI flow can be extracted from the UI launch intent via
+ * {@link IntentHelper#extractResultReceiver(Intent)}.
+ */
+ public static void sendFailureResult(@NonNull ResultReceiver resultReceiver,
+ @NonNull FailureResult failureResult) {
+ FailureDialogResult result = failureResult.toFailureDialogResult();
+ Bundle resultData = new Bundle();
+ FailureDialogResult.addToBundle(result, resultData);
+ resultReceiver.send(failureResult.errorCodeToResultCode(),
+ resultData);
+ }
+
+ /**
+ * Sends the completed {@code userSelectionResult} back to the CredentialManager service.
+ *
+ * The {code resultReceiver} for a UI flow can be extracted from the UI launch intent via
+ * {@link IntentHelper#extractResultReceiver(Intent)}.
+ */
+ public static void sendUserSelectionResult(@NonNull ResultReceiver resultReceiver,
+ @NonNull UserSelectionResult userSelectionResult) {
+ UserSelectionDialogResult result = userSelectionResult.toUserSelectionDialogResult();
+ Bundle resultData = new Bundle();
+ UserSelectionDialogResult.addToBundle(result, resultData);
+ resultReceiver.send(BaseDialogResult.RESULT_CODE_DIALOG_COMPLETE_WITH_SELECTION,
+ resultData);
+ }
+
+ private ResultHelper() {}
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/VolumePanelComponentKey.kt b/core/java/android/credentials/ui/UiResult.java
similarity index 70%
copy from packages/SystemUI/src/com/android/systemui/volume/panel/VolumePanelComponentKey.kt
copy to core/java/android/credentials/ui/UiResult.java
index 22a74d2..692584d 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/VolumePanelComponentKey.kt
+++ b/core/java/android/credentials/ui/UiResult.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2023 The Android Open Source Project
+ * Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,7 +14,11 @@
* limitations under the License.
*/
-package com.android.systemui.volume.panel
+package android.credentials.ui;
-/** Uniquely identifies the [com.android.systemui.volume.panel.ui.VolumePanelComponent]. */
-typealias VolumePanelComponentKey = String
+/**
+ * Base class for different types of ui results.
+ *
+ * @hide
+ */
+public interface UiResult {}
diff --git a/core/java/android/credentials/ui/UserSelectionResult.java b/core/java/android/credentials/ui/UserSelectionResult.java
new file mode 100644
index 0000000..431dc63
--- /dev/null
+++ b/core/java/android/credentials/ui/UserSelectionResult.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright 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 android.credentials.ui;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+
+import com.android.internal.util.Preconditions;
+
+/**
+ * Result sent back from the UI after the user chose an option and completed the following
+ * transaction launched through the provider PendingIntent associated with that option.
+ *
+ * @hide
+ */
+public final class UserSelectionResult implements UiResult {
+ @NonNull
+ private final String mProviderId;
+ @NonNull
+ private final String mEntryKey;
+ @NonNull
+ private final String mEntrySubkey;
+ @Nullable
+ private ProviderPendingIntentResponse mProviderPendingIntentResponse;
+
+ /**
+ * Constructs a {@link UserSelectionResult}.
+ *
+ * @throws IllegalArgumentException if {@code providerId} is empty
+ */
+
+ public UserSelectionResult(@NonNull String providerId,
+ @NonNull String entryKey, @NonNull String entrySubkey,
+ @Nullable ProviderPendingIntentResponse providerPendingIntentResponse) {
+ mProviderId = Preconditions.checkStringNotEmpty(providerId);
+ mEntryKey = Preconditions.checkNotNull(entryKey);
+ mEntrySubkey = Preconditions.checkNotNull(entrySubkey);
+ mProviderPendingIntentResponse = providerPendingIntentResponse;
+ }
+
+ /** Returns provider package name whose entry was selected by the user. */
+ @NonNull
+ public String getProviderId() {
+ return mProviderId;
+ }
+
+ /** Returns the key of the visual entry that the user selected. */
+ @NonNull
+ public String getEntryKey() {
+ return mEntryKey;
+ }
+
+ /** Returns the subkey of the visual entry that the user selected. */
+ @NonNull
+ public String getEntrySubkey() {
+ return mEntrySubkey;
+ }
+
+ /** Returns the pending intent response from the provider. */
+ @Nullable
+ public ProviderPendingIntentResponse getPendingIntentProviderResponse() {
+ return mProviderPendingIntentResponse;
+ }
+
+ @NonNull
+ UserSelectionDialogResult toUserSelectionDialogResult() {
+ return new UserSelectionDialogResult(/*requestToken=*/null, mProviderId, mEntryKey,
+ mEntrySubkey, mProviderPendingIntentResponse);
+ }
+}
diff --git a/core/java/android/database/sqlite/SQLiteDatabase.java b/core/java/android/database/sqlite/SQLiteDatabase.java
index 8a4f678..35ae3c9 100644
--- a/core/java/android/database/sqlite/SQLiteDatabase.java
+++ b/core/java/android/database/sqlite/SQLiteDatabase.java
@@ -40,12 +40,15 @@
import android.os.OperationCanceledException;
import android.os.SystemProperties;
import android.text.TextUtils;
+import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.EventLog;
import android.util.Log;
import android.util.Pair;
import android.util.Printer;
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.Preconditions;
import dalvik.annotation.optimization.NeverCompile;
@@ -103,8 +106,14 @@
// Stores reference to all databases opened in the current process.
// (The referent Object is not used at this time.)
// INVARIANT: Guarded by sActiveDatabases.
+ @GuardedBy("sActiveDatabases")
private static WeakHashMap<SQLiteDatabase, Object> sActiveDatabases = new WeakHashMap<>();
+ // Tracks which database files are currently open. If a database file is opened more than
+ // once at any given moment, the associated databases are marked as "concurrent".
+ @GuardedBy("sActiveDatabases")
+ private static final OpenTracker sOpenTracker = new OpenTracker();
+
// Thread-local for database sessions that belong to this database.
// Each thread has its own database session.
// INVARIANT: Immutable.
@@ -510,6 +519,7 @@
private void dispose(boolean finalized) {
final SQLiteConnectionPool pool;
+ final String path;
synchronized (mLock) {
if (mCloseGuardLocked != null) {
if (finalized) {
@@ -520,10 +530,12 @@
pool = mConnectionPoolLocked;
mConnectionPoolLocked = null;
+ path = isInMemoryDatabase() ? null : getPath();
}
if (!finalized) {
synchronized (sActiveDatabases) {
+ sOpenTracker.close(path);
sActiveDatabases.remove(this);
}
@@ -1132,6 +1144,74 @@
}
}
+ /**
+ * Track the number of times a database file has been opened. There is a primary connection
+ * associated with every open database, and these can contend with each other, leading to
+ * unexpected SQLiteDatabaseLockedException exceptions. The tracking here is only advisory:
+ * multiply-opened databases are logged but no other action is taken.
+ *
+ * This class is not thread-safe.
+ */
+ private static class OpenTracker {
+ // The list of currently-open databases. This maps the database file to the number of
+ // currently-active opens.
+ private final ArrayMap<String, Integer> mOpens = new ArrayMap<>();
+
+ // The maximum number of concurrently open database paths that will be stored. Once this
+ // many paths have been recorded, further paths are logged but not saved.
+ private static final int MAX_RECORDED_PATHS = 20;
+
+ // The list of databases that were ever concurrently opened.
+ private final ArraySet<String> mConcurrent = new ArraySet<>();
+
+ /** Return the canonical path. On error, just return the input path. */
+ private static String normalize(String path) {
+ try {
+ return new File(path).toPath().toRealPath().toString();
+ } catch (Exception e) {
+ // If there is an IO or security exception, just continue, using the input path.
+ return path;
+ }
+ }
+
+ /** Return true if the path is currently open in another SQLiteDatabase instance. */
+ void open(@Nullable String path) {
+ if (path == null) return;
+ path = normalize(path);
+
+ Integer count = mOpens.get(path);
+ if (count == null || count == 0) {
+ mOpens.put(path, 1);
+ return;
+ } else {
+ mOpens.put(path, count + 1);
+ if (mConcurrent.size() < MAX_RECORDED_PATHS) {
+ mConcurrent.add(path);
+ }
+ Log.w(TAG, "multiple primary connections on " + path);
+ return;
+ }
+ }
+
+ void close(@Nullable String path) {
+ if (path == null) return;
+ path = normalize(path);
+ Integer count = mOpens.get(path);
+ if (count == null || count <= 0) {
+ Log.e(TAG, "open database counting failure on " + path);
+ } else if (count == 1) {
+ // Implicitly set the count to zero, and make mOpens smaller.
+ mOpens.remove(path);
+ } else {
+ mOpens.put(path, count - 1);
+ }
+ }
+
+ ArraySet<String> getConcurrentDatabasePaths() {
+ return new ArraySet<>(mConcurrent);
+ }
+ }
+
private void open() {
try {
try {
@@ -1153,14 +1233,17 @@
}
private void openInner() {
+ final String path;
synchronized (mLock) {
assert mConnectionPoolLocked == null;
mConnectionPoolLocked = SQLiteConnectionPool.open(mConfigurationLocked);
mCloseGuardLocked.open("close");
+ path = isInMemoryDatabase() ? null : getPath();
}
synchronized (sActiveDatabases) {
sActiveDatabases.put(this, null);
+ sOpenTracker.open(path);
}
}
@@ -2345,6 +2428,17 @@
}
/**
+ * Return list of databases that have been concurrently opened.
+ * @hide
+ */
+ @VisibleForTesting
+ public static ArraySet<String> getConcurrentDatabasePaths() {
+ synchronized (sActiveDatabases) {
+ return sOpenTracker.getConcurrentDatabasePaths();
+ }
+ }
+
+ /**
* Returns true if the new version code is greater than the current database version.
*
* @param newVersion The new version code.
@@ -2766,6 +2860,19 @@
dumpDatabaseDirectory(printer, new File(dir), isSystem);
}
}
+
+ // Dump concurrently-opened database files, if any
+ final ArraySet<String> concurrent;
+ synchronized (sActiveDatabases) {
+ concurrent = sOpenTracker.getConcurrentDatabasePaths();
+ }
+ if (concurrent.size() > 0) {
+ printer.println("");
+ printer.println("Concurrently opened database files");
+ for (String f : concurrent) {
+ printer.println(" " + f);
+ }
+ }
}
private static void dumpDatabaseDirectory(Printer pw, File dir, boolean isSystem) {
diff --git a/core/java/android/hardware/HardwareBuffer.java b/core/java/android/hardware/HardwareBuffer.java
index f5b3a7b..ce0f9f59 100644
--- a/core/java/android/hardware/HardwareBuffer.java
+++ b/core/java/android/hardware/HardwareBuffer.java
@@ -67,8 +67,8 @@
S_UI8,
YCBCR_P010,
R_8,
- R_16_UINT,
- RG_1616_UINT,
+ R_16,
+ RG_1616,
RGBA_10101010,
})
public @interface Format {
@@ -115,17 +115,19 @@
@FlaggedApi(com.android.graphics.hwui.flags.Flags.FLAG_REQUESTED_FORMATS_V)
public static final int R_8 = 0x38;
/**
- * Format: 16 bits red. Bits should be represented in unsigned integer, instead of the
- * implicit unsigned normalized.
+ * Format: 16 bits red. When sampled on the GPU this is represented as an
+ * unsigned integer instead of implicit unsigned normalize.
+ * For more information see https://www.khronos.org/opengl/wiki/Normalized_Integer
*/
@FlaggedApi(com.android.graphics.hwui.flags.Flags.FLAG_REQUESTED_FORMATS_V)
- public static final int R_16_UINT = 0x39;
+ public static final int R_16 = 0x39;
/**
- * Format: 16 bits each red, green. Bits should be represented in unsigned integer,
- * instead of the implicit unsigned normalized.
+ * Format: 16 bits each red, green. When sampled on the GPU this is represented
+ * as an unsigned integer instead of implicit unsigned normalize.
+ * For more information see https://www.khronos.org/opengl/wiki/Normalized_Integer
*/
@FlaggedApi(com.android.graphics.hwui.flags.Flags.FLAG_REQUESTED_FORMATS_V)
- public static final int RG_1616_UINT = 0x3a;
+ public static final int RG_1616 = 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;
diff --git a/core/java/android/hardware/biometrics/AuthenticationStateListener.aidl b/core/java/android/hardware/biometrics/AuthenticationStateListener.aidl
index 73ac333..d51e62e 100644
--- a/core/java/android/hardware/biometrics/AuthenticationStateListener.aidl
+++ b/core/java/android/hardware/biometrics/AuthenticationStateListener.aidl
@@ -33,4 +33,20 @@
* Defines behavior in response to authentication stopping
*/
void onAuthenticationStopped();
+
+ /**
+ * Defines behavior in response to a successful authentication
+ * @param requestReason Reason from [BiometricRequestConstants.RequestReason] for the requested
+ * authentication
+ * @param userId The user Id for the requested authentication
+ */
+ void onAuthenticationSucceeded(int requestReason, int userId);
+
+ /**
+ * Defines behavior in response to a failed authentication
+ * @param requestReason Reason from [BiometricRequestConstants.RequestReason] for the requested
+ * authentication
+ * @param userId The user Id for the requested authentication
+ */
+ void onAuthenticationFailed(int requestReason, int userId);
}
diff --git a/core/java/android/hardware/biometrics/PromptContentItemBulletedText.java b/core/java/android/hardware/biometrics/PromptContentItemBulletedText.java
index c5e5a80..25e5cca 100644
--- a/core/java/android/hardware/biometrics/PromptContentItemBulletedText.java
+++ b/core/java/android/hardware/biometrics/PromptContentItemBulletedText.java
@@ -28,14 +28,14 @@
*/
@FlaggedApi(FLAG_CUSTOM_BIOMETRIC_PROMPT)
public final class PromptContentItemBulletedText implements PromptContentItemParcelable {
- private final CharSequence mText;
+ private final String mText;
/**
* A list item with bulleted text shown on {@link PromptVerticalListContentView}.
*
* @param text The text of this list item.
*/
- public PromptContentItemBulletedText(@NonNull CharSequence text) {
+ public PromptContentItemBulletedText(@NonNull String text) {
mText = text;
}
@@ -43,7 +43,7 @@
* @hide
*/
@NonNull
- public CharSequence getText() {
+ public String getText() {
return mText;
}
@@ -60,7 +60,7 @@
*/
@Override
public void writeToParcel(@NonNull Parcel dest, int flags) {
- dest.writeCharSequence(mText);
+ dest.writeString(mText);
}
/**
@@ -70,7 +70,7 @@
public static final Creator<PromptContentItemBulletedText> CREATOR = new Creator<>() {
@Override
public PromptContentItemBulletedText createFromParcel(Parcel in) {
- return new PromptContentItemBulletedText(in.readCharSequence());
+ return new PromptContentItemBulletedText(in.readString());
}
@Override
diff --git a/core/java/android/hardware/biometrics/PromptContentItemPlainText.java b/core/java/android/hardware/biometrics/PromptContentItemPlainText.java
index 6434c59..7919256 100644
--- a/core/java/android/hardware/biometrics/PromptContentItemPlainText.java
+++ b/core/java/android/hardware/biometrics/PromptContentItemPlainText.java
@@ -28,14 +28,14 @@
*/
@FlaggedApi(FLAG_CUSTOM_BIOMETRIC_PROMPT)
public final class PromptContentItemPlainText implements PromptContentItemParcelable {
- private final CharSequence mText;
+ private final String mText;
/**
* A list item with plain text shown on {@link PromptVerticalListContentView}.
*
* @param text The text of this list item.
*/
- public PromptContentItemPlainText(@NonNull CharSequence text) {
+ public PromptContentItemPlainText(@NonNull String text) {
mText = text;
}
@@ -43,7 +43,7 @@
* @hide
*/
@NonNull
- public CharSequence getText() {
+ public String getText() {
return mText;
}
@@ -60,7 +60,7 @@
*/
@Override
public void writeToParcel(@NonNull Parcel dest, int flags) {
- dest.writeCharSequence(mText);
+ dest.writeString(mText);
}
/**
@@ -70,7 +70,7 @@
public static final Creator<PromptContentItemPlainText> CREATOR = new Creator<>() {
@Override
public PromptContentItemPlainText createFromParcel(Parcel in) {
- return new PromptContentItemPlainText(in.readCharSequence());
+ return new PromptContentItemPlainText(in.readString());
}
@Override
diff --git a/core/java/android/hardware/biometrics/PromptVerticalListContentView.java b/core/java/android/hardware/biometrics/PromptVerticalListContentView.java
index f3e6290..38d32dc 100644
--- a/core/java/android/hardware/biometrics/PromptVerticalListContentView.java
+++ b/core/java/android/hardware/biometrics/PromptVerticalListContentView.java
@@ -52,11 +52,11 @@
private static final int MAX_ITEM_NUMBER = 20;
private static final int MAX_EACH_ITEM_CHARACTER_NUMBER = 640;
private final List<PromptContentItemParcelable> mContentList;
- private final CharSequence mDescription;
+ private final String mDescription;
private PromptVerticalListContentView(
@NonNull List<PromptContentItemParcelable> contentList,
- @NonNull CharSequence description) {
+ @NonNull String description) {
mContentList = contentList;
mDescription = description;
}
@@ -65,7 +65,7 @@
mContentList = in.readArrayList(
PromptContentItemParcelable.class.getClassLoader(),
PromptContentItemParcelable.class);
- mDescription = in.readCharSequence();
+ mDescription = in.readString();
}
/**
@@ -84,12 +84,12 @@
/**
* Gets the description for the content view, as set by
- * {@link PromptVerticalListContentView.Builder#setDescription(CharSequence)}.
+ * {@link PromptVerticalListContentView.Builder#setDescription(String)}.
*
* @return The description for the content view, or null if the content view has no description.
*/
@Nullable
- public CharSequence getDescription() {
+ public String getDescription() {
return mDescription;
}
@@ -118,7 +118,7 @@
@Override
public void writeToParcel(@androidx.annotation.NonNull Parcel dest, int flags) {
dest.writeList(mContentList);
- dest.writeCharSequence(mDescription);
+ dest.writeString(mDescription);
}
/**
@@ -143,7 +143,7 @@
*/
public static final class Builder {
private final List<PromptContentItemParcelable> mContentList = new ArrayList<>();
- private CharSequence mDescription;
+ private String mDescription;
/**
* Optional: Sets a description that will be shown on the content view.
@@ -152,7 +152,7 @@
* @return This builder.
*/
@NonNull
- public Builder setDescription(@NonNull CharSequence description) {
+ public Builder setDescription(@NonNull String description) {
mDescription = description;
return this;
}
diff --git a/core/java/android/hardware/face/IFaceService.aidl b/core/java/android/hardware/face/IFaceService.aidl
index e267e6b..8e234fa 100644
--- a/core/java/android/hardware/face/IFaceService.aidl
+++ b/core/java/android/hardware/face/IFaceService.aidl
@@ -15,6 +15,7 @@
*/
package android.hardware.face;
+import android.hardware.biometrics.AuthenticationStateListener;
import android.hardware.biometrics.IBiometricSensorReceiver;
import android.hardware.biometrics.IBiometricServiceLockoutResetCallback;
import android.hardware.biometrics.IBiometricStateListener;
@@ -181,6 +182,14 @@
// authenticators. The callback is automatically removed after it's invoked.
void addAuthenticatorsRegisteredCallback(IFaceAuthenticatorsRegisteredCallback callback);
+ // Registers AuthenticationStateListener.
+ @EnforcePermission("USE_BIOMETRIC_INTERNAL")
+ void registerAuthenticationStateListener(AuthenticationStateListener listener);
+
+ // Unregisters AuthenticationStateListener.
+ @EnforcePermission("USE_BIOMETRIC_INTERNAL")
+ void unregisterAuthenticationStateListener(AuthenticationStateListener listener);
+
// Registers BiometricStateListener.
void registerBiometricStateListener(IBiometricStateListener listener);
diff --git a/core/java/android/hardware/input/InputSettings.java b/core/java/android/hardware/input/InputSettings.java
index d939532..fdbd319 100644
--- a/core/java/android/hardware/input/InputSettings.java
+++ b/core/java/android/hardware/input/InputSettings.java
@@ -17,6 +17,7 @@
package android.hardware.input;
import static com.android.hardware.input.Flags.keyboardA11yBounceKeysFlag;
+import static com.android.hardware.input.Flags.keyboardA11ySlowKeysFlag;
import static com.android.hardware.input.Flags.keyboardA11yStickyKeysFlag;
import static com.android.input.flags.Flags.enableInputFilterRustImpl;
@@ -68,6 +69,12 @@
*/
public static final int MAX_ACCESSIBILITY_BOUNCE_KEYS_THRESHOLD_MILLIS = 5000;
+ /**
+ * The maximum allowed Accessibility slow keys threshold.
+ * @hide
+ */
+ public static final int MAX_ACCESSIBILITY_SLOW_KEYS_THRESHOLD_MILLIS = 5000;
+
private InputSettings() {
}
@@ -419,6 +426,86 @@
}
/**
+ * Whether Accessibility slow keys feature flags is enabled.
+ *
+ * <p>
+ * 'Slow keys' is an accessibility feature to aid users who have physical disabilities, that
+ * allows the user to specify the duration for which one must press-and-hold a key before the
+ * system accepts the keypress.
+ * </p>
+ *
+ * @hide
+ */
+ public static boolean isAccessibilitySlowKeysFeatureFlagEnabled() {
+ return keyboardA11ySlowKeysFlag() && enableInputFilterRustImpl();
+ }
+
+ /**
+ * Whether Accessibility slow keys is enabled.
+ *
+ * <p>
+ * 'Slow keys' is an accessibility feature to aid users who have physical disabilities, that
+ * allows the user to specify the duration for which one must press-and-hold a key before the
+ * system accepts the keypress.
+ * </p>
+ *
+ * @hide
+ */
+ public static boolean isAccessibilitySlowKeysEnabled(@NonNull Context context) {
+ return getAccessibilitySlowKeysThreshold(context) != 0;
+ }
+
+ /**
+ * Get Accessibility slow keys threshold duration in milliseconds.
+ *
+ * <p>
+ * 'Slow keys' is an accessibility feature to aid users who have physical disabilities, that
+ * allows the user to specify the duration for which one must press-and-hold a key before the
+ * system accepts the keypress.
+ * </p>
+ *
+ * @hide
+ */
+ public static int getAccessibilitySlowKeysThreshold(@NonNull Context context) {
+ if (!isAccessibilitySlowKeysFeatureFlagEnabled()) {
+ return 0;
+ }
+ return Settings.Secure.getIntForUser(context.getContentResolver(),
+ Settings.Secure.ACCESSIBILITY_SLOW_KEYS, 0, UserHandle.USER_CURRENT);
+ }
+
+ /**
+ * Set Accessibility slow keys threshold duration in milliseconds.
+ * @param thresholdTimeMillis time duration for which a key should be pressed to be registered
+ * in the system. The threshold must be between 0 and
+ * {@link MAX_ACCESSIBILITY_SLOW_KEYS_THRESHOLD_MILLIS}
+ *
+ * <p>
+ * 'Slow keys' is an accessibility feature to aid users who have physical disabilities, that
+ * allows the user to specify the duration for which one must press-and-hold a key before the
+ * system accepts the keypress.
+ * </p>
+ *
+ * @hide
+ */
+ @RequiresPermission(Manifest.permission.WRITE_SETTINGS)
+ public static void setAccessibilitySlowKeysThreshold(@NonNull Context context,
+ int thresholdTimeMillis) {
+ if (!isAccessibilitySlowKeysFeatureFlagEnabled()) {
+ return;
+ }
+ if (thresholdTimeMillis < 0
+ || thresholdTimeMillis > MAX_ACCESSIBILITY_SLOW_KEYS_THRESHOLD_MILLIS) {
+ throw new IllegalArgumentException(
+ "Provided Slow keys threshold should be in range [0, "
+ + MAX_ACCESSIBILITY_SLOW_KEYS_THRESHOLD_MILLIS + "]");
+ }
+ Settings.Secure.putIntForUser(context.getContentResolver(),
+ Settings.Secure.ACCESSIBILITY_SLOW_KEYS, thresholdTimeMillis,
+ UserHandle.USER_CURRENT);
+ }
+
+ /**
* Whether Accessibility sticky keys feature is enabled.
*
* <p>
diff --git a/core/java/android/hardware/input/input_framework.aconfig b/core/java/android/hardware/input/input_framework.aconfig
index 362fe78..0ed6569 100644
--- a/core/java/android/hardware/input/input_framework.aconfig
+++ b/core/java/android/hardware/input/input_framework.aconfig
@@ -29,4 +29,11 @@
name: "pointer_coords_is_resampled_api"
description: "Makes MotionEvent.PointerCoords#isResampled() a public API"
bug: "298197511"
+}
+
+flag {
+ namespace: "input_native"
+ name: "keyboard_a11y_slow_keys_flag"
+ description: "Controls if the slow keys accessibility feature for physical keyboard is available to the user"
+ bug: "294546335"
}
\ No newline at end of file
diff --git a/core/java/android/metrics/LogMaker.java b/core/java/android/metrics/LogMaker.java
index 8644d91..f65b713 100644
--- a/core/java/android/metrics/LogMaker.java
+++ b/core/java/android/metrics/LogMaker.java
@@ -32,6 +32,7 @@
* @hide
*/
@SystemApi
+@android.ravenwood.annotation.RavenwoodKeepWholeClass
public class LogMaker {
private static final String TAG = "LogBuilder";
diff --git a/core/java/android/net/NetworkPolicyManager.java b/core/java/android/net/NetworkPolicyManager.java
index 365f913..594ec18 100644
--- a/core/java/android/net/NetworkPolicyManager.java
+++ b/core/java/android/net/NetworkPolicyManager.java
@@ -16,6 +16,7 @@
package android.net;
+import static android.app.ActivityManager.PROCESS_CAPABILITY_POWER_RESTRICTED_NETWORK;
import static android.app.ActivityManager.PROCESS_STATE_UNKNOWN;
import static android.app.ActivityManager.procStateToString;
import static android.content.pm.PackageManager.GET_SIGNATURES;
@@ -170,6 +171,8 @@
public static final String FIREWALL_CHAIN_NAME_RESTRICTED = "restricted";
/** @hide */
public static final String FIREWALL_CHAIN_NAME_LOW_POWER_STANDBY = "low_power_standby";
+ /** @hide */
+ public static final String FIREWALL_CHAIN_NAME_BACKGROUND = "background";
private static final boolean ALLOW_PLATFORM_APP_POLICY = true;
@@ -180,6 +183,9 @@
/** @hide */
public static final int TOP_THRESHOLD_STATE = ActivityManager.PROCESS_STATE_BOUND_TOP;
+ /** @hide */
+ public static final int BACKGROUND_THRESHOLD_STATE = ActivityManager.PROCESS_STATE_TOP_SLEEPING;
+
/**
* {@link Intent} extra that indicates which {@link NetworkTemplate} rule it
* applies to.
@@ -264,6 +270,16 @@
* @hide
*/
public static final int ALLOWED_REASON_LOW_POWER_STANDBY_ALLOWLIST = 1 << 6;
+
+ /**
+ * Flag to indicate that the app is exempt from always-on background network restrictions.
+ * Note that this is explicitly different to the flag NOT_FOREGROUND which is used to grant
+ * shared exception to apps from power restrictions like doze, battery saver and app-standby.
+ *
+ * @hide
+ */
+ public static final int ALLOWED_REASON_NOT_IN_BACKGROUND = 1 << 7;
+
/**
* Flag to indicate that app is exempt from certain metered network restrictions because user
* explicitly exempted it.
@@ -822,6 +838,21 @@
}
/**
+ * This is currently only used as an implementation detail for
+ * {@link com.android.server.net.NetworkPolicyManagerService}.
+ * Only put here to be together with other isProcStateAllowed* methods.
+ *
+ * @hide
+ */
+ public static boolean isProcStateAllowedNetworkWhileBackground(@Nullable UidState uidState) {
+ if (uidState == null) {
+ return false;
+ }
+ return uidState.procState < BACKGROUND_THRESHOLD_STATE
+ || (uidState.capability & PROCESS_CAPABILITY_POWER_RESTRICTED_NETWORK) != 0;
+ }
+
+ /**
* Returns true if {@param procState} is considered foreground and as such will be allowed
* to access network when the device is in data saver mode. Otherwise, false.
* @hide
diff --git a/core/java/android/net/thread/OWNERS b/core/java/android/net/thread/OWNERS
new file mode 100644
index 0000000..55c307b
--- /dev/null
+++ b/core/java/android/net/thread/OWNERS
@@ -0,0 +1,3 @@
+# Bug component: 1203089
+
+include platform/packages/modules/ThreadNetwork:/OWNERS
diff --git a/core/java/android/net/thread/flags.aconfig b/core/java/android/net/thread/flags.aconfig
new file mode 100644
index 0000000..6e72f8e
--- /dev/null
+++ b/core/java/android/net/thread/flags.aconfig
@@ -0,0 +1,8 @@
+package: "com.android.net.thread.flags"
+
+flag {
+ name: "thread_user_restriction_enabled"
+ namespace: "thread_network"
+ description: "Controls whether user restriction on thread networks is enabled"
+ bug: "307679182"
+}
diff --git a/core/java/android/os/Build.java b/core/java/android/os/Build.java
index 5871717..3977bdf 100755
--- a/core/java/android/os/Build.java
+++ b/core/java/android/os/Build.java
@@ -28,6 +28,7 @@
import android.app.Application;
import android.compat.annotation.UnsupportedAppUsage;
import android.content.Context;
+import android.ravenwood.annotation.RavenwoodKeepWholeClass;
import android.sysprop.DeviceProperties;
import android.sysprop.SocProperties;
import android.sysprop.TelephonyProperties;
@@ -47,6 +48,7 @@
/**
* Information about the current build, extracted from system properties.
*/
+@RavenwoodKeepWholeClass
public class Build {
private static final String TAG = "Build";
@@ -307,7 +309,7 @@
* compatibility.
*/
final String[] abiList;
- if (VMRuntime.getRuntime().is64Bit()) {
+ if (android.os.Process.is64Bit()) {
abiList = SUPPORTED_64_BIT_ABIS;
} else {
abiList = SUPPORTED_32_BIT_ABIS;
diff --git a/core/java/android/os/PersistableBundle.java b/core/java/android/os/PersistableBundle.java
index 02704f5..236194d 100644
--- a/core/java/android/os/PersistableBundle.java
+++ b/core/java/android/os/PersistableBundle.java
@@ -294,6 +294,43 @@
XmlUtils.writeMapXml(mMap, out, this);
}
+ /**
+ * Checks whether all keys and values are within the given character limit.
+ * Note: Maximum character limit of String that can be saved to XML as part of bundle is 65535.
+ * Otherwise IOException is thrown.
+ * @param limit length of String keys and values in the PersistableBundle, including nested
+ * PersistableBundles to check against.
+ *
+ * @hide
+ */
+ public boolean isBundleContentsWithinLengthLimit(int limit) {
+ unparcel();
+ if (mMap == null) {
+ return true;
+ }
+ for (int i = 0; i < mMap.size(); i++) {
+ if (mMap.keyAt(i) != null && mMap.keyAt(i).length() > limit) {
+ return false;
+ }
+ final Object value = mMap.valueAt(i);
+ if (value instanceof String && ((String) value).length() > limit) {
+ return false;
+ } else if (value instanceof String[]) {
+ String[] stringArray = (String[]) value;
+ for (int j = 0; j < stringArray.length; j++) {
+ if (stringArray[j] != null
+ && stringArray[j].length() > limit) {
+ return false;
+ }
+ }
+ } else if (value instanceof PersistableBundle
+ && !((PersistableBundle) value).isBundleContentsWithinLengthLimit(limit)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
/** @hide */
static class MyReadMapCallback implements XmlUtils.ReadMapCallback {
@Override
diff --git a/core/java/android/os/Process.java b/core/java/android/os/Process.java
index dd0436c..1f3a162 100644
--- a/core/java/android/os/Process.java
+++ b/core/java/android/os/Process.java
@@ -1589,7 +1589,15 @@
@UnsupportedAppUsage
public static final native long getPss(int pid);
- /** @hide */
+ /**
+ * Gets the total Rss value for a given process, in bytes.
+ *
+ * @param pid the process to the Rss for
+ * @return an ordered array containing multiple values, they are:
+ * [total_rss, file, anon, swap, shmem].
+ * or NULL if the value cannot be determined
+ * @hide
+ */
public static final native long[] getRss(int pid);
/**
diff --git a/core/java/android/os/SystemProperties.java b/core/java/android/os/SystemProperties.java
index aa283a2..a818919 100644
--- a/core/java/android/os/SystemProperties.java
+++ b/core/java/android/os/SystemProperties.java
@@ -20,6 +20,8 @@
import android.annotation.Nullable;
import android.annotation.SystemApi;
import android.compat.annotation.UnsupportedAppUsage;
+import android.ravenwood.annotation.RavenwoodKeepWholeClass;
+import android.ravenwood.annotation.RavenwoodNativeSubstitutionClass;
import android.util.Log;
import android.util.MutableInt;
@@ -36,6 +38,8 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
+import java.util.Map;
+import java.util.function.Predicate;
/**
* Gives access to the system properties store. The system properties
@@ -51,6 +55,8 @@
* {@hide}
*/
@SystemApi
+@RavenwoodKeepWholeClass
+@RavenwoodNativeSubstitutionClass("com.android.hoststubgen.nativesubstitution.SystemProperties_host")
public class SystemProperties {
private static final String TAG = "SystemProperties";
private static final boolean TRACK_KEY_ACCESS = false;
@@ -94,6 +100,31 @@
}
}
+ /** @hide */
+ public static void init$ravenwood(Map<String, String> values,
+ Predicate<String> keyReadablePredicate, Predicate<String> keyWritablePredicate) {
+ native_init$ravenwood(values, keyReadablePredicate, keyWritablePredicate,
+ SystemProperties::callChangeCallbacks);
+ synchronized (sChangeCallbacks) {
+ sChangeCallbacks.clear();
+ }
+ }
+
+ /** @hide */
+ public static void reset$ravenwood() {
+ native_reset$ravenwood();
+ synchronized (sChangeCallbacks) {
+ sChangeCallbacks.clear();
+ }
+ }
+
+ // These native methods are currently only implemented by Ravenwood, as it's the only
+ // mechanism we have to jump to our RavenwoodNativeSubstitutionClass
+ private static native void native_init$ravenwood(Map<String, String> values,
+ Predicate<String> keyReadablePredicate, Predicate<String> keyWritablePredicate,
+ Runnable changeCallback);
+ private static native void native_reset$ravenwood();
+
// The one-argument version of native_get used to be a regular native function. Nowadays,
// we use the two-argument form of native_get all the time, but we can't just delete the
// one-argument overload: apps use it via reflection, as the UnsupportedAppUsage annotation
diff --git a/core/java/android/os/Trace.java b/core/java/android/os/Trace.java
index 5d7e04d..c0b4909 100644
--- a/core/java/android/os/Trace.java
+++ b/core/java/android/os/Trace.java
@@ -36,6 +36,7 @@
* href="{@docRoot}tools/debugging/systrace.html">Analyzing Display and Performance
* with Systrace</a>.
*/
+@android.ravenwood.annotation.RavenwoodKeepWholeClass
public final class Trace {
/*
* Writes trace events to the kernel trace buffer. These trace events can be
@@ -123,10 +124,26 @@
@UnsupportedAppUsage
@CriticalNative
+ @android.ravenwood.annotation.RavenwoodReplace
private static native long nativeGetEnabledTags();
+ @android.ravenwood.annotation.RavenwoodReplace
private static native void nativeSetAppTracingAllowed(boolean allowed);
+ @android.ravenwood.annotation.RavenwoodReplace
private static native void nativeSetTracingEnabled(boolean allowed);
+ private static long nativeGetEnabledTags$ravenwood() {
+ // Tracing currently completely disabled under Ravenwood
+ return 0;
+ }
+
+ private static void nativeSetAppTracingAllowed$ravenwood(boolean allowed) {
+ // Tracing currently completely disabled under Ravenwood
+ }
+
+ private static void nativeSetTracingEnabled$ravenwood(boolean allowed) {
+ // Tracing currently completely disabled under Ravenwood
+ }
+
@FastNative
private static native void nativeTraceCounter(long tag, String name, long value);
@FastNative
diff --git a/core/java/android/os/UserManager.java b/core/java/android/os/UserManager.java
index c280d13..533946d 100644
--- a/core/java/android/os/UserManager.java
+++ b/core/java/android/os/UserManager.java
@@ -88,6 +88,7 @@
* See {@link DevicePolicyManager#ACTION_PROVISION_MANAGED_PROFILE} for more on managed profiles.
*/
@SystemService(Context.USER_SERVICE)
+@android.ravenwood.annotation.RavenwoodKeepPartialClass
public class UserManager {
private static final String TAG = "UserManager";
@@ -106,6 +107,21 @@
/** Whether the device is in headless system user mode; null until cached. */
private static Boolean sIsHeadlessSystemUser = null;
+ /** Maximum length of username.
+ * @hide
+ */
+ public static final int MAX_USER_NAME_LENGTH = 100;
+
+ /** Maximum length of user property String value.
+ * @hide
+ */
+ public static final int MAX_ACCOUNT_STRING_LENGTH = 500;
+
+ /** Maximum length of account options String values.
+ * @hide
+ */
+ public static final int MAX_ACCOUNT_OPTIONS_LENGTH = 1000;
+
/**
* User type representing a {@link UserHandle#USER_SYSTEM system} user that is a human user.
* This type of user cannot be created; it can only pre-exist on first boot.
@@ -2906,6 +2922,7 @@
* {@link UserManager#USER_TYPE_PROFILE_MANAGED managed profile}.
* @hide
*/
+ @android.ravenwood.annotation.RavenwoodKeep
public static boolean isUserTypeManagedProfile(@Nullable String userType) {
return USER_TYPE_PROFILE_MANAGED.equals(userType);
}
@@ -2914,6 +2931,7 @@
* Returns whether the user type is a {@link UserManager#USER_TYPE_FULL_GUEST guest user}.
* @hide
*/
+ @android.ravenwood.annotation.RavenwoodKeep
public static boolean isUserTypeGuest(@Nullable String userType) {
return USER_TYPE_FULL_GUEST.equals(userType);
}
@@ -2923,6 +2941,7 @@
* {@link UserManager#USER_TYPE_FULL_RESTRICTED restricted user}.
* @hide
*/
+ @android.ravenwood.annotation.RavenwoodKeep
public static boolean isUserTypeRestricted(@Nullable String userType) {
return USER_TYPE_FULL_RESTRICTED.equals(userType);
}
@@ -2931,6 +2950,7 @@
* Returns whether the user type is a {@link UserManager#USER_TYPE_FULL_DEMO demo user}.
* @hide
*/
+ @android.ravenwood.annotation.RavenwoodKeep
public static boolean isUserTypeDemo(@Nullable String userType) {
return USER_TYPE_FULL_DEMO.equals(userType);
}
@@ -2939,6 +2959,7 @@
* Returns whether the user type is a {@link UserManager#USER_TYPE_PROFILE_CLONE clone user}.
* @hide
*/
+ @android.ravenwood.annotation.RavenwoodKeep
public static boolean isUserTypeCloneProfile(@Nullable String userType) {
return USER_TYPE_PROFILE_CLONE.equals(userType);
}
@@ -2948,6 +2969,7 @@
* {@link UserManager#USER_TYPE_PROFILE_COMMUNAL communal profile}.
* @hide
*/
+ @android.ravenwood.annotation.RavenwoodKeep
public static boolean isUserTypeCommunalProfile(@Nullable String userType) {
return USER_TYPE_PROFILE_COMMUNAL.equals(userType);
}
@@ -2958,6 +2980,7 @@
*
* @hide
*/
+ @android.ravenwood.annotation.RavenwoodKeep
public static boolean isUserTypePrivateProfile(@Nullable String userType) {
return USER_TYPE_PROFILE_PRIVATE.equals(userType);
}
@@ -4423,15 +4446,15 @@
* This API should only be called if the current user is an {@link #isAdminUser() admin} user,
* as otherwise the returned intent will not be able to create a user.
*
- * @param userName Optional name to assign to the user.
+ * @param userName Optional name to assign to the user. Character limit is 100.
* @param accountName Optional account name that will be used by the setup wizard to initialize
- * the user.
+ * the user. Character limit is 500.
* @param accountType Optional account type for the account to be created. This is required
- * if the account name is specified.
+ * if the account name is specified. Character limit is 500.
* @param accountOptions Optional bundle of data to be passed in during account creation in the
* new user via {@link AccountManager#addAccount(String, String, String[],
* Bundle, android.app.Activity, android.accounts.AccountManagerCallback,
- * Handler)}.
+ * Handler)}. Character limit is 1000.
* @return An Intent that can be launched from an Activity.
* @see #USER_CREATION_FAILED_NOT_PERMITTED
* @see #USER_CREATION_FAILED_NO_MORE_USERS
diff --git a/core/java/android/permission/flags.aconfig b/core/java/android/permission/flags.aconfig
index 39b6aeb..8c70501 100644
--- a/core/java/android/permission/flags.aconfig
+++ b/core/java/android/permission/flags.aconfig
@@ -86,3 +86,11 @@
description: "This flag is used to enabled the Wallet Role for all users on the device"
bug: "283989236"
}
+
+flag {
+ name: "signature_permission_allowlist_enabled"
+ is_fixed_read_only: true
+ namespace: "permissions"
+ description: "Enable signature permission allowlist"
+ bug: "308573169"
+}
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index ab98c94..e3a41ba 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -138,6 +138,20 @@
public static final String ACTION_SETTINGS = "android.settings.SETTINGS";
/**
+ * Activity Action: Show settings to provide guide about carrier satellite messaging.
+ * <p>
+ * In some cases, a matching Activity may not exist, so ensure you
+ * safeguard against this.
+ * <p>
+ * Input: Nothing.
+ * <p>
+ * Output: Nothing.
+ */
+ @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
+ @FlaggedApi(com.android.internal.telephony.flags.Flags.FLAG_CARRIER_ENABLED_SATELLITE_FLAG)
+ public static final String ACTION_SATELLITE_SETTING = "android.settings.SATELLITE_SETTING";
+
+ /**
* Activity Action: Show settings to allow configuration of APNs.
* <p>
* Input: Nothing.
@@ -3243,9 +3257,17 @@
private static final String NAME_EQ_PLACEHOLDER = "name=?";
+ // Cached values of queried settings.
+ // Key is the setting's name, value is the setting's value.
// Must synchronize on 'this' to access mValues and mValuesVersion.
private final ArrayMap<String, String> mValues = new ArrayMap<>();
+ // Cached values for queried prefixes.
+ // Key is the prefix, value is all of the settings under the prefix, mapped from a setting's
+ // name to a setting's value. The name string doesn't include the prefix.
+ // Must synchronize on 'this' to access.
+ private final ArrayMap<String, ArrayMap<String, String>> mPrefixToValues = new ArrayMap<>();
+
private final Uri mUri;
@UnsupportedAppUsage
private final ContentProviderHolder mProviderHolder;
@@ -3592,15 +3614,13 @@
|| applicationInfo.isSignedWithPlatformKey();
}
- private ArrayMap<String, String> getStringsForPrefixStripPrefix(
- ContentResolver cr, String prefix, String[] names) {
+ private Map<String, String> getStringsForPrefixStripPrefix(
+ ContentResolver cr, String prefix, List<String> names) {
String namespace = prefix.substring(0, prefix.length() - 1);
ArrayMap<String, String> keyValues = new ArrayMap<>();
int substringLength = prefix.length();
-
int currentGeneration = -1;
boolean needsGenerationTracker = false;
-
synchronized (NameValueCache.this) {
final GenerationTracker generationTracker = mGenerationTrackers.get(prefix);
if (generationTracker != null) {
@@ -3614,40 +3634,24 @@
// generation tracker and request a new one
generationTracker.destroy();
mGenerationTrackers.remove(prefix);
- for (int i = mValues.size() - 1; i >= 0; i--) {
- String key = mValues.keyAt(i);
- if (key.startsWith(prefix)) {
- mValues.remove(key);
- }
- }
+ mPrefixToValues.remove(prefix);
needsGenerationTracker = true;
} else {
- boolean prefixCached = mValues.containsKey(prefix);
- if (prefixCached) {
- if (DEBUG) {
- Log.i(TAG, "Cache hit for prefix:" + prefix);
- }
- if (names.length > 0) {
+ final ArrayMap<String, String> cachedSettings = mPrefixToValues.get(prefix);
+ if (cachedSettings != null) {
+ if (!names.isEmpty()) {
for (String name : names) {
- // mValues can contain "null" values, need to use containsKey.
- if (mValues.containsKey(name)) {
+ // The cache can contain "null" values, need to use containsKey.
+ if (cachedSettings.containsKey(name)) {
keyValues.put(
- name.substring(substringLength),
- mValues.get(name));
+ name,
+ cachedSettings.get(name));
}
}
} else {
- for (int i = 0; i < mValues.size(); ++i) {
- String key = mValues.keyAt(i);
- // Explicitly exclude the prefix as it is only there to
- // signal that the prefix has been cached.
- if (key.startsWith(prefix) && !key.equals(prefix)) {
- String value = mValues.valueAt(i);
- keyValues.put(
- key.substring(substringLength),
- value);
- }
- }
+ keyValues.putAll(cachedSettings);
+ // Remove the hack added for the legacy behavior.
+ keyValues.remove("");
}
return keyValues;
}
@@ -3657,7 +3661,6 @@
needsGenerationTracker = true;
}
}
-
if (mCallListCommand == null) {
// No list command specified, return empty map
return keyValues;
@@ -3702,20 +3705,23 @@
}
// All flags for the namespace
- Map<String, String> flagsToValues =
+ HashMap<String, String> flagsToValues =
(HashMap) b.getSerializable(Settings.NameValueTable.VALUE, java.util.HashMap.class);
+ if (flagsToValues == null) {
+ return keyValues;
+ }
// Only the flags requested by the caller
- if (names.length > 0) {
+ if (!names.isEmpty()) {
for (String name : names) {
// flagsToValues can contain "null" values, need to use containsKey.
- if (flagsToValues.containsKey(name)) {
+ final String key = Config.createCompositeName(namespace, name);
+ if (flagsToValues.containsKey(key)) {
keyValues.put(
- name.substring(substringLength),
- flagsToValues.get(name));
+ name,
+ flagsToValues.get(key));
}
}
} else {
- keyValues.ensureCapacity(keyValues.size() + flagsToValues.size());
for (Map.Entry<String, String> flag : flagsToValues.entrySet()) {
keyValues.put(
flag.getKey().substring(substringLength),
@@ -3751,10 +3757,18 @@
if (DEBUG) {
Log.i(TAG, "Updating cache for prefix:" + prefix);
}
- // cache the complete list of flags for the namespace
- mValues.putAll(flagsToValues);
- // Adding the prefix as a signal that the prefix is cached.
- mValues.put(prefix, null);
+ // Cache the complete list of flags for the namespace for bulk queries.
+ // In this cached list, the setting's name doesn't include the prefix.
+ ArrayMap<String, String> namesToValues =
+ new ArrayMap<>(flagsToValues.size() + 1);
+ for (Map.Entry<String, String> flag : flagsToValues.entrySet()) {
+ namesToValues.put(
+ flag.getKey().substring(substringLength),
+ flag.getValue());
+ }
+ // Legacy behavior, we return <"", null> when queried with name = ""
+ namesToValues.put("", null);
+ mPrefixToValues.put(prefix, namesToValues);
}
}
return keyValues;
@@ -4499,10 +4513,11 @@
/** @hide */
public static void adjustConfigurationForUser(ContentResolver cr, Configuration outConfig,
int userHandle, boolean updateSettingsIfEmpty) {
+ final float defaultFontScale = getDefaultFontScale(cr, userHandle);
outConfig.fontScale = Settings.System.getFloatForUser(
- cr, FONT_SCALE, DEFAULT_FONT_SCALE, userHandle);
+ cr, FONT_SCALE, defaultFontScale, userHandle);
if (outConfig.fontScale < 0) {
- outConfig.fontScale = DEFAULT_FONT_SCALE;
+ outConfig.fontScale = defaultFontScale;
}
outConfig.fontWeightAdjustment = Settings.Secure.getIntForUser(
cr, Settings.Secure.FONT_WEIGHT_ADJUSTMENT, DEFAULT_FONT_WEIGHT, userHandle);
@@ -4527,6 +4542,12 @@
}
}
+ private static float getDefaultFontScale(ContentResolver cr, int userHandle) {
+ return com.android.window.flags.Flags.configurableFontScaleDefault()
+ ? Settings.System.getFloatForUser(cr, DEFAULT_DEVICE_FONT_SCALE,
+ DEFAULT_FONT_SCALE, userHandle) : DEFAULT_FONT_SCALE;
+ }
+
/**
* @hide Erase the fields in the Configuration that should be applied
* by the settings.
@@ -4893,6 +4914,15 @@
public static final String FONT_SCALE = "font_scale";
/**
+ * Default scaling factor for fonts for the specific device, float.
+ * The value is read from the {@link R.dimen.def_device_font_scale}
+ * configuration property.
+ *
+ * @hide
+ */
+ public static final String DEFAULT_DEVICE_FONT_SCALE = "device_font_scale";
+
+ /**
* The serialized system locale value.
*
* Do not use this value directory.
@@ -6231,6 +6261,7 @@
PRIVATE_SETTINGS.add(CAMERA_FLASH_NOTIFICATION);
PRIVATE_SETTINGS.add(SCREEN_FLASH_NOTIFICATION);
PRIVATE_SETTINGS.add(SCREEN_FLASH_NOTIFICATION_COLOR);
+ PRIVATE_SETTINGS.add(DEFAULT_DEVICE_FONT_SCALE);
}
/**
@@ -7867,6 +7898,17 @@
public static final String ACCESSIBILITY_BOUNCE_KEYS = "accessibility_bounce_keys";
/**
+ * Whether to enable slow keys for Physical Keyboard accessibility.
+ *
+ * If set to non-zero value, any key press on physical keyboard needs to be pressed and
+ * held for the provided threshold duration (in milliseconds) to be registered in the
+ * system.
+ *
+ * @hide
+ */
+ public static final String ACCESSIBILITY_SLOW_KEYS = "accessibility_slow_keys";
+
+ /**
* Whether to enable sticky keys for Physical Keyboard accessibility.
*
* This is a boolean value that determines if Sticky keys feature is enabled.
@@ -12262,6 +12304,8 @@
CLONE_TO_MANAGED_PROFILE.add(LOCATION_MODE);
CLONE_TO_MANAGED_PROFILE.add(SHOW_IME_WITH_HARD_KEYBOARD);
CLONE_TO_MANAGED_PROFILE.add(ACCESSIBILITY_BOUNCE_KEYS);
+ CLONE_TO_MANAGED_PROFILE.add(ACCESSIBILITY_SLOW_KEYS);
+ CLONE_TO_MANAGED_PROFILE.add(ACCESSIBILITY_STICKY_KEYS);
CLONE_TO_MANAGED_PROFILE.add(NOTIFICATION_BUBBLES);
CLONE_TO_MANAGED_PROFILE.add(NOTIFICATION_HISTORY_ENABLED);
}
@@ -19945,16 +19989,9 @@
@RequiresPermission(Manifest.permission.READ_DEVICE_CONFIG)
public static Map<String, String> getStrings(@NonNull ContentResolver resolver,
@NonNull String namespace, @NonNull List<String> names) {
- String[] compositeNames = new String[names.size()];
- for (int i = 0, size = names.size(); i < size; ++i) {
- compositeNames[i] = createCompositeName(namespace, names.get(i));
- }
-
String prefix = createPrefix(namespace);
- ArrayMap<String, String> keyValues = sNameValueCache.getStringsForPrefixStripPrefix(
- resolver, prefix, compositeNames);
- return keyValues;
+ return sNameValueCache.getStringsForPrefixStripPrefix(resolver, prefix, names);
}
/**
@@ -20276,7 +20313,7 @@
}
}
- private static String createCompositeName(@NonNull String namespace, @NonNull String name) {
+ static String createCompositeName(@NonNull String namespace, @NonNull String name) {
Preconditions.checkNotNull(namespace);
Preconditions.checkNotNull(name);
var sb = new StringBuilder(namespace.length() + 1 + name.length());
diff --git a/core/java/android/security/flags.aconfig b/core/java/android/security/flags.aconfig
index 1994058..43163b3 100644
--- a/core/java/android/security/flags.aconfig
+++ b/core/java/android/security/flags.aconfig
@@ -58,3 +58,10 @@
bug: "290312729"
is_fixed_read_only: true
}
+
+flag {
+ name: "report_primary_auth_attempts"
+ namespace: "biometrics"
+ description: "Report primary auth attempts from LockSettingsService"
+ bug: "285053096"
+}
diff --git a/core/java/android/service/autofill/FillResponse.java b/core/java/android/service/autofill/FillResponse.java
index 7ea74d3..09ec933 100644
--- a/core/java/android/service/autofill/FillResponse.java
+++ b/core/java/android/service/autofill/FillResponse.java
@@ -28,6 +28,7 @@
import android.annotation.SuppressLint;
import android.annotation.TestApi;
import android.app.Activity;
+import android.app.PendingIntent;
import android.content.Intent;
import android.content.IntentSender;
import android.content.pm.ParceledListSlice;
@@ -116,6 +117,7 @@
private final boolean mShowFillDialogIcon;
private final boolean mShowSaveDialogIcon;
private final @Nullable FieldClassification[] mDetectedFieldTypes;
+ private final @Nullable PendingIntent mDialogPendingIntent;
/**
* Creates a shollow copy of the provided FillResponse.
@@ -150,7 +152,8 @@
r.mServiceDisplayNameResourceId,
r.mShowFillDialogIcon,
r.mShowSaveDialogIcon,
- r.mDetectedFieldTypes);
+ r.mDetectedFieldTypes,
+ r.mDialogPendingIntent);
}
private FillResponse(ParceledListSlice<Dataset> datasets, SaveInfo saveInfo, Bundle clientState,
@@ -163,7 +166,7 @@
int[] cancelIds, boolean supportsInlineSuggestions, int iconResourceId,
int serviceDisplayNameResourceId, boolean showFillDialogIcon,
boolean showSaveDialogIcon,
- FieldClassification[] detectedFieldTypes) {
+ FieldClassification[] detectedFieldTypes, PendingIntent dialogPendingIntent) {
mDatasets = datasets;
mSaveInfo = saveInfo;
mClientState = clientState;
@@ -190,6 +193,7 @@
mShowFillDialogIcon = showFillDialogIcon;
mShowSaveDialogIcon = showSaveDialogIcon;
mDetectedFieldTypes = detectedFieldTypes;
+ mDialogPendingIntent = dialogPendingIntent;
}
private FillResponse(@NonNull Builder builder) {
@@ -219,6 +223,7 @@
mShowFillDialogIcon = builder.mShowFillDialogIcon;
mShowSaveDialogIcon = builder.mShowSaveDialogIcon;
mDetectedFieldTypes = builder.mDetectedFieldTypes;
+ mDialogPendingIntent = builder.mDialogPendingIntent;
}
/** @hide */
@@ -399,6 +404,7 @@
private boolean mShowFillDialogIcon = true;
private boolean mShowSaveDialogIcon = true;
private FieldClassification[] mDetectedFieldTypes;
+ private PendingIntent mDialogPendingIntent;
/**
* Adds a new {@link FieldClassification} to this response, to
@@ -1079,6 +1085,24 @@
}
/**
+ * Sets credential dialog pending intent. Framework will use the intent to launch the
+ * selector UI. A replacement for previous fill bottom sheet.
+ *
+ * @throws IllegalStateException if {@link #build()} was already called.
+ * @throws NullPointerException if {@code pendingIntent} is {@code null}.
+ *
+ * @hide
+ */
+ @NonNull
+ public Builder setDialogPendingIntent(@NonNull PendingIntent pendingIntent) {
+ throwIfDestroyed();
+ Preconditions.checkNotNull(pendingIntent,
+ "can't pass a null object to setDialogPendingIntent");
+ mDialogPendingIntent = pendingIntent;
+ return this;
+ }
+
+ /**
* Builds a new {@link FillResponse} instance.
*
* @throws IllegalStateException if any of the following conditions occur:
@@ -1187,6 +1211,9 @@
if (mAuthentication != null) {
builder.append(", hasAuthentication");
}
+ if (mDialogPendingIntent != null) {
+ builder.append(", hasDialogPendingIntent");
+ }
if (mAuthenticationIds != null) {
builder.append(", authenticationIds=").append(Arrays.toString(mAuthenticationIds));
}
@@ -1232,6 +1259,7 @@
parcel.writeParcelable(mInlineTooltipPresentation, flags);
parcel.writeParcelable(mDialogPresentation, flags);
parcel.writeParcelable(mDialogHeader, flags);
+ parcel.writeParcelable(mDialogPendingIntent, flags);
parcel.writeParcelableArray(mFillDialogTriggerIds, flags);
parcel.writeParcelable(mHeader, flags);
parcel.writeParcelable(mFooter, flags);
@@ -1282,6 +1310,11 @@
if (dialogHeader != null) {
builder.setDialogHeader(dialogHeader);
}
+ final PendingIntent dialogPendingIntent = parcel.readParcelable(null,
+ PendingIntent.class);
+ if (dialogPendingIntent != null) {
+ builder.setDialogPendingIntent(dialogPendingIntent);
+ }
final AutofillId[] triggerIds = parcel.readParcelableArray(null, AutofillId.class);
if (triggerIds != null) {
builder.setFillDialogTriggerIds(triggerIds);
diff --git a/core/java/android/service/notification/ZenModeConfig.java b/core/java/android/service/notification/ZenModeConfig.java
index c479877..9d19ef6 100644
--- a/core/java/android/service/notification/ZenModeConfig.java
+++ b/core/java/android/service/notification/ZenModeConfig.java
@@ -185,7 +185,13 @@
SUPPRESSED_EFFECT_SCREEN_OFF | SUPPRESSED_EFFECT_FULL_SCREEN_INTENT
| SUPPRESSED_EFFECT_LIGHTS | SUPPRESSED_EFFECT_PEEK | SUPPRESSED_EFFECT_AMBIENT;
- public static final int XML_VERSION = 8;
+ // ZenModeConfig XML versions distinguishing key changes.
+ public static final int XML_VERSION_ZEN_UPGRADE = 8;
+ public static final int XML_VERSION_MODES_API = 11;
+
+ // TODO: b/310620812 - Update XML_VERSION and update default_zen_config.xml accordingly when
+ // modes_api is inlined.
+ private static final int XML_VERSION = 10;
public static final String ZEN_TAG = "zen";
private static final String ZEN_ATT_VERSION = "version";
private static final String ZEN_ATT_USER = "user";
@@ -586,6 +592,10 @@
}
}
+ public static int getCurrentXmlVersion() {
+ return Flags.modesApi() ? XML_VERSION_MODES_API : XML_VERSION;
+ }
+
public static ZenModeConfig readXml(TypedXmlPullParser parser)
throws XmlPullParserException, IOException {
int type = parser.getEventType();
@@ -593,7 +603,7 @@
String tag = parser.getName();
if (!ZEN_TAG.equals(tag)) return null;
final ZenModeConfig rt = new ZenModeConfig();
- rt.version = safeInt(parser, ZEN_ATT_VERSION, XML_VERSION);
+ rt.version = safeInt(parser, ZEN_ATT_VERSION, getCurrentXmlVersion());
rt.user = safeInt(parser, ZEN_ATT_USER, rt.user);
boolean readSuppressedEffects = false;
while ((type = parser.next()) != XmlPullParser.END_DOCUMENT) {
@@ -707,14 +717,16 @@
/**
* Writes XML of current ZenModeConfig
* @param out serializer
- * @param version uses XML_VERSION if version is null
+ * @param version uses the current XML version if version is null
* @throws IOException
*/
+
public void writeXml(TypedXmlSerializer out, Integer version, boolean forBackup)
throws IOException {
+ int xmlVersion = getCurrentXmlVersion();
out.startTag(null, ZEN_TAG);
out.attribute(null, ZEN_ATT_VERSION, version == null
- ? Integer.toString(XML_VERSION) : Integer.toString(version));
+ ? Integer.toString(xmlVersion) : Integer.toString(version));
out.attributeInt(null, ZEN_ATT_USER, user);
out.startTag(null, ALLOW_TAG);
out.attributeBoolean(null, ALLOW_ATT_CALLS, allowCalls);
@@ -1041,7 +1053,7 @@
out);
if (Flags.modesApi()) {
- writeZenPolicyState(ALLOW_ATT_CHANNELS, policy.getPriorityChannels(), out);
+ writeZenPolicyState(ALLOW_ATT_CHANNELS, policy.getPriorityChannelsAllowed(), out);
}
}
@@ -1369,7 +1381,7 @@
int state = defaultPolicy.state;
if (Flags.modesApi()) {
state = Policy.policyState(defaultPolicy.hasPriorityChannels(),
- ZenPolicy.stateToBoolean(zenPolicy.getPriorityChannels(),
+ ZenPolicy.stateToBoolean(zenPolicy.getPriorityChannelsAllowed(),
DEFAULT_ALLOW_PRIORITY_CHANNELS));
}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/VolumePanelComponentKey.kt b/core/java/android/service/notification/ZenPolicy.aidl
similarity index 77%
copy from packages/SystemUI/src/com/android/systemui/volume/panel/VolumePanelComponentKey.kt
copy to core/java/android/service/notification/ZenPolicy.aidl
index 22a74d2..b56f5c6 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/VolumePanelComponentKey.kt
+++ b/core/java/android/service/notification/ZenPolicy.aidl
@@ -14,7 +14,6 @@
* limitations under the License.
*/
-package com.android.systemui.volume.panel
+package android.service.notification;
-/** Uniquely identifies the [com.android.systemui.volume.panel.ui.VolumePanelComponent]. */
-typealias VolumePanelComponentKey = String
+parcelable ZenPolicy;
\ No newline at end of file
diff --git a/core/java/android/service/notification/ZenPolicy.java b/core/java/android/service/notification/ZenPolicy.java
index fb491d0..786d768 100644
--- a/core/java/android/service/notification/ZenPolicy.java
+++ b/core/java/android/service/notification/ZenPolicy.java
@@ -570,7 +570,7 @@
* with {@link NotificationChannel#canBypassDnd()} will be intercepted.
*/
@FlaggedApi(Flags.FLAG_MODES_API)
- public @State int getPriorityChannels() {
+ public @State int getPriorityChannelsAllowed() {
switch (mAllowChannels) {
case CHANNEL_POLICY_PRIORITY:
return STATE_ALLOW;
@@ -1422,6 +1422,54 @@
}
/**
+ * Overwrites any policy values in this ZenPolicy with set values from newPolicy and
+ * returns a copy of the resulting ZenPolicy.
+ * Unlike apply(), values set in newPolicy will always be kept over pre-existing
+ * fields. Any values in newPolicy that are not set keep their currently set values.
+ *
+ * @hide
+ */
+ @TestApi
+ @FlaggedApi(Flags.FLAG_MODES_API)
+ public @NonNull ZenPolicy overwrittenWith(@Nullable ZenPolicy newPolicy) {
+ ZenPolicy result = this.copy();
+
+ if (newPolicy == null) {
+ return result;
+ }
+
+ // set priority categories
+ for (int category = 0; category < mPriorityCategories.size(); category++) {
+ @State int newState = newPolicy.mPriorityCategories.get(category);
+ if (newState != STATE_UNSET) {
+ result.mPriorityCategories.set(category, newState);
+
+ if (category == PRIORITY_CATEGORY_MESSAGES) {
+ result.mPriorityMessages = newPolicy.mPriorityMessages;
+ } else if (category == PRIORITY_CATEGORY_CALLS) {
+ result.mPriorityCalls = newPolicy.mPriorityCalls;
+ } else if (category == PRIORITY_CATEGORY_CONVERSATIONS) {
+ result.mConversationSenders = newPolicy.mConversationSenders;
+ }
+ }
+ }
+
+ // set visual effects
+ for (int visualEffect = 0; visualEffect < mVisualEffects.size(); visualEffect++) {
+ if (newPolicy.mVisualEffects.get(visualEffect) != STATE_UNSET) {
+ result.mVisualEffects.set(visualEffect, newPolicy.mVisualEffects.get(visualEffect));
+ }
+ }
+
+ // set allowed channels
+ if (newPolicy.mAllowChannels != CHANNEL_POLICY_UNSET) {
+ result.mAllowChannels = newPolicy.mAllowChannels;
+ }
+
+ return result;
+ }
+
+ /**
* @hide
*/
public void dumpDebug(ProtoOutputStream proto, long fieldId) {
@@ -1481,7 +1529,7 @@
proto.write(DNDPolicyProto.ALLOW_CONVERSATIONS_FROM, getPriorityConversationSenders());
if (Flags.modesApi()) {
- proto.write(DNDPolicyProto.ALLOW_CHANNELS, getPriorityChannels());
+ proto.write(DNDPolicyProto.ALLOW_CHANNELS, getPriorityChannelsAllowed());
}
proto.flush();
diff --git a/core/java/android/service/wallpaper/WallpaperService.java b/core/java/android/service/wallpaper/WallpaperService.java
index 1a2be15..bbda068 100644
--- a/core/java/android/service/wallpaper/WallpaperService.java
+++ b/core/java/android/service/wallpaper/WallpaperService.java
@@ -16,6 +16,7 @@
package android.service.wallpaper;
+import static android.app.WallpaperManager.COMMAND_DISPLAY_SWITCH;
import static android.app.WallpaperManager.COMMAND_FREEZE;
import static android.app.WallpaperManager.COMMAND_UNFREEZE;
import static android.app.WallpaperManager.SetWallpaperFlags;
@@ -153,6 +154,7 @@
static final boolean DEBUG = false;
static final float MIN_PAGE_ALLOWED_MARGIN = .05f;
private static final int MIN_BITMAP_SCREENSHOT_WIDTH = 64;
+ private static final long PRESERVE_VISIBLE_TIMEOUT_MS = 1000;
private static final long DEFAULT_UPDATE_SCREENSHOT_DURATION = 60 * 1000; //Once per minute
private static final @NonNull RectF LOCAL_COLOR_BOUNDS =
new RectF(0, 0, 1, 1);
@@ -165,6 +167,7 @@
private static final int MSG_UPDATE_SURFACE = 10000;
private static final int MSG_VISIBILITY_CHANGED = 10010;
+ private static final int MSG_REFRESH_VISIBILITY = 10011;
private static final int MSG_WALLPAPER_OFFSETS = 10020;
private static final int MSG_WALLPAPER_COMMAND = 10025;
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
@@ -248,6 +251,11 @@
*/
private boolean mIsScreenTurningOn;
boolean mReportedVisible;
+ /**
+ * This is used with {@link #PRESERVE_VISIBLE_TIMEOUT_MS} to avoid intermediate visibility
+ * changes if the display may be toggled in a short time, e.g. display switch.
+ */
+ boolean mPreserveVisible;
boolean mDestroyed;
// Set to true after receiving WallpaperManager#COMMAND_FREEZE. It's reset back to false
// after receiving WallpaperManager#COMMAND_UNFREEZE. COMMAND_FREEZE is fully applied once
@@ -263,7 +271,6 @@
boolean mDrawingAllowed;
boolean mOffsetsChanged;
boolean mFixedSizeAllowed;
- boolean mShouldDim;
// Whether the wallpaper should be dimmed by default (when no additional dimming is applied)
// based on its color hints
boolean mShouldDimByDefault;
@@ -340,9 +347,11 @@
private Display mDisplay;
private Context mDisplayContext;
private int mDisplayState;
- private float mWallpaperDimAmount = 0.05f;
+
+ private float mCustomDimAmount = 0f;
+ private float mWallpaperDimAmount = 0f;
private float mPreviousWallpaperDimAmount = mWallpaperDimAmount;
- private float mDefaultDimAmount = mWallpaperDimAmount;
+ private float mDefaultDimAmount = 0.05f;
SurfaceControl mSurfaceControl = new SurfaceControl();
SurfaceControl mBbqSurfaceControl;
@@ -978,11 +987,8 @@
mShouldDimByDefault = ((colorHints & WallpaperColors.HINT_SUPPORTS_DARK_TEXT) == 0
&& (colorHints & WallpaperColors.HINT_SUPPORTS_DARK_THEME) == 0);
- // If default dimming value changes and no additional dimming is applied
- if (mShouldDimByDefault != mShouldDim && mWallpaperDimAmount == 0f) {
- mShouldDim = mShouldDimByDefault;
- updateSurfaceDimming();
- }
+ // Recompute dim in case it changed compared to the previous WallpaperService
+ updateWallpaperDimming(mCustomDimAmount);
}
/**
@@ -991,28 +997,21 @@
* @param dimAmount Float amount between [0.0, 1.0] to dim the wallpaper.
*/
private void updateWallpaperDimming(float dimAmount) {
- if (dimAmount == mWallpaperDimAmount) {
- return;
- }
+ mCustomDimAmount = Math.min(1f, dimAmount);
- // Custom dim amount cannot be less than the default dim amount.
- mWallpaperDimAmount = Math.max(mDefaultDimAmount, dimAmount);
- // If dim amount is 0f (additional dimming is removed), then the wallpaper should dim
- // based on its default wallpaper color hints.
- mShouldDim = dimAmount != 0f || mShouldDimByDefault;
- updateSurfaceDimming();
- }
+ // If default dim is enabled, the actual dim amount is at least the default dim amount
+ mWallpaperDimAmount = (!mShouldDimByDefault) ? mCustomDimAmount
+ : Math.max(mDefaultDimAmount, mCustomDimAmount);
- private void updateSurfaceDimming() {
- if (!ENABLE_WALLPAPER_DIMMING || mBbqSurfaceControl == null) {
+ if (!ENABLE_WALLPAPER_DIMMING || mBbqSurfaceControl == null
+ || mWallpaperDimAmount == mPreviousWallpaperDimAmount) {
return;
}
SurfaceControl.Transaction surfaceControlTransaction = new SurfaceControl.Transaction();
// TODO: apply the dimming to preview as well once surface transparency works in
// preview mode.
- if ((!isPreview() && mShouldDim)
- || mPreviousWallpaperDimAmount != mWallpaperDimAmount) {
+ if (!isPreview()) {
Log.v(TAG, "Setting wallpaper dimming: " + mWallpaperDimAmount);
// Animate dimming to gradually change the wallpaper alpha from the previous
@@ -1084,6 +1083,9 @@
if (pendingCount != 0) {
out.print(prefix); out.print("mPendingResizeCount="); out.println(pendingCount);
}
+ if (mPreserveVisible) {
+ out.print(prefix); out.print("mPreserveVisible=true");
+ }
synchronized (mLock) {
out.print(prefix); out.print("mPendingXOffset="); out.print(mPendingXOffset);
out.print(" mPendingXOffset="); out.println(mPendingXOffset);
@@ -1534,8 +1536,6 @@
.createWindowContext(TYPE_WALLPAPER, null /* options */);
mDefaultDimAmount = mDisplayContext.getResources().getFloat(
com.android.internal.R.dimen.config_wallpaperDimAmount);
- mWallpaperDimAmount = mDefaultDimAmount;
- mPreviousWallpaperDimAmount = mWallpaperDimAmount;
mDisplayState = mDisplay.getCommittedState();
mMergedConfiguration.setOverrideConfiguration(
mDisplayContext.getResources().getConfiguration());
@@ -1643,7 +1643,8 @@
? false
: mIWallpaperEngine.mInfo.supportsAmbientMode();
// Report visibility only if display is fully on or wallpaper supports ambient mode.
- boolean visible = mVisible && (displayFullyOn || supportsAmbientMode);
+ final boolean visible = (mVisible && (displayFullyOn || supportsAmbientMode))
+ || mPreserveVisible;
if (DEBUG) {
Log.v(
TAG,
@@ -2080,6 +2081,9 @@
if (!mDestroyed) {
if (COMMAND_FREEZE.equals(cmd.action) || COMMAND_UNFREEZE.equals(cmd.action)) {
updateFrozenState(/* frozenRequested= */ !COMMAND_UNFREEZE.equals(cmd.action));
+ } else if (COMMAND_DISPLAY_SWITCH.equals(cmd.action)) {
+ handleDisplaySwitch(cmd.z == 1 /* startToSwitch */);
+ return;
}
result = onCommand(cmd.action, cmd.x, cmd.y, cmd.z,
cmd.extras, cmd.sync);
@@ -2095,6 +2099,23 @@
}
}
+ private void handleDisplaySwitch(boolean startToSwitch) {
+ if (startToSwitch && mReportedVisible) {
+ // The display may be off/on in a short time when the display is switching.
+ // Keep the visible state until onScreenTurnedOn or !startToSwitch is received, so
+ // the rendering thread can be active to redraw in time when receiving size change.
+ mPreserveVisible = true;
+ mCaller.removeMessages(MSG_REFRESH_VISIBILITY);
+ mCaller.sendMessageDelayed(mCaller.obtainMessage(MSG_REFRESH_VISIBILITY),
+ PRESERVE_VISIBLE_TIMEOUT_MS);
+ } else if (!startToSwitch && mPreserveVisible) {
+ // The switch is finished, so restore to actual visibility.
+ mPreserveVisible = false;
+ mCaller.removeMessages(MSG_REFRESH_VISIBILITY);
+ reportVisibility(false /* forceReport */);
+ }
+ }
+
private void updateFrozenState(boolean frozenRequested) {
if (mIWallpaperEngine.mInfo == null
// Procees the unfreeze command in case the wallaper became static while
@@ -2638,6 +2659,10 @@
+ ": " + message.arg1);
mEngine.doVisibilityChanged(message.arg1 != 0);
break;
+ case MSG_REFRESH_VISIBILITY:
+ mEngine.mPreserveVisible = false;
+ mEngine.reportVisibility(false /* forceReport */);
+ break;
case MSG_UPDATE_SCREEN_TURNING_ON:
if (DEBUG) {
Log.v(TAG,
diff --git a/core/java/android/telephony/TelephonyRegistryManager.java b/core/java/android/telephony/TelephonyRegistryManager.java
index 8935ab3..0a813a3 100644
--- a/core/java/android/telephony/TelephonyRegistryManager.java
+++ b/core/java/android/telephony/TelephonyRegistryManager.java
@@ -38,6 +38,7 @@
import android.telephony.Annotation.RadioPowerState;
import android.telephony.Annotation.SimActivationState;
import android.telephony.Annotation.SrvccState;
+import android.telephony.SubscriptionManager.OnSubscriptionsChangedListener;
import android.telephony.TelephonyManager.CarrierPrivilegesCallback;
import android.telephony.emergency.EmergencyNumber;
import android.telephony.ims.ImsCallSession;
@@ -116,14 +117,15 @@
}
/**
- * Register for changes to the list of active {@link SubscriptionInfo} records or to the
- * individual records themselves. When a change occurs the onSubscriptionsChanged method of
- * the listener will be invoked immediately if there has been a notification. The
- * onSubscriptionChanged method will also be triggered once initially when calling this
- * function.
+ * Register for changes to the list of {@link SubscriptionInfo} records or to the
+ * individual records (active or inactive) themselves. When a change occurs, the
+ * {@link OnSubscriptionsChangedListener#onSubscriptionsChanged()} method of
+ * the listener will be invoked immediately. The
+ * {@link OnSubscriptionsChangedListener#onSubscriptionsChanged()} method will also be invoked
+ * once initially when calling this method.
*
- * @param listener an instance of {@link SubscriptionManager.OnSubscriptionsChangedListener}
- * with onSubscriptionsChanged overridden.
+ * @param listener an instance of {@link OnSubscriptionsChangedListener} with
+ * {@link OnSubscriptionsChangedListener#onSubscriptionsChanged()} overridden.
* @param executor the executor that will execute callbacks.
* @hide
*/
diff --git a/core/java/android/text/DynamicLayout.java b/core/java/android/text/DynamicLayout.java
index 7b9cb6a..9286049 100644
--- a/core/java/android/text/DynamicLayout.java
+++ b/core/java/android/text/DynamicLayout.java
@@ -40,6 +40,7 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.ArrayUtils;
import com.android.internal.util.GrowingArrayUtils;
+import com.android.text.flags.Flags;
import java.lang.ref.WeakReference;
@@ -1276,8 +1277,21 @@
}
public void onSpanRemoved(Spannable s, Object o, int start, int end) {
- if (o instanceof UpdateLayout)
- transformAndReflow(s, start, end);
+ if (o instanceof UpdateLayout) {
+ if (Flags.insertModeCrashWhenDelete()) {
+ final DynamicLayout dynamicLayout = mLayout.get();
+ if (dynamicLayout != null && dynamicLayout.mDisplay instanceof OffsetMapping) {
+ // It's possible that a Span is removed when the text covering it is
+ // deleted, in this case, the original start and end of the span might be
+ // OOB. So it'll reflow the entire string instead.
+ reflow(s, 0, 0, s.length());
+ } else {
+ reflow(s, start, end - start, end - start);
+ }
+ } else {
+ transformAndReflow(s, start, end);
+ }
+ }
}
public void onSpanChanged(Spannable s, Object o, int start, int end, int nstart, int nend) {
@@ -1287,8 +1301,21 @@
// instead of causing an exception
start = 0;
}
- transformAndReflow(s, start, end);
- transformAndReflow(s, nstart, nend);
+ if (Flags.insertModeCrashWhenDelete()) {
+ final DynamicLayout dynamicLayout = mLayout.get();
+ if (dynamicLayout != null && dynamicLayout.mDisplay instanceof OffsetMapping) {
+ // When text is changed, it'll also trigger onSpanChanged. In this case we
+ // can't determine the updated range in the transformed text. So it'll
+ // reflow the entire range instead.
+ reflow(s, 0, 0, s.length());
+ } else {
+ reflow(s, start, end - start, end - start);
+ reflow(s, nstart, nend - nstart, nend - nstart);
+ }
+ } else {
+ transformAndReflow(s, start, end);
+ transformAndReflow(s, nstart, nend);
+ }
}
}
diff --git a/core/java/android/text/flags/flags.aconfig b/core/java/android/text/flags/flags.aconfig
index bf1a596..6e45fea 100644
--- a/core/java/android/text/flags/flags.aconfig
+++ b/core/java/android/text/flags/flags.aconfig
@@ -89,3 +89,10 @@
description: "Feature flag for clearing focus when the escape key is pressed."
bug: "312921137"
}
+
+flag {
+ name: "insert_mode_crash_when_delete"
+ namespace: "text"
+ description: "A feature flag for fixing the crash while delete text in insert mode."
+ bug: "314254153"
+}
diff --git a/core/java/android/util/Singleton.java b/core/java/android/util/Singleton.java
index 92646b4..d27bef9 100644
--- a/core/java/android/util/Singleton.java
+++ b/core/java/android/util/Singleton.java
@@ -25,6 +25,7 @@
*
* @hide
*/
+@android.ravenwood.annotation.RavenwoodKeepWholeClass
public abstract class Singleton<T> {
@UnsupportedAppUsage
diff --git a/core/java/android/view/Display.java b/core/java/android/view/Display.java
index 1908c64c..fbadef3 100644
--- a/core/java/android/view/Display.java
+++ b/core/java/android/view/Display.java
@@ -2079,6 +2079,7 @@
*
* @see Display#getSupportedModes()
*/
+ @android.ravenwood.annotation.RavenwoodKeepWholeClass
public static final class Mode implements Parcelable {
/**
* @hide
@@ -2467,6 +2468,7 @@
* <p>You can get an instance for a given {@link Display} object with
* {@link Display#getHdrCapabilities getHdrCapabilities()}.
*/
+ @android.ravenwood.annotation.RavenwoodKeepWholeClass
public static final class HdrCapabilities implements Parcelable {
/**
* Invalid luminance value.
diff --git a/core/java/android/view/DisplayInfo.java b/core/java/android/view/DisplayInfo.java
index 981911e..5654bc1 100644
--- a/core/java/android/view/DisplayInfo.java
+++ b/core/java/android/view/DisplayInfo.java
@@ -51,6 +51,7 @@
* Describes the characteristics of a particular logical display.
* @hide
*/
+@android.ravenwood.annotation.RavenwoodKeepWholeClass
public final class DisplayInfo implements Parcelable {
/**
* The surface flinger layer stack associated with this logical display.
diff --git a/core/java/android/view/IWindowSession.aidl b/core/java/android/view/IWindowSession.aidl
index 1d81be1..d2c25cd 100644
--- a/core/java/android/view/IWindowSession.aidl
+++ b/core/java/android/view/IWindowSession.aidl
@@ -235,7 +235,7 @@
*/
oneway void setWallpaperDisplayOffset(IBinder windowToken, int x, int y);
- Bundle sendWallpaperCommand(IBinder window, String action, int x, int y,
+ oneway void sendWallpaperCommand(IBinder window, String action, int x, int y,
int z, in Bundle extras, boolean sync);
@UnsupportedAppUsage
diff --git a/core/java/android/view/InsetsAnimationControlImpl.java b/core/java/android/view/InsetsAnimationControlImpl.java
index 17a3a12..7f1e037 100644
--- a/core/java/android/view/InsetsAnimationControlImpl.java
+++ b/core/java/android/view/InsetsAnimationControlImpl.java
@@ -34,11 +34,11 @@
import static android.view.InsetsController.LAYOUT_INSETS_DURING_ANIMATION_SHOWN;
import static android.view.InsetsController.LayoutInsetsDuringAnimation;
import static android.view.InsetsSource.ID_IME;
-import static android.view.InsetsState.ISIDE_BOTTOM;
-import static android.view.InsetsState.ISIDE_FLOATING;
-import static android.view.InsetsState.ISIDE_LEFT;
-import static android.view.InsetsState.ISIDE_RIGHT;
-import static android.view.InsetsState.ISIDE_TOP;
+import static android.view.InsetsSource.SIDE_BOTTOM;
+import static android.view.InsetsSource.SIDE_NONE;
+import static android.view.InsetsSource.SIDE_LEFT;
+import static android.view.InsetsSource.SIDE_RIGHT;
+import static android.view.InsetsSource.SIDE_TOP;
import static android.view.WindowInsets.Type.ime;
import static android.view.WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE;
import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION;
@@ -60,7 +60,7 @@
import android.util.SparseIntArray;
import android.util.SparseSetArray;
import android.util.proto.ProtoOutputStream;
-import android.view.InsetsState.InternalInsetsSide;
+import android.view.InsetsSource.InternalInsetsSide;
import android.view.SyncRtSurfaceTransactionApplier.SurfaceParams;
import android.view.WindowInsets.Type.InsetsType;
import android.view.WindowInsetsAnimation.Bounds;
@@ -142,7 +142,7 @@
if (mHasZeroInsetsIme) {
// IME has shownInsets of ZERO, and can't map to a side by default.
// Map zero insets IME to bottom, making it a special case of bottom insets.
- idSideMap.put(ID_IME, ISIDE_BOTTOM);
+ idSideMap.put(ID_IME, SIDE_BOTTOM);
}
buildSideControlsMap(idSideMap, mSideControlsMap, controls);
} else {
@@ -286,10 +286,10 @@
}
final Insets offset = Insets.subtract(mShownInsets, mPendingInsets);
final ArrayList<SurfaceParams> params = new ArrayList<>();
- updateLeashesForSide(ISIDE_LEFT, offset.left, params, outState, mPendingAlpha);
- updateLeashesForSide(ISIDE_TOP, offset.top, params, outState, mPendingAlpha);
- updateLeashesForSide(ISIDE_RIGHT, offset.right, params, outState, mPendingAlpha);
- updateLeashesForSide(ISIDE_BOTTOM, offset.bottom, params, outState, mPendingAlpha);
+ updateLeashesForSide(SIDE_LEFT, offset.left, params, outState, mPendingAlpha);
+ updateLeashesForSide(SIDE_TOP, offset.top, params, outState, mPendingAlpha);
+ updateLeashesForSide(SIDE_RIGHT, offset.right, params, outState, mPendingAlpha);
+ updateLeashesForSide(SIDE_BOTTOM, offset.bottom, params, outState, mPendingAlpha);
mController.applySurfaceParams(params.toArray(new SurfaceParams[params.size()]));
mCurrentInsets = mPendingInsets;
@@ -499,19 +499,19 @@
final float surfaceOffset = mTranslator != null
? mTranslator.translateLengthInAppWindowToScreen(offset) : offset;
switch (side) {
- case ISIDE_LEFT:
+ case SIDE_LEFT:
m.postTranslate(-surfaceOffset, 0);
frame.offset(-offset, 0);
break;
- case ISIDE_TOP:
+ case SIDE_TOP:
m.postTranslate(0, -surfaceOffset);
frame.offset(0, -offset);
break;
- case ISIDE_RIGHT:
+ case SIDE_RIGHT:
m.postTranslate(surfaceOffset, 0);
frame.offset(offset, 0);
break;
- case ISIDE_BOTTOM:
+ case SIDE_BOTTOM:
m.postTranslate(0, surfaceOffset);
frame.offset(0, offset);
break;
@@ -543,9 +543,10 @@
// control may be null if it got revoked.
continue;
}
- @InternalInsetsSide int side = InsetsState.getInsetSide(control.getInsetsHint());
- if (side == ISIDE_FLOATING && control.getType() == WindowInsets.Type.ime()) {
- side = ISIDE_BOTTOM;
+ @InternalInsetsSide int side = InsetsSource.getInsetSide(control.getInsetsHint());
+ if (side == SIDE_NONE && control.getType() == WindowInsets.Type.ime()) {
+ // IME might not provide insets when it is fullscreen or floating.
+ side = SIDE_BOTTOM;
}
sideControlsMap.add(side, control);
}
diff --git a/core/java/android/view/InsetsSource.java b/core/java/android/view/InsetsSource.java
index 0927d45..bc33d5e 100644
--- a/core/java/android/view/InsetsSource.java
+++ b/core/java/android/view/InsetsSource.java
@@ -17,6 +17,7 @@
package android.view;
import static android.view.InsetsSourceProto.FRAME;
+import static android.view.InsetsSourceProto.TYPE;
import static android.view.InsetsSourceProto.TYPE_NUMBER;
import static android.view.InsetsSourceProto.VISIBLE;
import static android.view.InsetsSourceProto.VISIBLE_FRAME;
@@ -46,6 +47,24 @@
*/
public class InsetsSource implements Parcelable {
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(prefix = "SIDE_", value = {
+ SIDE_NONE,
+ SIDE_LEFT,
+ SIDE_TOP,
+ SIDE_RIGHT,
+ SIDE_BOTTOM,
+ SIDE_UNKNOWN
+ })
+ public @interface InternalInsetsSide {}
+
+ static final int SIDE_NONE = 0;
+ static final int SIDE_LEFT = 1;
+ static final int SIDE_TOP = 2;
+ static final int SIDE_RIGHT = 3;
+ static final int SIDE_BOTTOM = 4;
+ static final int SIDE_UNKNOWN = 5;
+
/** The insets source ID of IME */
public static final int ID_IME = createId(null, 0, ime());
@@ -101,6 +120,12 @@
private boolean mVisible;
+ /**
+ * Used to decide which side of the relative frame should receive insets when the frame fully
+ * covers the relative frame.
+ */
+ private @InternalInsetsSide int mSideHint = SIDE_NONE;
+
private final Rect mTmpFrame = new Rect();
public InsetsSource(int id, @InsetsType int type) {
@@ -119,6 +144,7 @@
? new Rect(other.mVisibleFrame)
: null;
mFlags = other.mFlags;
+ mSideHint = other.mSideHint;
}
public void set(InsetsSource other) {
@@ -128,6 +154,7 @@
? new Rect(other.mVisibleFrame)
: null;
mFlags = other.mFlags;
+ mSideHint = other.mSideHint;
}
public InsetsSource setFrame(int left, int top, int right, int bottom) {
@@ -160,6 +187,18 @@
return this;
}
+ /**
+ * Updates the side hint which is used to decide which side of the relative frame should receive
+ * insets when the frame fully covers the relative frame.
+ *
+ * @param bounds A rectangle which contains the frame. It will be used to calculate the hint.
+ */
+ public InsetsSource updateSideHint(Rect bounds) {
+ mSideHint = getInsetSide(
+ calculateInsets(bounds, mFrame, true /* ignoreVisibility */));
+ return this;
+ }
+
public int getId() {
return mId;
}
@@ -236,8 +275,21 @@
return Insets.of(0, 0, 0, mTmpFrame.height());
}
- // Intersecting at top/bottom
- if (mTmpFrame.width() == relativeFrame.width()) {
+ if (mTmpFrame.equals(relativeFrame)) {
+ // Covering all sides
+ switch (mSideHint) {
+ default:
+ case SIDE_LEFT:
+ return Insets.of(mTmpFrame.width(), 0, 0, 0);
+ case SIDE_TOP:
+ return Insets.of(0, mTmpFrame.height(), 0, 0);
+ case SIDE_RIGHT:
+ return Insets.of(0, 0, mTmpFrame.width(), 0);
+ case SIDE_BOTTOM:
+ return Insets.of(0, 0, 0, mTmpFrame.height());
+ }
+ } else if (mTmpFrame.width() == relativeFrame.width()) {
+ // Intersecting at top/bottom
if (mTmpFrame.top == relativeFrame.top) {
return Insets.of(0, mTmpFrame.height(), 0, 0);
} else if (mTmpFrame.bottom == relativeFrame.bottom) {
@@ -249,9 +301,8 @@
if (mTmpFrame.top == 0) {
return Insets.of(0, mTmpFrame.height(), 0, 0);
}
- }
- // Intersecting at left/right
- else if (mTmpFrame.height() == relativeFrame.height()) {
+ } else if (mTmpFrame.height() == relativeFrame.height()) {
+ // Intersecting at left/right
if (mTmpFrame.left == relativeFrame.left) {
return Insets.of(mTmpFrame.width(), 0, 0, 0);
} else if (mTmpFrame.right == relativeFrame.right) {
@@ -283,6 +334,46 @@
}
/**
+ * Retrieves the side for a certain {@code insets}. It is required that only one field l/t/r/b
+ * is set in order that this method returns a meaningful result.
+ */
+ static @InternalInsetsSide int getInsetSide(Insets insets) {
+ if (Insets.NONE.equals(insets)) {
+ return SIDE_NONE;
+ }
+ if (insets.left != 0) {
+ return SIDE_LEFT;
+ }
+ if (insets.top != 0) {
+ return SIDE_TOP;
+ }
+ if (insets.right != 0) {
+ return SIDE_RIGHT;
+ }
+ if (insets.bottom != 0) {
+ return SIDE_BOTTOM;
+ }
+ return SIDE_UNKNOWN;
+ }
+
+ static String sideToString(@InternalInsetsSide int side) {
+ switch (side) {
+ case SIDE_NONE:
+ return "NONE";
+ case SIDE_LEFT:
+ return "LEFT";
+ case SIDE_TOP:
+ return "TOP";
+ case SIDE_RIGHT:
+ return "RIGHT";
+ case SIDE_BOTTOM:
+ return "BOTTOM";
+ default:
+ return "UNKNOWN:" + side;
+ }
+ }
+
+ /**
* Creates an identifier of an {@link InsetsSource}.
*
* @param owner An object owned by the owner. Only the owner can modify its own sources.
@@ -331,7 +422,7 @@
}
public static String flagsToString(@Flags int flags) {
- final StringJoiner joiner = new StringJoiner(" ");
+ final StringJoiner joiner = new StringJoiner("|");
if ((flags & FLAG_SUPPRESS_SCRIM) != 0) {
joiner.add("SUPPRESS_SCRIM");
}
@@ -352,6 +443,10 @@
*/
public void dumpDebug(ProtoOutputStream proto, long fieldId) {
final long token = proto.start(fieldId);
+ if (!android.os.Flags.androidOsBuildVanillaIceCream()) {
+ // Deprecated since V.
+ proto.write(TYPE, WindowInsets.Type.toString(mType));
+ }
mFrame.dumpDebug(proto, FRAME);
if (mVisibleFrame != null) {
mVisibleFrame.dumpDebug(proto, VISIBLE_FRAME);
@@ -371,6 +466,7 @@
}
pw.print(" visible="); pw.print(mVisible);
pw.print(" flags="); pw.print(flagsToString(mFlags));
+ pw.print(" sideHint="); pw.print(sideToString(mSideHint));
pw.println();
}
@@ -393,6 +489,7 @@
if (mType != that.mType) return false;
if (mVisible != that.mVisible) return false;
if (mFlags != that.mFlags) return false;
+ if (mSideHint != that.mSideHint) return false;
if (excludeInvisibleImeFrames && !mVisible && mType == WindowInsets.Type.ime()) return true;
if (!Objects.equals(mVisibleFrame, that.mVisibleFrame)) return false;
return mFrame.equals(that.mFrame);
@@ -400,7 +497,7 @@
@Override
public int hashCode() {
- return Objects.hash(mId, mType, mFrame, mVisibleFrame, mVisible, mFlags);
+ return Objects.hash(mId, mType, mFrame, mVisibleFrame, mVisible, mFlags, mSideHint);
}
public InsetsSource(Parcel in) {
@@ -414,6 +511,7 @@
}
mVisible = in.readBoolean();
mFlags = in.readInt();
+ mSideHint = in.readInt();
}
@Override
@@ -434,6 +532,7 @@
}
dest.writeBoolean(mVisible);
dest.writeInt(mFlags);
+ dest.writeInt(mSideHint);
}
@Override
@@ -442,7 +541,8 @@
+ " mType=" + WindowInsets.Type.toString(mType)
+ " mFrame=" + mFrame.toShortString()
+ " mVisible=" + mVisible
- + " mFlags=[" + flagsToString(mFlags) + "]"
+ + " mFlags=" + flagsToString(mFlags)
+ + " mSideHint=" + sideToString(mSideHint)
+ "}";
}
diff --git a/core/java/android/view/InsetsState.java b/core/java/android/view/InsetsState.java
index 59e0932..c88da9e 100644
--- a/core/java/android/view/InsetsState.java
+++ b/core/java/android/view/InsetsState.java
@@ -37,7 +37,6 @@
import static android.view.WindowManager.LayoutParams.TYPE_SYSTEM_ERROR;
import static android.view.WindowManager.LayoutParams.TYPE_WALLPAPER;
-import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.WindowConfiguration.ActivityType;
@@ -48,6 +47,7 @@
import android.util.SparseArray;
import android.util.SparseIntArray;
import android.util.proto.ProtoOutputStream;
+import android.view.InsetsSource.InternalInsetsSide;
import android.view.WindowInsets.Type;
import android.view.WindowInsets.Type.InsetsType;
import android.view.WindowManager.LayoutParams.SoftInputModeFlags;
@@ -55,8 +55,6 @@
import com.android.internal.annotations.VisibleForTesting;
import java.io.PrintWriter;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
import java.util.Objects;
import java.util.StringJoiner;
@@ -66,23 +64,6 @@
*/
public class InsetsState implements Parcelable {
- @Retention(RetentionPolicy.SOURCE)
- @IntDef(prefix = "ISIDE", value = {
- ISIDE_LEFT,
- ISIDE_TOP,
- ISIDE_RIGHT,
- ISIDE_BOTTOM,
- ISIDE_FLOATING,
- ISIDE_UNKNOWN
- })
- public @interface InternalInsetsSide {}
- static final int ISIDE_LEFT = 0;
- static final int ISIDE_TOP = 1;
- static final int ISIDE_RIGHT = 2;
- static final int ISIDE_BOTTOM = 3;
- static final int ISIDE_FLOATING = 4;
- static final int ISIDE_UNKNOWN = 5;
-
private final SparseArray<InsetsSource> mSources;
/**
@@ -398,37 +379,14 @@
}
if (idSideMap != null) {
- @InternalInsetsSide int insetSide = getInsetSide(insets);
- if (insetSide != ISIDE_UNKNOWN) {
+ @InternalInsetsSide int insetSide = InsetsSource.getInsetSide(insets);
+ if (insetSide != InsetsSource.SIDE_UNKNOWN) {
idSideMap.put(source.getId(), insetSide);
}
}
}
/**
- * Retrieves the side for a certain {@code insets}. It is required that only one field l/t/r/b
- * is set in order that this method returns a meaningful result.
- */
- static @InternalInsetsSide int getInsetSide(Insets insets) {
- if (Insets.NONE.equals(insets)) {
- return ISIDE_FLOATING;
- }
- if (insets.left != 0) {
- return ISIDE_LEFT;
- }
- if (insets.top != 0) {
- return ISIDE_TOP;
- }
- if (insets.right != 0) {
- return ISIDE_RIGHT;
- }
- if (insets.bottom != 0) {
- return ISIDE_BOTTOM;
- }
- return ISIDE_UNKNOWN;
- }
-
- /**
* Gets the source mapped from the ID, or creates one if no such mapping has been made.
*/
public InsetsSource getOrCreateSource(int id, int type) {
diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java
index c98d1d7..1b22fda 100644
--- a/core/java/android/view/View.java
+++ b/core/java/android/view/View.java
@@ -5546,11 +5546,11 @@
@FlaggedApi(FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY)
public static final float REQUESTED_FRAME_RATE_CATEGORY_NO_PREFERENCE = -1;
@FlaggedApi(FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY)
- public static final float REQUESTED_FRAME_RATE_CATEGORY_LOW = -30;
+ public static final float REQUESTED_FRAME_RATE_CATEGORY_LOW = -2;
@FlaggedApi(FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY)
- public static final float REQUESTED_FRAME_RATE_CATEGORY_NORMAL = -60;
+ public static final float REQUESTED_FRAME_RATE_CATEGORY_NORMAL = -3;
@FlaggedApi(FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY)
- public static final float REQUESTED_FRAME_RATE_CATEGORY_HIGH = -120;
+ public static final float REQUESTED_FRAME_RATE_CATEGORY_HIGH = -4;
/**
* Simple constructor to use when creating a view from code.
@@ -28492,6 +28492,7 @@
surface.destroy();
}
session.kill();
+ surfaceControl.release();
}
}
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index e0bda91..7bc832e 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -26,7 +26,6 @@
import static android.view.InputDevice.SOURCE_CLASS_NONE;
import static android.view.InsetsSource.ID_IME;
import static android.view.Surface.FRAME_RATE_CATEGORY_HIGH;
-import static android.view.Surface.FRAME_RATE_CATEGORY_HIGH_HINT;
import static android.view.Surface.FRAME_RATE_CATEGORY_NO_PREFERENCE;
import static android.view.View.PFLAG_DRAW_ANIMATION;
import static android.view.View.SYSTEM_UI_FLAG_FULLSCREEN;
@@ -1012,10 +1011,8 @@
// Used to check if there were any view invalidations in
// the previous time frame (FRAME_RATE_IDLENESS_REEVALUATE_TIME).
private boolean mHasInvalidation = false;
- // Used to check if it is in the frame rate boosting period.
+ // Used to check if it is in the touch boosting period.
private boolean mIsFrameRateBoosting = false;
- // Used to check if it is in touch boosting period.
- private boolean mIsTouchBoosting = false;
// Used to check if there is a message in the message queue
// for idleness handling.
private boolean mHasIdledMessage = false;
@@ -6424,12 +6421,11 @@
* Lower the frame rate after the boosting period (FRAME_RATE_TOUCH_BOOST_TIME).
*/
mIsFrameRateBoosting = false;
- mIsTouchBoosting = false;
setPreferredFrameRateCategory(Math.max(mPreferredFrameRateCategory,
mLastPreferredFrameRateCategory));
break;
case MSG_CHECK_INVALIDATION_IDLE:
- if (!mHasInvalidation && !mIsFrameRateBoosting && !mIsTouchBoosting) {
+ if (!mHasInvalidation && !mIsFrameRateBoosting) {
mPreferredFrameRateCategory = FRAME_RATE_CATEGORY_NO_PREFERENCE;
setPreferredFrameRateCategory(mPreferredFrameRateCategory);
mHasIdledMessage = false;
@@ -7452,7 +7448,7 @@
// For the variable refresh rate project
if (handled && shouldTouchBoost(action, mWindowAttributes.type)) {
// set the frame rate to the maximum value.
- mIsTouchBoosting = true;
+ mIsFrameRateBoosting = true;
setPreferredFrameRateCategory(mPreferredFrameRateCategory);
}
/**
@@ -11995,7 +11991,7 @@
Runnable timeoutRunnable = () -> Log.e(mTag,
"Failed to submit the sync transaction after 4s. Likely to ANR "
+ "soon");
- mHandler.postDelayed(timeoutRunnable, 4L * Build.HW_TIMEOUT_MULTIPLIER);
+ mHandler.postDelayed(timeoutRunnable, 4000L * Build.HW_TIMEOUT_MULTIPLIER);
transaction.addTransactionCommittedListener(mSimpleExecutor,
() -> mHandler.removeCallbacks(timeoutRunnable));
surfaceSyncGroup.addTransaction(transaction);
@@ -12204,16 +12200,8 @@
return;
}
- int frameRateCategory = mIsTouchBoosting
- ? FRAME_RATE_CATEGORY_HIGH_HINT : preferredFrameRateCategory;
-
- // FRAME_RATE_CATEGORY_HIGH has a higher precedence than FRAME_RATE_CATEGORY_HIGH_HINT
- // For now, FRAME_RATE_CATEGORY_HIGH_HINT is used for boosting with user interaction.
- // FRAME_RATE_CATEGORY_HIGH is for boosting without user interaction
- // (e.g., Window Initialization).
- if (mIsFrameRateBoosting || mInsetsAnimationRunning) {
- frameRateCategory = FRAME_RATE_CATEGORY_HIGH;
- }
+ int frameRateCategory = mIsFrameRateBoosting || mInsetsAnimationRunning
+ ? FRAME_RATE_CATEGORY_HIGH : preferredFrameRateCategory;
try {
if (mLastPreferredFrameRateCategory != frameRateCategory) {
diff --git a/core/java/android/view/WindowlessWindowManager.java b/core/java/android/view/WindowlessWindowManager.java
index b95e459..c4d18c6 100644
--- a/core/java/android/view/WindowlessWindowManager.java
+++ b/core/java/android/view/WindowlessWindowManager.java
@@ -534,9 +534,8 @@
}
@Override
- public android.os.Bundle sendWallpaperCommand(android.os.IBinder window,
+ public void sendWallpaperCommand(android.os.IBinder window,
java.lang.String action, int x, int y, int z, android.os.Bundle extras, boolean sync) {
- return null;
}
@Override
diff --git a/core/java/android/view/accessibility/flags/accessibility_flags.aconfig b/core/java/android/view/accessibility/flags/accessibility_flags.aconfig
index efae57c..a11ac7c 100644
--- a/core/java/android/view/accessibility/flags/accessibility_flags.aconfig
+++ b/core/java/android/view/accessibility/flags/accessibility_flags.aconfig
@@ -94,6 +94,13 @@
}
flag {
+ name: "skip_accessibility_warning_dialog_for_trusted_services"
+ namespace: "accessibility"
+ description: "Skips showing the accessibility warning dialog for trusted services."
+ bug: "303511250"
+}
+
+flag {
namespace: "accessibility"
name: "update_always_on_a11y_service"
description: "Updates the Always-On A11yService state when the user changes the enablement of the shortcut."
diff --git a/core/java/android/view/contentcapture/MainContentCaptureSessionV2.java b/core/java/android/view/contentcapture/MainContentCaptureSessionV2.java
index bf1d31c..fbb66d1 100644
--- a/core/java/android/view/contentcapture/MainContentCaptureSessionV2.java
+++ b/core/java/android/view/contentcapture/MainContentCaptureSessionV2.java
@@ -149,9 +149,12 @@
*
* Because it is not guaranteed that the events will be enqueued from a single thread, the
* implementation must be thread-safe to prevent unexpected behaviour.
+ *
+ * @hide
*/
+ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
@NonNull
- private final ConcurrentLinkedQueue<ContentCaptureEvent> mEventProcessQueue;
+ public final ConcurrentLinkedQueue<ContentCaptureEvent> mEventProcessQueue;
/**
* List of events held to be sent to the {@link ContentCaptureService} as a batch.
@@ -908,7 +911,7 @@
* clear the buffer events then starting sending out current event.
*/
private void enqueueEvent(@NonNull final ContentCaptureEvent event, boolean forceFlush) {
- if (forceFlush) {
+ if (forceFlush || mEventProcessQueue.size() >= mManager.mOptions.maxBufferSize - 1) {
// The buffer events are cleared in the same thread first to prevent new events
// being added during the time of context switch. This would disrupt the sequence
// of events.
diff --git a/core/java/android/view/flags/view_flags.aconfig b/core/java/android/view/flags/view_flags.aconfig
index 9f9b7b4..1dd99ba 100644
--- a/core/java/android/view/flags/view_flags.aconfig
+++ b/core/java/android/view/flags/view_flags.aconfig
@@ -8,6 +8,15 @@
}
flag {
+ name: "enable_surface_native_alloc_registration_ro"
+ namespace: "toolkit"
+ description: "Feature flag for registering surfaces with the VM for faster"
+ " cleanup. Fixed readonly version."
+ bug: "306193257"
+ is_fixed_read_only: true
+}
+
+flag {
name: "enable_use_measure_cache_during_force_layout"
namespace: "toolkit"
description: "Enables using the measure cache during a view force layout from the second "
diff --git a/core/java/android/webkit/WebSettings.java b/core/java/android/webkit/WebSettings.java
index d12eda3..14c5348 100644
--- a/core/java/android/webkit/WebSettings.java
+++ b/core/java/android/webkit/WebSettings.java
@@ -1203,11 +1203,7 @@
* changes to this setting after that point.
*
* @param flag {@code true} if the WebView should use the database storage API
- * @deprecated WebSQL is deprecated and this method will become a no-op on all
- * Android versions once support is removed in Chromium. See
- * https://developer.chrome.com/blog/deprecating-web-sql for more information.
*/
- @Deprecated
public abstract void setDatabaseEnabled(boolean flag);
/**
@@ -1240,11 +1236,7 @@
*
* @return {@code true} if the database storage API is enabled
* @see #setDatabaseEnabled
- * @deprecated WebSQL is deprecated and this method will become a no-op on all
- * Android versions once support is removed in Chromium. See
- * https://developer.chrome.com/blog/deprecating-web-sql for more information.
*/
- @Deprecated
public abstract boolean getDatabaseEnabled();
/**
diff --git a/core/java/android/widget/Editor.java b/core/java/android/widget/Editor.java
index ddcfb40..57d268c 100644
--- a/core/java/android/widget/Editor.java
+++ b/core/java/android/widget/Editor.java
@@ -148,6 +148,7 @@
import com.android.internal.util.GrowingArrayUtils;
import com.android.internal.util.Preconditions;
import com.android.internal.view.FloatingActionMode;
+import com.android.text.flags.Flags;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -2343,6 +2344,13 @@
*/
void invalidateTextDisplayList(Layout layout, int start, int end) {
if (mTextRenderNodes != null && layout instanceof DynamicLayout) {
+ if (Flags.insertModeCrashWhenDelete()
+ && mTextView.isOffsetMappingAvailable()) {
+ // Text is transformed with an OffsetMapping, and we can't know the changed range
+ // on the transformed text. Invalidate the all display lists instead.
+ invalidateTextDisplayList();
+ return;
+ }
final int startTransformed =
mTextView.originalToTransformed(start, OffsetMapping.MAP_STRATEGY_CHARACTER);
final int endTransformed =
diff --git a/core/java/android/widget/RemoteCanvas.java b/core/java/android/widget/RemoteCanvas.java
new file mode 100644
index 0000000..9a0898c
--- /dev/null
+++ b/core/java/android/widget/RemoteCanvas.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.widget;
+
+import static android.appwidget.flags.Flags.FLAG_DRAW_DATA_PARCEL;
+
+import android.annotation.AttrRes;
+import android.annotation.FlaggedApi;
+import android.annotation.StyleRes;
+import android.content.Context;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.util.SparseArray;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+
+import java.util.function.IntConsumer;
+
+/**
+ * {@link RemoteCanvas} is designed to support arbitrary protocols between two processes using
+ * {@link RemoteViews.DrawInstructions}. Upon instantiation in the host process,
+ * {@link RemoteCanvas#setDrawInstructions(RemoteViews.DrawInstructions)} is called so that the
+ * host process can render the {@link RemoteViews.DrawInstructions} from the provider process
+ * accordingly.
+ *
+ * @hide
+ */
+@FlaggedApi(FLAG_DRAW_DATA_PARCEL)
+public class RemoteCanvas extends View {
+
+ private static final String TAG = "RemoteCanvas";
+
+ @Nullable
+ private SparseArray<Runnable> mCallbacks;
+
+ private final IntConsumer mOnClickHandler = (viewId) -> {
+ if (mCallbacks == null) {
+ Log.w(TAG, "Cannot find callback for " + viewId
+ + ", in fact there were no callbacks from this RemoteViews at all.");
+ return;
+ }
+ final Runnable cb = getCallbacks().get(viewId);
+ if (cb != null) {
+ cb.run();
+ } else {
+ Log.w(TAG, "Cannot find callback for " + viewId);
+ }
+ };
+
+ RemoteCanvas(@NonNull Context context) {
+ super(context);
+ }
+
+ RemoteCanvas(@NonNull Context context, @Nullable AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ RemoteCanvas(@NonNull Context context, @Nullable AttributeSet attrs,
+ @AttrRes int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ RemoteCanvas(@NonNull Context context, @Nullable AttributeSet attrs,
+ @AttrRes int defStyleAttr, @StyleRes int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ /**
+ * Setter method for the {@link RemoteViews.DrawInstructions} from the provider process for
+ * the host process to render accordingly.
+ *
+ * @param instructions {@link RemoteViews.DrawInstructions} from the provider process.
+ */
+ void setDrawInstructions(@NonNull final RemoteViews.DrawInstructions instructions) {
+ setTag(instructions);
+ // TODO: handle draw instructions
+ // TODO: attach mOnClickHandler
+ }
+
+ /**
+ * Adds a callback function to a clickable area in the RemoteCanvas.
+ *
+ * @param viewId the viewId of the clickable area
+ * @param cb the callback function to be triggered when clicked
+ */
+ void addOnClickHandler(final int viewId, @NonNull final Runnable cb) {
+ getCallbacks().set(viewId, cb);
+ }
+
+ /**
+ * Returns all callbacks added to the RemoteCanvas through
+ * {@link #addOnClickHandler(int, Runnable)}.
+ */
+ @VisibleForTesting
+ public SparseArray<Runnable> getCallbacks() {
+ if (mCallbacks == null) {
+ mCallbacks = new SparseArray<>();
+ }
+ return mCallbacks;
+ }
+}
diff --git a/core/java/android/widget/RemoteViews.aidl b/core/java/android/widget/RemoteViews.aidl
index 6a5fc03..19a5f25 100644
--- a/core/java/android/widget/RemoteViews.aidl
+++ b/core/java/android/widget/RemoteViews.aidl
@@ -18,3 +18,4 @@
parcelable RemoteViews;
parcelable RemoteViews.RemoteCollectionItems;
+parcelable RemoteViews.DrawInstructions;
diff --git a/core/java/android/widget/RemoteViews.java b/core/java/android/widget/RemoteViews.java
index 0d499a1..0654add 100644
--- a/core/java/android/widget/RemoteViews.java
+++ b/core/java/android/widget/RemoteViews.java
@@ -16,6 +16,8 @@
package android.widget;
+import static android.appwidget.flags.Flags.FLAG_DRAW_DATA_PARCEL;
+import static android.appwidget.flags.Flags.drawDataParcel;
import static android.appwidget.flags.Flags.remoteAdapterConversion;
import static android.view.inputmethod.Flags.FLAG_HOME_SCREEN_HANDWRITING_DELEGATOR;
@@ -243,6 +245,7 @@
private static final int ATTRIBUTE_REFLECTION_ACTION_TAG = 32;
private static final int SET_REMOTE_ADAPTER_TAG = 33;
private static final int SET_ON_STYLUS_HANDWRITING_RESPONSE_TAG = 34;
+ private static final int SET_DRAW_INSTRUCTION_TAG = 35;
/** @hide **/
@IntDef(prefix = "MARGIN_", value = {
@@ -442,6 +445,19 @@
@Nullable
private LayoutInflater.Factory2 mLayoutInflaterFactory2;
+ /**
+ * Indicates whether this {@link RemoteViews} was instantiated with a {@link DrawInstructions}
+ * object. {@link DrawInstructions} serves as an alternative protocol for the host process
+ * to render.
+ */
+ private boolean mHasDrawInstructions;
+
+ @Nullable
+ private SparseArray<PendingIntent> mPendingIntentTemplate;
+
+ @Nullable
+ private SparseArray<Intent> mFillInIntent;
+
private static final InteractionHandler DEFAULT_INTERACTION_HANDLER =
(view, pendingIntent, response) ->
startPendingIntent(view, pendingIntent, response.getLaunchOptions(view));
@@ -1463,6 +1479,11 @@
@Override
public void apply(View root, ViewGroup rootParent, ActionApplyParams params) {
+ if (hasDrawInstructions() && root instanceof RemoteCanvas target) {
+ target.addOnClickHandler(mViewId, () ->
+ mResponse.handleViewInteraction(root, params.handler));
+ return;
+ }
final View target = root.findViewById(mViewId);
if (target == null) return;
@@ -3851,6 +3872,45 @@
}
}
+ private static class SetDrawInstructionAction extends Action {
+
+ @Nullable
+ private final DrawInstructions mInstructions;
+
+ SetDrawInstructionAction(@NonNull final DrawInstructions instructions) {
+ mInstructions = instructions;
+ }
+
+ SetDrawInstructionAction(@NonNull final Parcel in) {
+ if (drawDataParcel()) {
+ mInstructions = DrawInstructions.readFromParcel(in);
+ } else {
+ mInstructions = null;
+ }
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ if (drawDataParcel()) {
+ DrawInstructions.writeToParcel(mInstructions, dest, flags);
+ }
+ }
+
+ @Override
+ public void apply(View root, ViewGroup rootParent, ActionApplyParams params)
+ throws ActionException {
+ if (drawDataParcel() && mInstructions != null
+ && root instanceof RemoteCanvas remoteCanvas) {
+ remoteCanvas.setDrawInstructions(mInstructions);
+ }
+ }
+
+ @Override
+ public int getActionTag() {
+ return SET_DRAW_INSTRUCTION_TAG;
+ }
+ }
+
/**
* Create a new RemoteViews object that will display the views contained
* in the specified layout file.
@@ -4080,6 +4140,7 @@
mClassCookies = src.mClassCookies;
mIdealSize = src.mIdealSize;
mProviderInstanceId = src.mProviderInstanceId;
+ mHasDrawInstructions = src.mHasDrawInstructions;
if (src.hasLandscapeAndPortraitLayouts()) {
mLandscape = createInitializedFrom(src.mLandscape, hierarchyRoot);
@@ -4114,12 +4175,26 @@
/**
* Reads a RemoteViews object from a parcel.
*
- * @param parcel
+ * @param parcel the parcel object
*/
public RemoteViews(Parcel parcel) {
this(parcel, /* rootData= */ null, /* info= */ null, /* depth= */ 0);
}
+ /**
+ * Instantiates a RemoteViews object using {@link DrawInstructions}, which serves as an
+ * alternative to XML layout. {@link DrawInstructions} objects contains the instructions which
+ * can be interpreted and rendered accordingly in the host process.
+ *
+ * @param drawInstructions The {@link DrawInstructions} object
+ */
+ @FlaggedApi(FLAG_DRAW_DATA_PARCEL)
+ public RemoteViews(@NonNull final DrawInstructions drawInstructions) {
+ Objects.requireNonNull(drawInstructions);
+ mHasDrawInstructions = true;
+ addAction(new SetDrawInstructionAction(drawInstructions));
+ }
+
private RemoteViews(@NonNull Parcel parcel, @Nullable HierarchyRootData rootData,
@Nullable ApplicationInfo info, int depth) {
if (depth > MAX_NESTED_VIEWS
@@ -4178,6 +4253,7 @@
}
mApplyFlags = parcel.readInt();
mProviderInstanceId = parcel.readLong();
+ mHasDrawInstructions = parcel.readBoolean();
// Ensure that all descendants have their caches set up recursively.
if (mIsRoot) {
@@ -4254,6 +4330,8 @@
return new AttributeReflectionAction(parcel);
case SET_ON_STYLUS_HANDWRITING_RESPONSE_TAG:
return new SetOnStylusHandwritingResponse(parcel);
+ case SET_DRAW_INSTRUCTION_TAG:
+ return new SetDrawInstructionAction(parcel);
default:
throw new ActionException("Tag " + tag + " not found");
}
@@ -4747,7 +4825,12 @@
* by a child of viewId and executed when that child is clicked
*/
public void setPendingIntentTemplate(@IdRes int viewId, PendingIntent pendingIntentTemplate) {
- addAction(new SetPendingIntentTemplate(viewId, pendingIntentTemplate));
+ if (hasDrawInstructions()) {
+ getPendingIntentTemplate().set(viewId, pendingIntentTemplate);
+ tryAddRemoteResponse(viewId);
+ } else {
+ addAction(new SetPendingIntentTemplate(viewId, pendingIntentTemplate));
+ }
}
/**
@@ -4768,7 +4851,12 @@
* in order to determine the on-click behavior of the view specified by viewId
*/
public void setOnClickFillInIntent(@IdRes int viewId, Intent fillInIntent) {
- setOnClickResponse(viewId, RemoteResponse.fromFillInIntent(fillInIntent));
+ if (hasDrawInstructions()) {
+ getFillInIntent().set(viewId, fillInIntent);
+ tryAddRemoteResponse(viewId);
+ } else {
+ setOnClickResponse(viewId, RemoteResponse.fromFillInIntent(fillInIntent));
+ }
}
/**
@@ -5791,6 +5879,10 @@
}
}
+ private boolean hasDrawInstructions() {
+ return mHasDrawInstructions;
+ }
+
private RemoteViews getRemoteViewsToApply(Context context) {
if (hasLandscapeAndPortraitLayouts()) {
int orientation = context.getResources().getConfiguration().orientation;
@@ -5973,6 +6065,10 @@
if (applyThemeResId != 0) {
inflationContext = new ContextThemeWrapper(inflationContext, applyThemeResId);
}
+ // If the RemoteViews contains draw instructions, just use it instead.
+ if (rv.hasDrawInstructions()) {
+ return new RemoteCanvas(inflationContext);
+ }
LayoutInflater inflater = LayoutInflater.from(context);
// Clone inflater so we load resources from correct context and
@@ -6236,7 +6332,7 @@
/** @hide */
public boolean canRecycleView(@Nullable View v) {
- if (v == null) {
+ if (v == null || hasDrawInstructions()) {
return false;
}
Integer previousLayoutId = (Integer) v.getTag(R.id.widget_frame);
@@ -6388,6 +6484,32 @@
return context;
}
+ @NonNull
+ private SparseArray<PendingIntent> getPendingIntentTemplate() {
+ if (mPendingIntentTemplate == null) {
+ mPendingIntentTemplate = new SparseArray<>();
+ }
+ return mPendingIntentTemplate;
+ }
+
+ @NonNull
+ private SparseArray<Intent> getFillInIntent() {
+ if (mFillInIntent == null) {
+ mFillInIntent = new SparseArray<>();
+ }
+ return mFillInIntent;
+ }
+
+ private void tryAddRemoteResponse(final int viewId) {
+ final PendingIntent pendingIntent = getPendingIntentTemplate().get(viewId);
+ final Intent intent = getFillInIntent().get(viewId);
+ if (pendingIntent != null && intent != null) {
+ addAction(new SetOnClickResponse(viewId,
+ RemoteResponse.fromPendingIntentTemplateAndFillInIntent(
+ pendingIntent, intent)));
+ }
+ }
+
/**
* Utility class to hold all the options when applying the remote views
* @hide
@@ -6624,6 +6746,7 @@
}
dest.writeInt(mApplyFlags);
dest.writeLong(mProviderInstanceId);
+ dest.writeBoolean(mHasDrawInstructions);
dest.restoreAllowSquashing(prevSquashingAllowed);
}
@@ -6926,6 +7049,14 @@
return response;
}
+ private static RemoteResponse fromPendingIntentTemplateAndFillInIntent(
+ @NonNull final PendingIntent pendingIntent, @NonNull final Intent intent) {
+ RemoteResponse response = new RemoteResponse();
+ response.mPendingIntent = pendingIntent;
+ response.mFillIntent = intent;
+ return response;
+ }
+
/**
* Adds a shared element to be transferred as part of the transition between Activities
* using cross-Activity scene animations. The position of the first element will be used as
@@ -6964,8 +7095,8 @@
private void writeToParcel(Parcel dest, int flags) {
PendingIntent.writePendingIntentOrNullToParcel(mPendingIntent, dest);
- if (mPendingIntent == null) {
- // Only write the intent if pending intent is null
+ dest.writeBoolean((mFillIntent != null));
+ if (mFillIntent != null) {
dest.writeTypedObject(mFillIntent, flags);
}
dest.writeInt(mInteractionType);
@@ -6975,9 +7106,7 @@
private void readFromParcel(Parcel parcel) {
mPendingIntent = PendingIntent.readPendingIntentOrNullFromParcel(parcel);
- if (mPendingIntent == null) {
- mFillIntent = parcel.readTypedObject(Intent.CREATOR);
- }
+ mFillIntent = parcel.readBoolean() ? parcel.readTypedObject(Intent.CREATOR) : null;
mInteractionType = parcel.readInt();
int[] viewIds = parcel.createIntArray();
mViewIds = viewIds == null ? null : IntArray.wrap(viewIds);
@@ -7054,7 +7183,7 @@
/** @hide */
public Pair<Intent, ActivityOptions> getLaunchOptions(View view) {
- Intent intent = mPendingIntent != null ? new Intent() : new Intent(mFillIntent);
+ Intent intent = mFillIntent == null ? new Intent() : new Intent(mFillIntent);
intent.setSourceBounds(getSourceBounds(view));
if (view instanceof CompoundButton
@@ -7413,6 +7542,98 @@
}
/**
+ * A data parcel that carries the instructions to draw the RemoteViews, as an alternative to
+ * XML layout.
+ */
+ @FlaggedApi(FLAG_DRAW_DATA_PARCEL)
+ public static final class DrawInstructions {
+
+ @NonNull
+ private final List<byte[]> mInstructions;
+
+ private DrawInstructions() {
+ throw new UnsupportedOperationException(
+ "DrawInstructions cannot be instantiate without instructions");
+ }
+
+ private DrawInstructions(@NonNull List<byte[]> instructions) {
+ // Create and retain an immutable copy of given instructions.
+ mInstructions = new ArrayList<>(instructions.size());
+ for (byte[] instruction : instructions) {
+ final int len = instruction.length;
+ final byte[] target = new byte[len];
+ System.arraycopy(instruction, 0, target, 0, len);
+ mInstructions.add(target);
+ }
+ }
+
+ @Nullable
+ private static DrawInstructions readFromParcel(@NonNull final Parcel in) {
+ int size = in.readInt();
+ if (size == -1) {
+ return null;
+ }
+ byte[] instruction;
+ final List<byte[]> instructions = new ArrayList<>(size);
+ for (int i = 0; i < size; i++) {
+ instruction = new byte[in.readInt()];
+ in.readByteArray(instruction);
+ instructions.add(instruction);
+ }
+ return new DrawInstructions(instructions);
+ }
+ private static void writeToParcel(@Nullable final DrawInstructions drawInstructions,
+ @NonNull final Parcel dest, final int flags) {
+ if (drawInstructions == null) {
+ dest.writeInt(-1);
+ return;
+ }
+ final List<byte[]> instructions = drawInstructions.mInstructions;
+ dest.writeInt(instructions.size());
+ for (byte[] instruction : instructions) {
+ dest.writeInt(instruction.length);
+ dest.writeByteArray(instruction);
+ }
+ }
+
+ /**
+ * Append additional instructions to this {@link DrawInstructions} object.
+ */
+ @FlaggedApi(FLAG_DRAW_DATA_PARCEL)
+ public void appendInstructions(@NonNull final byte[] instructions) {
+ mInstructions.add(instructions);
+ }
+
+ /**
+ * Builder class for {@link DrawInstructions} objects.
+ */
+ @FlaggedApi(FLAG_DRAW_DATA_PARCEL)
+ public static final class Builder {
+
+ private final List<byte[]> mInstructions;
+
+ /**
+ * Constructor.
+ *
+ * @param instructions Information to draw the RemoteViews.
+ */
+ @FlaggedApi(FLAG_DRAW_DATA_PARCEL)
+ public Builder(@NonNull final List<byte[]> instructions) {
+ mInstructions = new ArrayList<>(instructions);
+ }
+
+ /**
+ * Creates a {@link DrawInstructions} instance.
+ */
+ @NonNull
+ @FlaggedApi(FLAG_DRAW_DATA_PARCEL)
+ public DrawInstructions build() {
+ return new DrawInstructions(mInstructions);
+ }
+ }
+ }
+
+ /**
* Get the ID of the top-level view of the XML layout, if set using
* {@link RemoteViews#RemoteViews(String, int, int)}.
*/
diff --git a/core/java/android/window/TaskFragmentCreationParams.java b/core/java/android/window/TaskFragmentCreationParams.java
index 5dbf328..93297e6 100644
--- a/core/java/android/window/TaskFragmentCreationParams.java
+++ b/core/java/android/window/TaskFragmentCreationParams.java
@@ -94,11 +94,18 @@
@Nullable
private final IBinder mPairedActivityToken;
+ /**
+ * If {@code true}, transitions are allowed even if the TaskFragment is empty. If
+ * {@code false}, transitions will wait until the TaskFragment becomes non-empty or other
+ * conditions are met. Default to {@code false}.
+ */
+ private final boolean mAllowTransitionWhenEmpty;
+
private TaskFragmentCreationParams(
@NonNull TaskFragmentOrganizerToken organizer, @NonNull IBinder fragmentToken,
@NonNull IBinder ownerToken, @NonNull Rect initialRelativeBounds,
@WindowingMode int windowingMode, @Nullable IBinder pairedPrimaryFragmentToken,
- @Nullable IBinder pairedActivityToken) {
+ @Nullable IBinder pairedActivityToken, boolean allowTransitionWhenEmpty) {
if (pairedPrimaryFragmentToken != null && pairedActivityToken != null) {
throw new IllegalArgumentException("pairedPrimaryFragmentToken and"
+ " pairedActivityToken should not be set at the same time.");
@@ -110,6 +117,7 @@
mWindowingMode = windowingMode;
mPairedPrimaryFragmentToken = pairedPrimaryFragmentToken;
mPairedActivityToken = pairedActivityToken;
+ mAllowTransitionWhenEmpty = allowTransitionWhenEmpty;
}
@NonNull
@@ -155,6 +163,11 @@
return mPairedActivityToken;
}
+ /** @hide */
+ public boolean getAllowTransitionWhenEmpty() {
+ return mAllowTransitionWhenEmpty;
+ }
+
private TaskFragmentCreationParams(Parcel in) {
mOrganizer = TaskFragmentOrganizerToken.CREATOR.createFromParcel(in);
mFragmentToken = in.readStrongBinder();
@@ -163,6 +176,7 @@
mWindowingMode = in.readInt();
mPairedPrimaryFragmentToken = in.readStrongBinder();
mPairedActivityToken = in.readStrongBinder();
+ mAllowTransitionWhenEmpty = in.readBoolean();
}
/** @hide */
@@ -175,6 +189,7 @@
dest.writeInt(mWindowingMode);
dest.writeStrongBinder(mPairedPrimaryFragmentToken);
dest.writeStrongBinder(mPairedActivityToken);
+ dest.writeBoolean(mAllowTransitionWhenEmpty);
}
@NonNull
@@ -201,6 +216,7 @@
+ " windowingMode=" + mWindowingMode
+ " pairedFragmentToken=" + mPairedPrimaryFragmentToken
+ " pairedActivityToken=" + mPairedActivityToken
+ + " allowTransitionWhenEmpty=" + mAllowTransitionWhenEmpty
+ "}";
}
@@ -234,6 +250,8 @@
@Nullable
private IBinder mPairedActivityToken;
+ private boolean mAllowTransitionWhenEmpty;
+
public Builder(@NonNull TaskFragmentOrganizerToken organizer,
@NonNull IBinder fragmentToken, @NonNull IBinder ownerToken) {
mOrganizer = organizer;
@@ -298,12 +316,26 @@
return this;
}
+ /**
+ * Sets whether transitions are allowed when the TaskFragment is empty. If {@code true},
+ * transitions are allowed when the TaskFragment is empty. If {@code false}, transitions
+ * will wait until the TaskFragment becomes non-empty or other conditions are met. Default
+ * to {@code false}.
+ *
+ * @hide
+ */
+ @NonNull
+ public Builder setAllowTransitionWhenEmpty(boolean allowTransitionWhenEmpty) {
+ mAllowTransitionWhenEmpty = allowTransitionWhenEmpty;
+ return this;
+ }
+
/** Constructs the options to create TaskFragment with. */
@NonNull
public TaskFragmentCreationParams build() {
return new TaskFragmentCreationParams(mOrganizer, mFragmentToken, mOwnerToken,
mInitialRelativeBounds, mWindowingMode, mPairedPrimaryFragmentToken,
- mPairedActivityToken);
+ mPairedActivityToken, mAllowTransitionWhenEmpty);
}
}
}
diff --git a/core/java/android/window/WindowTokenClient.java b/core/java/android/window/WindowTokenClient.java
index c20b278..7f5331b 100644
--- a/core/java/android/window/WindowTokenClient.java
+++ b/core/java/android/window/WindowTokenClient.java
@@ -167,6 +167,11 @@
+ ", reported config=" + currentConfig
+ ", updated config=" + newConfig);
}
+ // Update display first. In case callers want to obtain display information(
+ // ex: DisplayMetrics) in #onConfigurationChanged callback.
+ if (displayChanged) {
+ context.updateDisplay(newDisplayId);
+ }
if (shouldUpdateResources) {
// TODO(ag/9789103): update resource manager logic to track non-activity tokens
mResourcesManager.updateResourcesForActivity(this, newConfig, newDisplayId);
@@ -195,9 +200,6 @@
}
}
}
- if (displayChanged) {
- context.updateDisplay(newDisplayId);
- }
}
/**
diff --git a/core/java/android/window/flags/large_screen_experiences_app_compat.aconfig b/core/java/android/window/flags/large_screen_experiences_app_compat.aconfig
index cbf6367..1de77f6 100644
--- a/core/java/android/window/flags/large_screen_experiences_app_compat.aconfig
+++ b/core/java/android/window/flags/large_screen_experiences_app_compat.aconfig
@@ -50,3 +50,19 @@
description: "Whether we should allow hiding the size compat restart button"
bug: "318840081"
}
+
+flag {
+ name: "configurable_font_scale_default"
+ namespace: "large_screen_experiences_app_compat"
+ description: "Whether the font_scale is read from a device dependent configuration file"
+ bug: "319808237"
+ is_fixed_read_only: true
+}
+
+flag {
+ name: "camera_compat_for_freeform"
+ namespace: "large_screen_experiences_app_compat"
+ description: "Whether to apply Camera Compat treatment to fixed-orientation apps in freeform windowing mode"
+ bug: "314952133"
+ is_fixed_read_only: true
+}
diff --git a/core/java/android/window/flags/windowing_frontend.aconfig b/core/java/android/window/flags/windowing_frontend.aconfig
index 2c5fbd7..f234637 100644
--- a/core/java/android/window/flags/windowing_frontend.aconfig
+++ b/core/java/android/window/flags/windowing_frontend.aconfig
@@ -38,14 +38,6 @@
}
flag {
- name: "draw_magnifier_border_outside_wmlock"
- namespace: "windowing_frontend"
- description: "Avoid holding WM locks for a long time when executing lockCanvas"
- bug: "316075123"
- is_fixed_read_only: true
-}
-
-flag {
name: "introduce_smoother_dimmer"
namespace: "windowing_frontend"
description: "Refactor dim to fix flickers"
diff --git a/core/java/com/android/internal/app/ConfirmUserCreationActivity.java b/core/java/com/android/internal/app/ConfirmUserCreationActivity.java
index 0a28997..b4e8749 100644
--- a/core/java/com/android/internal/app/ConfirmUserCreationActivity.java
+++ b/core/java/com/android/internal/app/ConfirmUserCreationActivity.java
@@ -116,6 +116,14 @@
if (cantCreateUser) {
setResult(UserManager.USER_CREATION_FAILED_NOT_PERMITTED);
return null;
+ } else if (!(isUserPropertyWithinLimit(mUserName, UserManager.MAX_USER_NAME_LENGTH)
+ && isUserPropertyWithinLimit(mAccountName, UserManager.MAX_ACCOUNT_STRING_LENGTH)
+ && isUserPropertyWithinLimit(mAccountType, UserManager.MAX_ACCOUNT_STRING_LENGTH))
+ || (mAccountOptions != null && !mAccountOptions.isBundleContentsWithinLengthLimit(
+ UserManager.MAX_ACCOUNT_OPTIONS_LENGTH))) {
+ setResult(UserManager.USER_CREATION_FAILED_NOT_PERMITTED);
+ Log.i(TAG, "User properties must not exceed their character limits");
+ return null;
} else if (cantCreateAnyMoreUsers) {
setResult(UserManager.USER_CREATION_FAILED_NO_MORE_USERS);
return null;
@@ -144,4 +152,8 @@
}
finish();
}
+
+ private boolean isUserPropertyWithinLimit(String property, int limit) {
+ return property == null || property.length() <= limit;
+ }
}
diff --git a/core/java/com/android/internal/config/sysui/SystemUiSystemPropertiesFlags.java b/core/java/com/android/internal/config/sysui/SystemUiSystemPropertiesFlags.java
index eeea17b..90ca95a 100644
--- a/core/java/com/android/internal/config/sysui/SystemUiSystemPropertiesFlags.java
+++ b/core/java/com/android/internal/config/sysui/SystemUiSystemPropertiesFlags.java
@@ -71,7 +71,7 @@
"persist.debug.sysui.notification.notif_cooldown_t1", 60000);
/** Value used by polite notif. feature */
public static final Flag NOTIF_COOLDOWN_T2 = devFlag(
- "persist.debug.sysui.notification.notif_cooldown_t2", 5000);
+ "persist.debug.sysui.notification.notif_cooldown_t2", 10000);
/** Value used by polite notif. feature */
public static final Flag NOTIF_VOLUME1 = devFlag(
"persist.debug.sysui.notification.notif_volume1", 30);
@@ -81,6 +81,10 @@
public static final Flag NOTIF_COOLDOWN_COUNTER_RESET = devFlag(
"persist.debug.sysui.notification.notif_cooldown_counter_reset", 10);
+ /** Value used by polite notif. feature */
+ public static final Flag NOTIF_AVALANCHE_TIMEOUT = devFlag(
+ "persist.debug.sysui.notification.notif_avalanche_timeout", 120_000);
+
/** b/303716154: For debugging only: use short bitmap duration. */
public static final Flag DEBUG_SHORT_BITMAP_DURATION = devFlag(
"persist.sysui.notification.debug_short_bitmap_duration");
diff --git a/core/java/com/android/internal/display/BrightnessSynchronizer.java b/core/java/com/android/internal/display/BrightnessSynchronizer.java
index 37aaa72..0068490 100644
--- a/core/java/com/android/internal/display/BrightnessSynchronizer.java
+++ b/core/java/com/android/internal/display/BrightnessSynchronizer.java
@@ -47,6 +47,7 @@
* (new) system for storing the brightness. It has methods to convert between the two and also
* observes for when one of the settings is changed and syncs this with the other.
*/
+@android.ravenwood.annotation.RavenwoodKeepPartialClass
public class BrightnessSynchronizer {
private static final String TAG = "BrightnessSynchronizer";
@@ -282,6 +283,7 @@
* @param b second float to compare
* @return whether the two values are within a small enough tolerance value
*/
+ @android.ravenwood.annotation.RavenwoodKeep
public static boolean floatEquals(float a, float b) {
if (a == b) {
return true;
diff --git a/core/java/com/android/internal/jank/Cuj.java b/core/java/com/android/internal/jank/Cuj.java
index 96740c5..7b3565b 100644
--- a/core/java/com/android/internal/jank/Cuj.java
+++ b/core/java/com/android/internal/jank/Cuj.java
@@ -121,10 +121,11 @@
public static final int CUJ_PREDICTIVE_BACK_CROSS_TASK = 85;
public static final int CUJ_PREDICTIVE_BACK_HOME = 86;
public static final int CUJ_LAUNCHER_SEARCH_QSB_OPEN = 87;
+ public static final int CUJ_BACK_PANEL_ARROW = 88;
// When adding a CUJ, update this and make sure to also update CUJ_TO_STATSD_INTERACTION_TYPE.
@VisibleForTesting
- static final int LAST_CUJ = CUJ_LAUNCHER_SEARCH_QSB_OPEN;
+ static final int LAST_CUJ = CUJ_BACK_PANEL_ARROW;
/** @hide */
@IntDef({
@@ -207,6 +208,7 @@
CUJ_PREDICTIVE_BACK_CROSS_TASK,
CUJ_PREDICTIVE_BACK_HOME,
CUJ_LAUNCHER_SEARCH_QSB_OPEN,
+ CUJ_BACK_PANEL_ARROW,
})
@Retention(RetentionPolicy.SOURCE)
public @interface CujType {
@@ -298,8 +300,8 @@
CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_PREDICTIVE_BACK_CROSS_ACTIVITY] = FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__PREDICTIVE_BACK_CROSS_ACTIVITY;
CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_PREDICTIVE_BACK_CROSS_TASK] = FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__PREDICTIVE_BACK_CROSS_TASK;
CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_PREDICTIVE_BACK_HOME] = FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__PREDICTIVE_BACK_HOME;
- CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_LAUNCHER_SEARCH_QSB_OPEN] =
- FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LAUNCHER_SEARCH_QSB_OPEN;
+ CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_LAUNCHER_SEARCH_QSB_OPEN] = FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LAUNCHER_SEARCH_QSB_OPEN;
+ CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_BACK_PANEL_ARROW] = FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__BACK_PANEL_ARROW;
}
private Cuj() {
@@ -474,6 +476,8 @@
return "PREDICTIVE_BACK_HOME";
case CUJ_LAUNCHER_SEARCH_QSB_OPEN:
return "LAUNCHER_SEARCH_QSB_OPEN";
+ case CUJ_BACK_PANEL_ARROW:
+ return "BACK_PANEL_ARROW";
}
return "UNKNOWN";
}
diff --git a/core/java/com/android/internal/jank/InteractionMonitorDebugOverlay.java b/core/java/com/android/internal/jank/InteractionMonitorDebugOverlay.java
index f3f16a0..d9cac12 100644
--- a/core/java/com/android/internal/jank/InteractionMonitorDebugOverlay.java
+++ b/core/java/com/android/internal/jank/InteractionMonitorDebugOverlay.java
@@ -28,6 +28,7 @@
import android.graphics.Rect;
import android.os.Handler;
import android.os.Trace;
+import android.util.Log;
import android.util.SparseArray;
import android.util.SparseIntArray;
import android.view.WindowCallbacks;
@@ -52,6 +53,7 @@
* @hide
*/
class InteractionMonitorDebugOverlay implements WindowCallbacks {
+ private static final String TAG = "InteractionMonitorDebug";
private static final int REASON_STILL_RUNNING = -1000;
private final Object mLock;
// Sparse array where the key in the CUJ and the value is the session status, or null if
@@ -77,7 +79,7 @@
mDebugPaint.setAntiAlias(false);
mDebugFontMetrics = new Paint.FontMetrics();
final Context context = ActivityThread.currentApplication();
- mPackageName = context.getPackageName();
+ mPackageName = context == null ? "null" : context.getPackageName();
}
@UiThread
@@ -153,8 +155,14 @@
SparseArray<InteractionJankMonitor.RunningTracker> runningTrackers) {
synchronized (mLock) {
mRunningCujs.put(removedCuj, reason);
+ boolean isLoggable = Log.isLoggable(TAG, Log.DEBUG);
+ if (isLoggable) {
+ String cujName = Cuj.getNameOfCuj(removedCuj);
+ Log.d(TAG, cujName + (reason == REASON_END_NORMAL ? " ended" : " cancelled"));
+ }
// If REASON_STILL_RUNNING is not in mRunningCujs, then all CUJs have ended
if (mRunningCujs.indexOfValue(REASON_STILL_RUNNING) < 0) {
+ if (isLoggable) Log.d(TAG, "All CUJs ended");
mRunningCujs.clear();
dispose();
} else {
@@ -186,6 +194,10 @@
@UiThread
void onTrackerAdded(@Cuj.CujType int addedCuj, InteractionJankMonitor.RunningTracker tracker) {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ String cujName = Cuj.getNameOfCuj(addedCuj);
+ Log.d(TAG, cujName + " started");
+ }
synchronized (mLock) {
// Use REASON_STILL_RUNNING (not technically one of the '@Reasons') to indicate the CUJ
// is still running
diff --git a/core/java/com/android/internal/logging/MetricsLogger.java b/core/java/com/android/internal/logging/MetricsLogger.java
index e58f4f0..88aa89a 100644
--- a/core/java/com/android/internal/logging/MetricsLogger.java
+++ b/core/java/com/android/internal/logging/MetricsLogger.java
@@ -34,6 +34,7 @@
*
* @hide
*/
+@android.ravenwood.annotation.RavenwoodKeepWholeClass
public class MetricsLogger {
// define metric categories in frameworks/base/proto/src/metrics_constants.proto.
// mirror changes in native version at system/core/libmetricslogger/metrics_logger.cpp
diff --git a/core/java/com/android/internal/logging/testing/FakeMetricsLogger.java b/core/java/com/android/internal/logging/testing/FakeMetricsLogger.java
index 6786427..df8bf31 100644
--- a/core/java/com/android/internal/logging/testing/FakeMetricsLogger.java
+++ b/core/java/com/android/internal/logging/testing/FakeMetricsLogger.java
@@ -12,6 +12,7 @@
*
* @hide.
*/
+@android.ravenwood.annotation.RavenwoodKeepWholeClass
public class FakeMetricsLogger extends MetricsLogger {
private Queue<LogMaker> logs = new LinkedList<>();
diff --git a/core/java/com/android/internal/logging/testing/UiEventLoggerFake.java b/core/java/com/android/internal/logging/testing/UiEventLoggerFake.java
index e303890..6787ddc 100644
--- a/core/java/com/android/internal/logging/testing/UiEventLoggerFake.java
+++ b/core/java/com/android/internal/logging/testing/UiEventLoggerFake.java
@@ -27,6 +27,7 @@
*
* @hide.
*/
+@android.ravenwood.annotation.RavenwoodKeepWholeClass
public class UiEventLoggerFake implements UiEventLogger {
/**
* Immutable data class used to record fake log events.
diff --git a/core/java/com/android/internal/pm/pkg/component/ParsedProviderUtils.java b/core/java/com/android/internal/pm/pkg/component/ParsedProviderUtils.java
index 5d82d04..12aff1c 100644
--- a/core/java/com/android/internal/pm/pkg/component/ParsedProviderUtils.java
+++ b/core/java/com/android/internal/pm/pkg/component/ParsedProviderUtils.java
@@ -29,6 +29,7 @@
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.content.res.XmlResourceParser;
+import android.multiuser.Flags;
import android.os.Build;
import android.os.PatternMatcher;
import android.util.Slog;
@@ -126,6 +127,10 @@
.setFlags(provider.getFlags() | flag(ProviderInfo.FLAG_SINGLE_USER,
R.styleable.AndroidManifestProvider_singleUser, sa));
+ if (Flags.enableSystemUserOnlyForServicesAndProviders()) {
+ provider.setFlags(provider.getFlags() | flag(ProviderInfo.FLAG_SYSTEM_USER_ONLY,
+ R.styleable.AndroidManifestProvider_systemUserOnly, sa));
+ }
visibleToEphemeral = sa.getBoolean(
R.styleable.AndroidManifestProvider_visibleToInstantApps, false);
if (visibleToEphemeral) {
diff --git a/core/java/com/android/internal/pm/pkg/component/ParsedServiceUtils.java b/core/java/com/android/internal/pm/pkg/component/ParsedServiceUtils.java
index a1dd19a3..4ac542f8 100644
--- a/core/java/com/android/internal/pm/pkg/component/ParsedServiceUtils.java
+++ b/core/java/com/android/internal/pm/pkg/component/ParsedServiceUtils.java
@@ -29,6 +29,7 @@
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.content.res.XmlResourceParser;
+import android.multiuser.Flags;
import android.os.Build;
import com.android.internal.R;
@@ -105,6 +106,11 @@
| flag(ServiceInfo.FLAG_SINGLE_USER,
R.styleable.AndroidManifestService_singleUser, sa)));
+ if (Flags.enableSystemUserOnlyForServicesAndProviders()) {
+ service.setFlags(service.getFlags() | flag(ServiceInfo.FLAG_SYSTEM_USER_ONLY,
+ R.styleable.AndroidManifestService_systemUserOnly, sa));
+ }
+
visibleToEphemeral = sa.getBoolean(
R.styleable.AndroidManifestService_visibleToInstantApps, false);
if (visibleToEphemeral) {
diff --git a/core/java/com/android/internal/policy/SystemBarUtils.java b/core/java/com/android/internal/policy/SystemBarUtils.java
index 7a1ac07..efa3697 100644
--- a/core/java/com/android/internal/policy/SystemBarUtils.java
+++ b/core/java/com/android/internal/policy/SystemBarUtils.java
@@ -19,8 +19,9 @@
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Insets;
-import android.util.RotationUtils;
+import android.view.Display;
import android.view.DisplayCutout;
+import android.view.DisplayInfo;
import android.view.Surface;
import com.android.internal.R;
@@ -56,21 +57,21 @@
*/
public static int getStatusBarHeightForRotation(
Context context, @Surface.Rotation int targetRot) {
- final int rotation = context.getDisplay().getRotation();
- final DisplayCutout cutout = context.getDisplay().getCutout();
-
- Insets insets = cutout == null ? Insets.NONE : Insets.of(cutout.getSafeInsets());
- Insets waterfallInsets = cutout == null ? Insets.NONE : cutout.getWaterfallInsets();
- // rotate insets to target rotation if needed.
- if (rotation != targetRot) {
- if (!insets.equals(Insets.NONE)) {
- insets = RotationUtils.rotateInsets(
- insets, RotationUtils.deltaRotation(rotation, targetRot));
- }
- if (!waterfallInsets.equals(Insets.NONE)) {
- waterfallInsets = RotationUtils.rotateInsets(
- waterfallInsets, RotationUtils.deltaRotation(rotation, targetRot));
- }
+ final Display display = context.getDisplay();
+ final int rotation = display.getRotation();
+ final DisplayCutout cutout = display.getCutout();
+ DisplayInfo info = new DisplayInfo();
+ display.getDisplayInfo(info);
+ Insets insets;
+ Insets waterfallInsets;
+ if (cutout == null) {
+ insets = Insets.NONE;
+ waterfallInsets = Insets.NONE;
+ } else {
+ DisplayCutout rotated =
+ cutout.getRotated(info.logicalWidth, info.logicalHeight, rotation, targetRot);
+ insets = Insets.of(rotated.getSafeInsets());
+ waterfallInsets = rotated.getWaterfallInsets();
}
final int defaultSize =
context.getResources().getDimensionPixelSize(R.dimen.status_bar_height_default);
diff --git a/core/java/com/android/internal/widget/ConversationLayout.java b/core/java/com/android/internal/widget/ConversationLayout.java
index 42be784..a8d0d37 100644
--- a/core/java/com/android/internal/widget/ConversationLayout.java
+++ b/core/java/com/android/internal/widget/ConversationLayout.java
@@ -105,6 +105,9 @@
private int mConversationIconTopPaddingExpandedGroup;
private int mConversationIconTopPadding;
private int mExpandedGroupMessagePadding;
+ // TODO (b/217799515) Currently, mConversationText shows the conversation title, the actual
+ // conversation text is inside of mMessagingLinearLayout, which is misleading, we should rename
+ // this to mConversationTitleView
private TextView mConversationText;
private View mConversationIconBadge;
private CachingIconView mConversationIconBadgeBg;
@@ -125,6 +128,11 @@
private int mNotificationBackgroundColor;
private CharSequence mFallbackChatName;
private CharSequence mFallbackGroupChatName;
+ //TODO (b/217799515) Currently, Notification.MessagingStyle, ConversationLayout, and
+ // HybridConversationNotificationView, each has their own definition of "ConversationTitle".
+ // What make things worse is that the term of "ConversationTitle" often confuses with
+ // "ConversationText".
+ // We need to unify them or differentiate the namings.
private CharSequence mConversationTitle;
private int mMessageSpacingStandard;
private int mMessageSpacingGroup;
@@ -160,12 +168,12 @@
}
public ConversationLayout(@NonNull Context context, @Nullable AttributeSet attrs,
- @AttrRes int defStyleAttr) {
+ @AttrRes int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public ConversationLayout(@NonNull Context context, @Nullable AttributeSet attrs,
- @AttrRes int defStyleAttr, @StyleRes int defStyleRes) {
+ @AttrRes int defStyleAttr, @StyleRes int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
@@ -297,13 +305,17 @@
mNameReplacement = nameReplacement;
}
- /** Sets this conversation as "important", adding some additional UI treatment. */
+ /**
+ * Sets this conversation as "important", adding some additional UI treatment.
+ */
@RemotableViewMethod
public void setIsImportantConversation(boolean isImportantConversation) {
setIsImportantConversation(isImportantConversation, false);
}
- /** @hide **/
+ /**
+ * @hide
+ **/
public void setIsImportantConversation(boolean isImportantConversation, boolean animate) {
mImportantConversation = isImportantConversation;
mImportanceRingView.setVisibility(isImportantConversation && mIcon.getVisibility() != GONE
@@ -386,6 +398,7 @@
/**
* Set conversation data
+ *
* @param extras Bundle contains conversation data
*/
@RemotableViewMethod(asyncImpl = "setDataAsync")
@@ -427,6 +440,7 @@
* RemotableViewMethod's asyncImpl of {@link #setData(Bundle)}.
* This should be called on a background thread, and returns a Runnable which is then must be
* called on the main thread to complete the operation and set text.
+ *
* @param extras Bundle contains conversation data
* @hide
*/
@@ -449,6 +463,7 @@
/**
* enable/disable precomputed text usage
+ *
* @hide
*/
public void setPrecomputedTextEnabled(boolean precomputedTextEnabled) {
@@ -466,7 +481,9 @@
mImageResolver = resolver;
}
- /** @hide */
+ /**
+ * @hide
+ */
public void setUnreadCount(int unreadCount) {
mExpandButton.setNumber(unreadCount);
}
@@ -795,6 +812,10 @@
mConversationTitle = conversationTitle != null ? conversationTitle.toString() : null;
}
+ // TODO (b/217799515) getConversationTitle is not consistent with setConversationTitle
+ // if you call getConversationTitle() immediately after setConversationTitle(), the result
+ // will not correctly reflect the new change without calling updateConversationLayout, for
+ // example.
public CharSequence getConversationTitle() {
return mConversationText.getText();
}
@@ -914,7 +935,7 @@
}
private void createGroupViews(List<List<MessagingMessage>> groups,
- List<Person> senders, boolean showSpinner) {
+ List<Person> senders, boolean showSpinner) {
mGroups.clear();
for (int groupIndex = 0; groupIndex < groups.size(); groupIndex++) {
List<MessagingMessage> group = groups.get(groupIndex);
@@ -963,8 +984,8 @@
}
private void findGroups(List<MessagingMessage> historicMessages,
- List<MessagingMessage> messages, List<List<MessagingMessage>> groups,
- List<Person> senders) {
+ List<MessagingMessage> messages, List<List<MessagingMessage>> groups,
+ List<Person> senders) {
CharSequence currentSenderKey = null;
List<MessagingMessage> currentGroup = null;
int histSize = historicMessages.size();
diff --git a/core/java/com/android/internal/widget/ILockSettingsStateListener.aidl b/core/java/com/android/internal/widget/ILockSettingsStateListener.aidl
new file mode 100644
index 0000000..25e3003
--- /dev/null
+++ b/core/java/com/android/internal/widget/ILockSettingsStateListener.aidl
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.widget;
+
+/**
+ * Callback interface between LockSettingService and other system services to be notified about the
+ * state of primary authentication (i.e. PIN/pattern/password).
+ * @hide
+ */
+oneway interface ILockSettingsStateListener {
+ /**
+ * Defines behavior in response to a successful authentication
+ * @param userId The user Id for the requested authentication
+ */
+ void onAuthenticationSucceeded(int userId);
+
+ /**
+ * Defines behavior in response to a failed authentication
+ * @param userId The user Id for the requested authentication
+ */
+ void onAuthenticationFailed(int userId);
+}
\ No newline at end of file
diff --git a/core/java/com/android/internal/widget/ImageFloatingTextView.java b/core/java/com/android/internal/widget/ImageFloatingTextView.java
index 0704cb8..5da6435 100644
--- a/core/java/com/android/internal/widget/ImageFloatingTextView.java
+++ b/core/java/com/android/internal/widget/ImageFloatingTextView.java
@@ -18,9 +18,11 @@
import android.annotation.Nullable;
import android.content.Context;
+import android.os.Build;
import android.os.Trace;
import android.text.BoringLayout;
import android.text.Layout;
+import android.text.PrecomputedText;
import android.text.StaticLayout;
import android.text.TextUtils;
import android.text.method.TransformationMethod;
@@ -48,6 +50,10 @@
private int mLayoutMaxLines = -1;
private int mImageEndMargin;
+ private int mStaticLayoutCreationCountInOnMeasure = 0;
+
+ private static final boolean TRACE_ONMEASURE = Build.isDebuggable();
+
public ImageFloatingTextView(Context context) {
this(context, null);
}
@@ -71,7 +77,10 @@
protected Layout makeSingleLayout(int wantWidth, BoringLayout.Metrics boring, int ellipsisWidth,
Layout.Alignment alignment, boolean shouldEllipsize,
TextUtils.TruncateAt effectiveEllipsize, boolean useSaved) {
- Trace.beginSection("ImageFloatingTextView#makeSingleLayout");
+ if (TRACE_ONMEASURE) {
+ Trace.beginSection("ImageFloatingTextView#makeSingleLayout");
+ mStaticLayoutCreationCountInOnMeasure++;
+ }
TransformationMethod transformationMethod = getTransformationMethod();
CharSequence text = getText();
if (transformationMethod != null) {
@@ -79,7 +88,7 @@
}
text = text == null ? "" : text;
StaticLayout.Builder builder = StaticLayout.Builder.obtain(text, 0, text.length(),
- getPaint(), wantWidth)
+ getPaint(), wantWidth)
.setAlignment(alignment)
.setTextDirection(getTextDirectionHeuristic())
.setLineSpacing(getLineSpacingExtra(), getLineSpacingMultiplier())
@@ -115,7 +124,10 @@
}
final StaticLayout result = builder.build();
- Trace.endSection();
+ if (TRACE_ONMEASURE) {
+ trackMaxLines();
+ Trace.endSection();
+ }
return result;
}
@@ -141,7 +153,10 @@
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- Trace.beginSection("ImageFloatingTextView#onMeasure");
+ if (TRACE_ONMEASURE) {
+ Trace.beginSection("ImageFloatingTextView#onMeasure");
+ }
+ mStaticLayoutCreationCountInOnMeasure = 0;
int availableHeight = MeasureSpec.getSize(heightMeasureSpec) - mPaddingTop - mPaddingBottom;
if (getLayout() != null && getLayout().getHeight() != availableHeight) {
// We've been measured before and the new size is different than before, lets make sure
@@ -168,7 +183,12 @@
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
}
- Trace.endSection();
+
+
+ if (TRACE_ONMEASURE) {
+ trackParameters();
+ Trace.endSection();
+ }
}
@Override
@@ -216,4 +236,37 @@
requestLayout();
}
}
+
+ private void trackParameters() {
+ if (!TRACE_ONMEASURE) {
+ return;
+ }
+ Trace.setCounter("ImageFloatingView#staticLayoutCreationCount",
+ mStaticLayoutCreationCountInOnMeasure);
+ Trace.setCounter("ImageFloatingView#isPrecomputedText",
+ isTextAPrecomputedText());
+ }
+ /**
+ * @return 1 if {@link TextView#getText()} is PrecomputedText, else 0
+ */
+ private int isTextAPrecomputedText() {
+ final CharSequence text = getText();
+ if (text == null) {
+ return 0;
+ }
+
+ if (text instanceof PrecomputedText) {
+ return 1;
+ }
+
+ return 0;
+ }
+
+ private void trackMaxLines() {
+ if (!TRACE_ONMEASURE) {
+ return;
+ }
+
+ Trace.setCounter("ImageFloatingView#layoutMaxLines", mLayoutMaxLines);
+ }
}
diff --git a/core/java/com/android/internal/widget/LockPatternUtils.java b/core/java/com/android/internal/widget/LockPatternUtils.java
index 757978b..b5b3a48 100644
--- a/core/java/com/android/internal/widget/LockPatternUtils.java
+++ b/core/java/com/android/internal/widget/LockPatternUtils.java
@@ -333,11 +333,17 @@
@UnsupportedAppUsage
public LockPatternUtils(Context context) {
+ this(context, null);
+ }
+
+ @VisibleForTesting
+ public LockPatternUtils(Context context, ILockSettings lockSettings) {
mContext = context;
mContentResolver = context.getContentResolver();
Looper looper = Looper.myLooper();
mHandler = looper != null ? new Handler(looper) : null;
+ mLockSettingsService = lockSettings;
}
@UnsupportedAppUsage
diff --git a/core/java/com/android/internal/widget/LockSettingsInternal.java b/core/java/com/android/internal/widget/LockSettingsInternal.java
index 8114e1f..627e877 100644
--- a/core/java/com/android/internal/widget/LockSettingsInternal.java
+++ b/core/java/com/android/internal/widget/LockSettingsInternal.java
@@ -166,4 +166,16 @@
* Refreshes pending strong auth timeout with the latest admin requirement set by device policy.
*/
public abstract void refreshStrongAuthTimeout(int userId);
+
+ /**
+ * Register a LockSettingsStateListener
+ * @param listener The listener to be registered
+ */
+ public abstract void registerLockSettingsStateListener(ILockSettingsStateListener listener);
+
+ /**
+ * Unregister a LockSettingsStateListener
+ * @param listener The listener to be unregistered
+ */
+ public abstract void unregisterLockSettingsStateListener(ILockSettingsStateListener listener);
}
diff --git a/core/java/com/android/internal/widget/MessagingLinearLayout.java b/core/java/com/android/internal/widget/MessagingLinearLayout.java
index c06f5f7..e07acac 100644
--- a/core/java/com/android/internal/widget/MessagingLinearLayout.java
+++ b/core/java/com/android/internal/widget/MessagingLinearLayout.java
@@ -21,6 +21,8 @@
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
+import android.os.Build;
+import android.os.Trace;
import android.util.AttributeSet;
import android.view.RemotableViewMethod;
import android.view.View;
@@ -45,6 +47,8 @@
private int mMaxDisplayedLines = Integer.MAX_VALUE;
+ private static final boolean TRACE_ONMEASURE = Build.isDebuggable();
+
public MessagingLinearLayout(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
@@ -67,6 +71,10 @@
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ if (TRACE_ONMEASURE) {
+ Trace.beginSection("MessagingLinearLayout#onMeasure");
+ trackMeasureSpecs(widthMeasureSpec, heightMeasureSpec);
+ }
// This is essentially a bottom-up linear layout that only adds children that fit entirely
// up to a maximum height.
int targetHeight = MeasureSpec.getSize(heightMeasureSpec);
@@ -177,6 +185,9 @@
resolveSize(Math.max(getSuggestedMinimumWidth(), measuredWidth),
widthMeasureSpec),
Math.max(getSuggestedMinimumHeight(), totalHeight));
+ if (TRACE_ONMEASURE) {
+ Trace.endSection();
+ }
}
@Override
@@ -240,6 +251,25 @@
}
}
+ private void trackMeasureSpecs(int widthMeasureSpec, int heightMeasureSpec) {
+ if (!TRACE_ONMEASURE) {
+ return;
+ }
+
+ final int availableWidth = MeasureSpec.getSize(widthMeasureSpec);
+ final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
+ final int availableHeight = MeasureSpec.getSize(heightMeasureSpec);
+ final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
+ Trace.setCounter("MessagingLinearLayout#onMeasure_widthMeasureSpecSize",
+ availableWidth);
+ Trace.setCounter("MessagingLinearLayout#onMeasure_widthMeasureSpecMode",
+ widthMode);
+ Trace.setCounter("MessagingLinearLayout#onMeasure_heightMeasureSpecSize",
+ availableHeight);
+ Trace.setCounter("MessagingLinearLayout#onMeasure_heightMeasureSpecMode",
+ heightMode);
+ }
+
@Override
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
diff --git a/core/java/com/android/internal/widget/PeopleHelper.java b/core/java/com/android/internal/widget/PeopleHelper.java
index 85cedc3..3f5b4a0 100644
--- a/core/java/com/android/internal/widget/PeopleHelper.java
+++ b/core/java/com/android/internal/widget/PeopleHelper.java
@@ -22,6 +22,8 @@
import android.annotation.ColorInt;
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.app.Notification;
+import android.app.Person;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
@@ -222,6 +224,72 @@
}
/**
+ * A class that represents a map from unique sender names in the groups to the string 1- or
+ * 2-character prefix strings for the names. This class uses the String value of the
+ * CharSequence Names as the key.
+ */
+ public class NameToPrefixMap {
+ Map<String, String> mMap;
+ NameToPrefixMap(Map<String, String> map) {
+ this.mMap = map;
+ }
+
+ /**
+ * @param name the name
+ * @return the prefix of the given name
+ */
+ public String getPrefix(CharSequence name) {
+ return mMap.get(name.toString());
+ }
+ }
+
+ /**
+ * Same functionality as mapUniqueNamesToPrefix, but takes list-represented message groups as
+ * the input. This method is better when inflating MessagingGroup from the UI thread is not
+ * an option.
+ * @param groups message groups represented by lists. A message group is some consecutive
+ * messages (>=3) from the same sender in a conversation.
+ */
+ public NameToPrefixMap mapUniqueNamesToPrefixWithGroupList(
+ List<List<Notification.MessagingStyle.Message>> groups) {
+ // Map of unique names to their prefix
+ ArrayMap<String, String> uniqueNames = new ArrayMap<>();
+ // Map of single-character string prefix to the only name which uses it, or null if multiple
+ ArrayMap<String, CharSequence> uniqueCharacters = new ArrayMap<>();
+ for (int i = 0; i < groups.size(); i++) {
+ List<Notification.MessagingStyle.Message> group = groups.get(i);
+ if (group.isEmpty()) continue;
+ Person sender = group.get(0).getSenderPerson();
+ if (sender == null) continue;
+ CharSequence senderName = sender.getName();
+ if (sender.getIcon() != null || TextUtils.isEmpty(senderName)) {
+ continue;
+ }
+ String senderNameString = senderName.toString();
+ if (!uniqueNames.containsKey(senderNameString)) {
+ String charPrefix = findNamePrefix(senderName, null);
+ if (charPrefix == null) {
+ continue;
+ }
+ if (uniqueCharacters.containsKey(charPrefix)) {
+ // this character was already used, lets make it more unique. We first need to
+ // resolve the existing character if it exists
+ CharSequence existingName = uniqueCharacters.get(charPrefix);
+ if (existingName != null) {
+ uniqueNames.put(existingName.toString(), findNameSplit(existingName));
+ uniqueCharacters.put(charPrefix, null);
+ }
+ uniqueNames.put(senderNameString, findNameSplit(senderName));
+ } else {
+ uniqueNames.put(senderNameString, charPrefix);
+ uniqueCharacters.put(charPrefix, senderName);
+ }
+ }
+ }
+ return new NameToPrefixMap(uniqueNames);
+ }
+
+ /**
* Update whether the groups can hide the sender if they are first
* (happens only for 1:1 conversations where the given title matches the sender's name)
*/
diff --git a/core/java/com/android/server/backup/AccountSyncSettingsBackupHelper.java b/core/java/com/android/server/backup/AccountSyncSettingsBackupHelper.java
index ce9ab82..2ff6225 100644
--- a/core/java/com/android/server/backup/AccountSyncSettingsBackupHelper.java
+++ b/core/java/com/android/server/backup/AccountSyncSettingsBackupHelper.java
@@ -21,6 +21,7 @@
import android.app.backup.BackupDataInputStream;
import android.app.backup.BackupDataOutput;
import android.app.backup.BackupHelper;
+import android.app.backup.BackupHelperWithLogger;
import android.content.ContentResolver;
import android.content.Context;
import android.content.SyncAdapterType;
@@ -56,7 +57,7 @@
* sync settings are backed up as a JSON object containing all the necessary information for
* restoring the sync settings later.
*/
-public class AccountSyncSettingsBackupHelper implements BackupHelper {
+public class AccountSyncSettingsBackupHelper extends BackupHelperWithLogger {
private static final String TAG = "AccountSyncSettingsBackupHelper";
private static final boolean DEBUG = false;
diff --git a/core/jni/android_util_Process.cpp b/core/jni/android_util_Process.cpp
index 7af69f2..d2e58bb 100644
--- a/core/jni/android_util_Process.cpp
+++ b/core/jni/android_util_Process.cpp
@@ -28,6 +28,7 @@
#include <meminfo/sysmeminfo.h>
#include <processgroup/processgroup.h>
#include <processgroup/sched_policy.h>
+#include <android-base/logging.h>
#include <android-base/unique_fd.h>
#include <algorithm>
@@ -232,6 +233,31 @@
}
}
+// Look up the user ID of a process in /proc/${pid}/status. The Uid: line is present in
+// /proc/${pid}/status since at least kernel v2.5.
+static int uid_from_pid(int pid)
+{
+ int uid = -1;
+ std::array<char, 64> path;
+ int res = snprintf(path.data(), path.size(), "/proc/%d/status", pid);
+ if (res < 0 || res >= static_cast<int>(path.size())) {
+ DCHECK(false);
+ return uid;
+ }
+ FILE* f = fopen(path.data(), "r");
+ if (!f) {
+ return uid;
+ }
+ char line[256];
+ while (fgets(line, sizeof(line), f)) {
+ if (sscanf(line, "Uid: %d", &uid) == 1) {
+ break;
+ }
+ }
+ fclose(f);
+ return uid;
+}
+
void android_os_Process_setProcessGroup(JNIEnv* env, jobject clazz, int pid, jint grp)
{
ALOGV("%s pid=%d grp=%" PRId32, __func__, pid, grp);
@@ -275,7 +301,12 @@
}
}
- if (!SetProcessProfilesCached(0, pid, {get_cpuset_policy_profile_name((SchedPolicy)grp)}))
+ const int uid = uid_from_pid(pid);
+ if (uid < 0) {
+ signalExceptionForGroupError(env, ESRCH, pid);
+ return;
+ }
+ if (!SetProcessProfilesCached(uid, pid, {get_cpuset_policy_profile_name((SchedPolicy)grp)}))
signalExceptionForGroupError(env, errno ? errno : EPERM, pid);
}
@@ -1134,12 +1165,11 @@
static jlongArray android_os_Process_getRss(JNIEnv* env, jobject clazz, jint pid)
{
- // total, file, anon, swap
- jlong rss[4] = {0, 0, 0, 0};
+ // total, file, anon, swap, shmem
+ jlong rss[5] = {0, 0, 0, 0, 0};
std::string status_path =
android::base::StringPrintf("/proc/%d/status", pid);
UniqueFile file = MakeUniqueFile(status_path.c_str(), "re");
-
char line[256];
while (file != nullptr && fgets(line, sizeof(line), file.get())) {
jlong v;
@@ -1151,17 +1181,18 @@
rss[2] = v;
} else if ( sscanf(line, "VmSwap: %" SCNd64 " kB", &v) == 1) {
rss[3] = v;
+ } else if ( sscanf(line, "RssShmem: %" SCNd64 " kB", &v) == 1) {
+ rss[4] = v;
}
}
- jlongArray rssArray = env->NewLongArray(4);
+ jlongArray rssArray = env->NewLongArray(5);
if (rssArray == NULL) {
jniThrowException(env, "java/lang/OutOfMemoryError", NULL);
return NULL;
}
- env->SetLongArrayRegion(rssArray, 0, 4, rss);
-
+ env->SetLongArrayRegion(rssArray, 0, 5, rss);
return rssArray;
}
diff --git a/core/jni/android_view_SurfaceControl.cpp b/core/jni/android_view_SurfaceControl.cpp
index 25b2aaf..98f409a 100644
--- a/core/jni/android_view_SurfaceControl.cpp
+++ b/core/jni/android_view_SurfaceControl.cpp
@@ -343,7 +343,9 @@
const std::vector<SurfaceControlStats>& /*stats*/) {
JNIEnv* env = getenv();
// Adding a strong reference for java SyncFence
- presentFence->incStrong(0);
+ if (presentFence) {
+ presentFence->incStrong(0);
+ }
jobject stats =
env->NewObject(gTransactionStatsClassInfo.clazz, gTransactionStatsClassInfo.ctor,
diff --git a/core/proto/android/server/windowmanagerservice.proto b/core/proto/android/server/windowmanagerservice.proto
index b63021d..c92435f 100644
--- a/core/proto/android/server/windowmanagerservice.proto
+++ b/core/proto/android/server/windowmanagerservice.proto
@@ -464,6 +464,7 @@
repeated .android.graphics.RectProto keep_clear_areas = 45;
repeated .android.graphics.RectProto unrestricted_keep_clear_areas = 46;
repeated .android.view.InsetsSourceProto mergedLocalInsetsSources = 47;
+ optional int32 requested_visible_types = 48;
}
message IdentifierProto {
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index 52cf679..723eb70 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -2961,7 +2961,7 @@
<p>Protection level: signature
@SystemApi
@hide
- @FlaggedApi("com.android.internal.telephony.flags.ap_domain_selection_enabled")
+ @FlaggedApi("com.android.internal.telephony.flags.use_oem_domain_selection_service")
-->
<permission android:name="android.permission.BIND_DOMAIN_SELECTION_SERVICE"
android:protectionLevel="signature" />
@@ -3775,6 +3775,13 @@
<permission android:name="android.permission.MANAGE_DEVICE_POLICY_ACROSS_USERS_FULL"
android:protectionLevel="internal|role" />
+ <!-- Allows an application to access EnhancedConfirmationManager.
+ @SystemApi
+ @FlaggedApi("android.permission.flags.enhanced_confirmation_mode_apis_enabled")
+ @hide This is not a third-party API (intended for OEMs and system apps). -->
+ <permission android:name="android.permission.MANAGE_ENHANCED_CONFIRMATION_STATES"
+ android:protectionLevel="signature|installer" />
+
<!-- @SystemApi @hide Allows an application to set a device owner on retail demo devices.-->
<permission android:name="android.permission.PROVISION_DEMO_DEVICE"
android:protectionLevel="signature|setup|knownSigner"
@@ -5737,6 +5744,14 @@
android:description="@string/permdesc_observeCompanionDevicePresence"
android:protectionLevel="normal" />
+ <!-- Allows an application to subscribe to notifications about the nearby devices' presence
+ status change base on the UUIDs.
+ <p>Not for use by third-party applications.</p>
+ @FlaggedApi("android.companion.flags.device_presence")
+ -->
+ <permission android:name="android.permission.REQUEST_OBSERVE_DEVICE_UUID_PRESENCE"
+ android:protectionLevel="signature|privileged" />
+
<!-- Allows an application to deliver companion messages to system
-->
<permission android:name="android.permission.DELIVER_COMPANION_MESSAGES"
@@ -7050,6 +7065,7 @@
android:protectionLevel="signature" />
<!-- @SystemApi Allows an application to access the smartspace service as a client.
+ @FlaggedApi(android.app.smartspace.flags.Flags.FLAG_ACCESS_SMARTSPACE)
@hide <p>Not for use by third-party applications.</p> -->
<permission android:name="android.permission.ACCESS_SMARTSPACE"
android:protectionLevel="signature|privileged|development" />
@@ -7946,11 +7962,11 @@
<!-- @SystemApi Allows an application to read the system grammatical gender.
@FlaggedApi("android.app.system_terms_of_address_enabled")
- <p>Protection level: signature|privileged|appop
+ <p>Protection level: signature|privileged
@hide
-->
<permission android:name="android.permission.READ_SYSTEM_GRAMMATICAL_GENDER"
- android:protectionLevel="signature|privileged|appop"/>
+ android:protectionLevel="signature|privileged"/>
<!-- Attribution for Geofencing service. -->
<attribution android:tag="GeofencingService" android:label="@string/geofencing_service"/>
@@ -8399,6 +8415,10 @@
android:permission="android.permission.BIND_JOB_SERVICE">
</service>
+ <service android:name="com.android.server.selinux.SelinuxAuditLogsService"
+ android:permission="android.permission.BIND_JOB_SERVICE">
+ </service>
+
<service android:name="com.android.server.compos.IsolatedCompilationJobService"
android:permission="android.permission.BIND_JOB_SERVICE">
</service>
diff --git a/core/res/res/drawable/autofill_half_sheet_divider.xml b/core/res/res/drawable/autofill_half_sheet_divider.xml
new file mode 100644
index 0000000..1a96c7d
--- /dev/null
+++ b/core/res/res/drawable/autofill_half_sheet_divider.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!-- Copied from //frameworks/base/core/res/res/drawable/list_divider_material.xml. -->
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:tint="@color/foreground_material_light">
+ <solid android:color="#1f000000" />
+ <size
+ android:height="1dp"
+ android:width="1dp"/>
+</shape>
\ No newline at end of file
diff --git a/core/res/res/layout/autofill_save.xml b/core/res/res/layout/autofill_save.xml
index 27f8138..ddedca2 100644
--- a/core/res/res/layout/autofill_save.xml
+++ b/core/res/res/layout/autofill_save.xml
@@ -31,11 +31,11 @@
android:gravity="center_horizontal"
android:orientation="vertical">
<ScrollView
+ android:id="@+id/autofill_sheet_scroll_view"
android:layout_width="fill_parent"
android:layout_height="0dp"
android:fillViewport="true"
- android:layout_weight="1"
- android:layout_marginBottom="8dp">
+ android:layout_weight="1">
<LinearLayout
android:layout_marginStart="@dimen/autofill_save_outer_margin"
android:layout_marginEnd="@dimen/autofill_save_outer_margin"
@@ -66,16 +66,25 @@
android:layout_height="wrap_content"
android:minHeight="0dp"
android:visibility="gone"/>
-
+ <View
+ android:id="@+id/autofill_sheet_scroll_view_space"
+ android:layout_width="match_parent"
+ android:layout_height="16dp"/>
</LinearLayout>
</ScrollView>
+ <View
+ android:id="@+id/autofill_sheet_divider"
+ android:layout_width="match_parent"
+ android:layout_height="1dp"
+ style="@style/AutofillHalfSheetDivider" />
+
<com.android.internal.widget.ButtonBarLayout
android:layout_width="match_parent"
android:layout_height="48dp"
android:layout_gravity="end"
android:clipToPadding="false"
- android:layout_marginTop="16dp"
+ android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
android:theme="@style/Theme.DeviceDefault.AutofillHalfScreenDialogButton"
android:orientation="horizontal"
diff --git a/core/res/res/values/attrs_manifest.xml b/core/res/res/values/attrs_manifest.xml
index 29086a45..35276bf 100644
--- a/core/res/res/values/attrs_manifest.xml
+++ b/core/res/res/values/attrs_manifest.xml
@@ -506,6 +506,12 @@
receivers, and providers; it can not be used with activities. -->
<attr name="singleUser" format="boolean" />
+ <!-- If set to true, only a single instance of this component will
+ run and be available for the SYSTEM user. Non SYSTEM users will not be
+ allowed to access the component if this flag is enabled.
+ This flag can be used with services, receivers, providers and activities. -->
+ <attr name="systemUserOnly" format="boolean" />
+
<!-- Specify a specific process that the associated code is to run in.
Use with the application tag (to supply a default process for all
application components), or with the activity, receiver, service,
@@ -2865,6 +2871,7 @@
Context.createAttributionContext() using the first attribution tag
contained here. -->
<attr name="attributionTags" />
+ <attr name="systemUserOnly" format="boolean" />
</declare-styleable>
<!-- Attributes that can be supplied in an AndroidManifest.xml
@@ -3023,6 +3030,7 @@
ignored when the process is bound into a shared isolated process by a client.
-->
<attr name="allowSharedIsolatedProcess" format="boolean" />
+ <attr name="systemUserOnly" format="boolean" />
</declare-styleable>
<!-- @hide The <code>apex-system-service</code> tag declares an apex system service
@@ -3150,7 +3158,7 @@
<attr name="uiOptions" />
<attr name="parentActivityName" />
<attr name="singleUser" />
- <!-- @hide This broadcast receiver or activity will only receive broadcasts for the
+ <!-- This broadcast receiver or activity will only receive broadcasts for the
system user-->
<attr name="systemUserOnly" format="boolean" />
<attr name="persistableMode" />
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 9e14006..23c78fd 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -4497,6 +4497,16 @@
<!-- URI for default Accessibility notification sound when to enable accessibility shortcut. -->
<string name="config_defaultAccessibilityNotificationSound" translatable="false"></string>
+ <!-- Array of component names, each flattened to a string, for accessibility services that
+ can be enabled by the user without showing a warning prompt. These services must be
+ preinstalled. -->
+ <string-array translatable="false" name="config_trustedAccessibilityServices">
+ <!--
+ <item>com.example.package.first/com.example.class.FirstService</item>
+ <item>com.example.package.second/com.example.class.SecondService</item>
+ -->
+ </string-array>
+
<!-- Warning: This API can be dangerous when not implemented properly. In particular,
escrow token must NOT be retrievable from device storage. In other words, either
escrow token is not stored on device or its ciphertext is stored on device while
@@ -6601,7 +6611,7 @@
</string-array>
<!-- Whether or not the monitoring on the apps' background battery drain is enabled -->
- <bool name="config_bg_current_drain_monitor_enabled">true</bool>
+ <bool name="config_bg_current_drain_monitor_enabled">false</bool>
<!-- The threshold of the background current drain (in percentage) to the restricted
standby bucket.
@@ -6891,4 +6901,11 @@
<!-- Defines suitability of the built-in speaker route.
Refer to {@link MediaRoute2Info} to see supported values. -->
<integer name="config_mediaRouter_builtInSpeakerSuitability">0</integer>
+
+ <!-- Whether to show a percentage text next to the progressbar while preparing to update the
+ device -->
+ <bool name="config_showPercentageTextDuringRebootToUpdate">true</bool>
+
+ <!-- Defines the minimum interval (in ms) between two input-based user-activity poke events. -->
+ <integer name="config_minMillisBetweenInputUserActivityEvents">100</integer>
</resources>
diff --git a/core/res/res/values/public-staging.xml b/core/res/res/values/public-staging.xml
index 7d22885..b8fc052 100644
--- a/core/res/res/values/public-staging.xml
+++ b/core/res/res/values/public-staging.xml
@@ -121,6 +121,8 @@
<public name="adServiceTypes" />
<!-- @hide @SystemApi @FlaggedApi("android.content.res.manifest_flagging") -->
<public name="featureFlag"/>
+ <!-- @FlaggedApi("android.multiuser.enable_system_user_only_for_services_and_providers") -->
+ <public name="systemUserOnly"/>
</staging-public-group>
<staging-public-group type="id" first-id="0x01bc0000">
diff --git a/core/res/res/values/styles.xml b/core/res/res/values/styles.xml
index 619ec31..22d028c 100644
--- a/core/res/res/values/styles.xml
+++ b/core/res/res/values/styles.xml
@@ -1515,6 +1515,11 @@
<item name="background">@drawable/btn_outlined</item>
</style>
+ <!-- @hide Divider for Autofill half screen dialog -->
+ <style name="AutofillHalfSheetDivider">
+ <item name="android:background">@drawable/autofill_half_sheet_divider</item>
+ </style>
+
<!-- @hide Autofill background for popup window (not for fullscreen) -->
<style name="AutofillDatasetPicker">
<item name="elevation">4dp</item>
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index ef12d8f..3c1a42f 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -3630,6 +3630,7 @@
<java-symbol type="string" name="config_defaultAccessibilityService" />
<java-symbol type="string" name="config_defaultAccessibilityNotificationSound" />
<java-symbol type="string" name="accessibility_shortcut_spoken_feedback" />
+ <java-symbol type="array" name="config_trustedAccessibilityServices" />
<java-symbol type="string" name="accessibility_select_shortcut_menu_title" />
<java-symbol type="string" name="accessibility_edit_shortcut_menu_button_title" />
@@ -3703,6 +3704,10 @@
<java-symbol type="id" name="autofill_dataset_list"/>
<java-symbol type="id" name="autofill_dataset_picker"/>
<java-symbol type="id" name="autofill_dataset_title" />
+ <java-symbol type="id" name="autofill_sheet_divider"/>
+ <java-symbol type="id" name="autofill_sheet_scroll_view"/>
+ <java-symbol type="id" name="autofill_sheet_scroll_view_space"/>
+
<java-symbol type="id" name="autofill_save_custom_subtitle" />
<java-symbol type="id" name="autofill_save_icon" />
<java-symbol type="id" name="autofill_save_no" />
@@ -5312,4 +5317,9 @@
<!-- Android MediaRouter framework configs. -->
<java-symbol type="integer" name="config_mediaRouter_builtInSpeakerSuitability" />
+
+ <!-- Shutdown thread config flags -->
+ <java-symbol type="bool" name="config_showPercentageTextDuringRebootToUpdate" />
+
+ <java-symbol type="integer" name="config_minMillisBetweenInputUserActivityEvents" />
</resources>
diff --git a/core/tests/coretests/Android.bp b/core/tests/coretests/Android.bp
index d1a90ae..f476799 100644
--- a/core/tests/coretests/Android.bp
+++ b/core/tests/coretests/Android.bp
@@ -209,11 +209,15 @@
"testng",
],
srcs: [
+ "src/android/content/pm/PackageManagerTest.java",
+ "src/android/content/pm/UserInfoTest.java",
"src/android/database/CursorWindowTest.java",
"src/android/os/**/*.java",
+ "src/android/telephony/PinResultTest.java",
"src/android/util/**/*.java",
+ "src/android/view/DisplayInfoTest.java",
+ "src/com/android/internal/logging/**/*.java",
"src/com/android/internal/os/**/*.java",
- "src/com/android/internal/os/LongArrayMultiStateCounterTest.java",
"src/com/android/internal/util/**/*.java",
"src/com/android/internal/power/EnergyConsumerStatsTest.java",
diff --git a/core/tests/coretests/src/android/content/pm/PackageManagerTest.java b/core/tests/coretests/src/android/content/pm/PackageManagerTest.java
new file mode 100644
index 0000000..20421d1
--- /dev/null
+++ b/core/tests/coretests/src/android/content/pm/PackageManagerTest.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.content.pm;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class PackageManagerTest {
+ @Test
+ public void testPackageInfoFlags() throws Exception {
+ assertThat(PackageManager.PackageInfoFlags.of(42L).getValue()).isEqualTo(42L);
+ }
+
+ @Test
+ public void testApplicationInfoFlags() throws Exception {
+ assertThat(PackageManager.ApplicationInfoFlags.of(42L).getValue()).isEqualTo(42L);
+ }
+
+ @Test
+ public void testComponentInfoFlags() throws Exception {
+ assertThat(PackageManager.ComponentInfoFlags.of(42L).getValue()).isEqualTo(42L);
+ }
+
+ @Test
+ public void testResolveInfoFlags() throws Exception {
+ assertThat(PackageManager.ResolveInfoFlags.of(42L).getValue()).isEqualTo(42L);
+ }
+}
diff --git a/core/tests/coretests/src/android/content/pm/UserInfoTest.java b/core/tests/coretests/src/android/content/pm/UserInfoTest.java
new file mode 100644
index 0000000..af36dbb
--- /dev/null
+++ b/core/tests/coretests/src/android/content/pm/UserInfoTest.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.content.pm;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.os.UserHandle;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class UserInfoTest {
+ @Test
+ public void testSimple() throws Exception {
+ final UserInfo ui = new UserInfo(10, "Test", UserInfo.FLAG_GUEST);
+
+ assertThat(ui.getUserHandle()).isEqualTo(UserHandle.of(10));
+ assertThat(ui.name).isEqualTo("Test");
+
+ // Derived based on userType field
+ assertThat(ui.isManagedProfile()).isEqualTo(false);
+ assertThat(ui.isGuest()).isEqualTo(true);
+ assertThat(ui.isRestricted()).isEqualTo(false);
+ assertThat(ui.isDemo()).isEqualTo(false);
+ assertThat(ui.isCloneProfile()).isEqualTo(false);
+ assertThat(ui.isCommunalProfile()).isEqualTo(false);
+ assertThat(ui.isPrivateProfile()).isEqualTo(false);
+
+ // Derived based on flags field
+ assertThat(ui.isPrimary()).isEqualTo(false);
+ assertThat(ui.isAdmin()).isEqualTo(false);
+ assertThat(ui.isProfile()).isEqualTo(false);
+ assertThat(ui.isEnabled()).isEqualTo(true);
+ assertThat(ui.isQuietModeEnabled()).isEqualTo(false);
+ assertThat(ui.isEphemeral()).isEqualTo(false);
+ assertThat(ui.isForTesting()).isEqualTo(false);
+ assertThat(ui.isInitialized()).isEqualTo(false);
+ assertThat(ui.isFull()).isEqualTo(false);
+ assertThat(ui.isMain()).isEqualTo(false);
+
+ // Derived dynamically
+ assertThat(ui.canHaveProfile()).isEqualTo(false);
+ }
+
+ @Test
+ public void testDebug() throws Exception {
+ final UserInfo ui = new UserInfo(10, "Test", UserInfo.FLAG_GUEST);
+
+ assertThat(ui.toString()).isNotEmpty();
+ assertThat(ui.toFullString()).isNotEmpty();
+ }
+}
diff --git a/core/tests/coretests/src/android/content/res/FontScaleConverterFactoryTest.kt b/core/tests/coretests/src/android/content/res/FontScaleConverterFactoryTest.kt
index e32a57b..a2a5433 100644
--- a/core/tests/coretests/src/android/content/res/FontScaleConverterFactoryTest.kt
+++ b/core/tests/coretests/src/android/content/res/FontScaleConverterFactoryTest.kt
@@ -145,7 +145,6 @@
fun unnecessaryFontScalesReturnsNull() {
assertThat(FontScaleConverterFactory.forScale(0F)).isNull()
assertThat(FontScaleConverterFactory.forScale(1F)).isNull()
- assertThat(FontScaleConverterFactory.forScale(1.1F)).isNull()
assertThat(FontScaleConverterFactory.forScale(0.85F)).isNull()
}
@@ -176,7 +175,7 @@
assertThat(FontScaleConverterFactory.isNonLinearFontScalingActive(-1f)).isFalse()
assertThat(FontScaleConverterFactory.isNonLinearFontScalingActive(0.85f)).isFalse()
assertThat(FontScaleConverterFactory.isNonLinearFontScalingActive(1.02f)).isFalse()
- assertThat(FontScaleConverterFactory.isNonLinearFontScalingActive(1.10f)).isFalse()
+ assertThat(FontScaleConverterFactory.isNonLinearFontScalingActive(1.10f)).isTrue()
assertThat(FontScaleConverterFactory.isNonLinearFontScalingActive(1.15f)).isTrue()
assertThat(FontScaleConverterFactory.isNonLinearFontScalingActive(1.1499999f))
.isTrue()
diff --git a/core/tests/coretests/src/android/database/sqlite/SQLiteDatabaseTest.java b/core/tests/coretests/src/android/database/sqlite/SQLiteDatabaseTest.java
index e118c98d..3ee565f 100644
--- a/core/tests/coretests/src/android/database/sqlite/SQLiteDatabaseTest.java
+++ b/core/tests/coretests/src/android/database/sqlite/SQLiteDatabaseTest.java
@@ -403,4 +403,41 @@
}
assertFalse(allowed);
}
+
+ /** Return true if the path is in the list of strings. */
+ private boolean isConcurrent(String path) throws Exception {
+ path = new File(path).toPath().toRealPath().toString();
+ return SQLiteDatabase.getConcurrentDatabasePaths().contains(path);
+ }
+
+ @Test
+ public void testDuplicateDatabases() throws Exception {
+ // The two database paths in this test are assumed not to have been opened earlier in this
+ // process.
+
+ // A database path that will be opened twice.
+ final String dbName = "never-used-db.db";
+ final File dbFile = mContext.getDatabasePath(dbName);
+ final String dbPath = dbFile.getPath();
+
+ // A database path that will be opened only once.
+ final String okName = "never-used-ok.db";
+ final File okFile = mContext.getDatabasePath(okName);
+ final String okPath = okFile.getPath();
+
+ SQLiteDatabase db1 = SQLiteDatabase.openOrCreateDatabase(dbFile, null);
+ assertFalse(isConcurrent(dbPath));
+ SQLiteDatabase db2 = SQLiteDatabase.openOrCreateDatabase(dbFile, null);
+ assertTrue(isConcurrent(dbPath));
+ db1.close();
+ assertTrue(isConcurrent(dbPath));
+ db2.close();
+ assertTrue(isConcurrent(dbPath));
+
+ SQLiteDatabase db3 = SQLiteDatabase.openOrCreateDatabase(okFile, null);
+ db3.close();
+ db3 = SQLiteDatabase.openOrCreateDatabase(okFile, null);
+ assertFalse(isConcurrent(okPath));
+ db3.close();
+ }
}
diff --git a/core/tests/coretests/src/android/os/BuildTest.java b/core/tests/coretests/src/android/os/BuildTest.java
index 2d3e123..2a718ff 100644
--- a/core/tests/coretests/src/android/os/BuildTest.java
+++ b/core/tests/coretests/src/android/os/BuildTest.java
@@ -20,7 +20,6 @@
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
-import android.platform.test.annotations.IgnoreUnderRavenwood;
import android.platform.test.flag.junit.SetFlagsRule;
import android.platform.test.ravenwood.RavenwoodRule;
@@ -71,7 +70,6 @@
*/
@Test
@SmallTest
- @IgnoreUnderRavenwood(blockedBy = Build.class)
public void testBuildFields() throws Exception {
assertNotEmpty("ID", Build.ID);
assertNotEmpty("DISPLAY", Build.DISPLAY);
diff --git a/core/tests/coretests/src/android/os/TraceTest.java b/core/tests/coretests/src/android/os/TraceTest.java
index 593833ec..b2c005f 100644
--- a/core/tests/coretests/src/android/os/TraceTest.java
+++ b/core/tests/coretests/src/android/os/TraceTest.java
@@ -34,7 +34,6 @@
* while tracing on the emulator and then run traceview to view the trace.
*/
@RunWith(AndroidJUnit4.class)
-@IgnoreUnderRavenwood(blockedBy = Trace.class)
public class TraceTest {
private static final String TAG = "TraceTest";
@@ -46,7 +45,51 @@
private int gMethodCalls = 0;
@Test
+ public void testEnableDisable() {
+ // Currently only verifying that we can invoke without crashing
+ Trace.setTracingEnabled(true, 0);
+ Trace.setTracingEnabled(false, 0);
+
+ Trace.setAppTracingAllowed(true);
+ Trace.setAppTracingAllowed(false);
+ }
+
+ @Test
+ public void testBeginEnd() {
+ // Currently only verifying that we can invoke without crashing
+ Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, TAG);
+ Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
+
+ Trace.asyncTraceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, TAG, 42);
+ Trace.asyncTraceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER, TAG, 42);
+
+ Trace.asyncTraceForTrackBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, TAG, TAG, 42);
+ Trace.asyncTraceForTrackEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER, TAG, 42);
+
+ Trace.beginSection(TAG);
+ Trace.endSection();
+
+ Trace.beginAsyncSection(TAG, 42);
+ Trace.endAsyncSection(TAG, 42);
+ }
+
+ @Test
+ public void testCounter() {
+ // Currently only verifying that we can invoke without crashing
+ Trace.traceCounter(Trace.TRACE_TAG_ACTIVITY_MANAGER, TAG, 42);
+ Trace.setCounter(TAG, 42);
+ }
+
+ @Test
+ public void testInstant() {
+ // Currently only verifying that we can invoke without crashing
+ Trace.instant(Trace.TRACE_TAG_ACTIVITY_MANAGER, TAG);
+ Trace.instantForTrack(Trace.TRACE_TAG_ACTIVITY_MANAGER, TAG, TAG);
+ }
+
+ @Test
public void testNullStrings() {
+ // Currently only verifying that we can invoke without crashing
Trace.traceCounter(Trace.TRACE_TAG_ACTIVITY_MANAGER, null, 42);
Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, null);
@@ -62,6 +105,7 @@
@Test
@SmallTest
+ @IgnoreUnderRavenwood(blockedBy = Debug.class)
public void testNativeTracingFromJava()
{
long start = System.currentTimeMillis();
@@ -82,6 +126,7 @@
// This should not run in the automated suite.
@Suppress
+ @IgnoreUnderRavenwood(blockedBy = Debug.class)
public void disableTestNativeTracingFromC()
{
long start = System.currentTimeMillis();
@@ -97,6 +142,7 @@
@Test
@LargeTest
@Suppress // Failing.
+ @IgnoreUnderRavenwood(blockedBy = Debug.class)
public void testMethodTracing()
{
long start = System.currentTimeMillis();
diff --git a/core/tests/coretests/src/android/telephony/PinResultTest.java b/core/tests/coretests/src/android/telephony/PinResultTest.java
new file mode 100644
index 0000000..c260807
--- /dev/null
+++ b/core/tests/coretests/src/android/telephony/PinResultTest.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.telephony;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class PinResultTest {
+ @Test
+ public void testSimple() throws Exception {
+ final PinResult res = new PinResult(PinResult.PIN_RESULT_TYPE_SUCCESS, 5);
+ assertThat(res.getResult()).isEqualTo(PinResult.PIN_RESULT_TYPE_SUCCESS);
+ assertThat(res.getAttemptsRemaining()).isEqualTo(5);
+ }
+}
diff --git a/core/tests/coretests/src/android/util/SingletonTest.java b/core/tests/coretests/src/android/util/SingletonTest.java
new file mode 100644
index 0000000..8c5a963
--- /dev/null
+++ b/core/tests/coretests/src/android/util/SingletonTest.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.util;
+
+import static org.junit.Assert.assertTrue;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class SingletonTest {
+ @Test
+ public void testSimple() throws Exception {
+ final Singleton<Object> singleton = new Singleton<>() {
+ @Override
+ protected Object create() {
+ return new Object();
+ }
+ };
+
+ final Object first = singleton.get();
+ final Object second = singleton.get();
+ assertTrue(first == second);
+ }
+}
diff --git a/core/tests/coretests/src/android/view/DisplayInfoTest.java b/core/tests/coretests/src/android/view/DisplayInfoTest.java
index 803d38c..4c5b7e5 100644
--- a/core/tests/coretests/src/android/view/DisplayInfoTest.java
+++ b/core/tests/coretests/src/android/view/DisplayInfoTest.java
@@ -21,9 +21,12 @@
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
+import android.platform.test.ravenwood.RavenwoodRule;
+
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
+import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -32,6 +35,9 @@
public class DisplayInfoTest {
private static final float FLOAT_EQUAL_DELTA = 0.0001f;
+ @Rule
+ public final RavenwoodRule mRavenwood = new RavenwoodRule();
+
@Test
public void testDefaultDisplayInfosAreEqual() {
DisplayInfo displayInfo1 = new DisplayInfo();
diff --git a/core/tests/coretests/src/android/view/InsetsStateTest.java b/core/tests/coretests/src/android/view/InsetsStateTest.java
index 906d84e..672875a 100644
--- a/core/tests/coretests/src/android/view/InsetsStateTest.java
+++ b/core/tests/coretests/src/android/view/InsetsStateTest.java
@@ -20,8 +20,8 @@
import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED;
import static android.view.InsetsSource.FLAG_FORCE_CONSUMING;
import static android.view.InsetsSource.ID_IME;
-import static android.view.InsetsState.ISIDE_BOTTOM;
-import static android.view.InsetsState.ISIDE_TOP;
+import static android.view.InsetsSource.SIDE_BOTTOM;
+import static android.view.InsetsSource.SIDE_TOP;
import static android.view.RoundedCorner.POSITION_BOTTOM_LEFT;
import static android.view.RoundedCorner.POSITION_BOTTOM_RIGHT;
import static android.view.RoundedCorner.POSITION_TOP_LEFT;
@@ -106,8 +106,8 @@
typeSideMap);
assertEquals(Insets.of(0, 100, 0, 100), insets.getSystemWindowInsets());
assertEquals(Insets.of(0, 100, 0, 100), insets.getInsets(Type.all()));
- assertEquals(ISIDE_TOP, typeSideMap.get(ID_STATUS_BAR));
- assertEquals(ISIDE_BOTTOM, typeSideMap.get(ID_IME));
+ assertEquals(SIDE_TOP, typeSideMap.get(ID_STATUS_BAR));
+ assertEquals(SIDE_BOTTOM, typeSideMap.get(ID_IME));
assertEquals(Insets.of(0, 100, 0, 0), insets.getInsets(statusBars()));
assertEquals(Insets.of(0, 0, 0, 100), insets.getInsets(ime()));
}
diff --git a/core/tests/coretests/src/android/view/ViewRootImplTest.java b/core/tests/coretests/src/android/view/ViewRootImplTest.java
index cfbda84..cf3eb12 100644
--- a/core/tests/coretests/src/android/view/ViewRootImplTest.java
+++ b/core/tests/coretests/src/android/view/ViewRootImplTest.java
@@ -19,7 +19,6 @@
import static android.view.accessibility.Flags.FLAG_FORCE_INVERT_COLOR;
import static android.view.flags.Flags.FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY;
import static android.view.Surface.FRAME_RATE_CATEGORY_HIGH;
-import static android.view.Surface.FRAME_RATE_CATEGORY_HIGH_HINT;
import static android.view.Surface.FRAME_RATE_CATEGORY_LOW;
import static android.view.Surface.FRAME_RATE_CATEGORY_NORMAL;
import static android.view.Surface.FRAME_RATE_CATEGORY_NO_PREFERENCE;
@@ -576,13 +575,8 @@
assertEquals(viewRootImpl.getPreferredFrameRateCategory(), FRAME_RATE_CATEGORY_LOW);
viewRootImpl.votePreferredFrameRateCategory(FRAME_RATE_CATEGORY_NORMAL);
assertEquals(viewRootImpl.getPreferredFrameRateCategory(), FRAME_RATE_CATEGORY_NORMAL);
- viewRootImpl.votePreferredFrameRateCategory(FRAME_RATE_CATEGORY_HIGH_HINT);
- assertEquals(viewRootImpl.getPreferredFrameRateCategory(),
- FRAME_RATE_CATEGORY_HIGH_HINT);
viewRootImpl.votePreferredFrameRateCategory(FRAME_RATE_CATEGORY_HIGH);
assertEquals(viewRootImpl.getPreferredFrameRateCategory(), FRAME_RATE_CATEGORY_HIGH);
- viewRootImpl.votePreferredFrameRateCategory(FRAME_RATE_CATEGORY_HIGH_HINT);
- assertEquals(viewRootImpl.getPreferredFrameRateCategory(), FRAME_RATE_CATEGORY_HIGH);
viewRootImpl.votePreferredFrameRateCategory(FRAME_RATE_CATEGORY_NORMAL);
assertEquals(viewRootImpl.getPreferredFrameRateCategory(), FRAME_RATE_CATEGORY_HIGH);
viewRootImpl.votePreferredFrameRateCategory(FRAME_RATE_CATEGORY_LOW);
diff --git a/core/tests/coretests/src/android/view/contentcapture/MainContentCaptureSessionV2Test.java b/core/tests/coretests/src/android/view/contentcapture/MainContentCaptureSessionV2Test.java
index f0f3a96..0075128 100644
--- a/core/tests/coretests/src/android/view/contentcapture/MainContentCaptureSessionV2Test.java
+++ b/core/tests/coretests/src/android/view/contentcapture/MainContentCaptureSessionV2Test.java
@@ -433,6 +433,72 @@
assertThat(session.mEvents).isEmpty();
}
+ @Test
+ public void notifyViewAppearedBelowMaximumBufferSize() throws RemoteException {
+ ContentCaptureOptions options =
+ createOptions(
+ /* enableContentCaptureReceiver= */ true,
+ /* enableContentProtectionReceiver= */ true);
+ MainContentCaptureSessionV2 session = createSession(options);
+ session.mDirectServiceInterface = mMockContentCaptureDirectManager;
+
+ session.onSessionStarted(0x2, null);
+ for (int i = 0; i < BUFFER_SIZE - 1; i++) {
+ View view = prepareView(session);
+ session.notifyViewAppeared(session.newViewStructure(view));
+ }
+ mTestableLooper.processAllMessages();
+
+ verify(mMockContentCaptureDirectManager, times(0))
+ .sendEvents(any(), anyInt(), any());
+ assertThat(session.mEvents).isNull();
+ assertThat(session.mEventProcessQueue).hasSize(BUFFER_SIZE - 1);
+ }
+
+ @Test
+ public void notifyViewAppearedExactAsMaximumBufferSize() throws RemoteException {
+ ContentCaptureOptions options =
+ createOptions(
+ /* enableContentCaptureReceiver= */ true,
+ /* enableContentProtectionReceiver= */ true);
+ MainContentCaptureSessionV2 session = createSession(options);
+ session.mDirectServiceInterface = mMockContentCaptureDirectManager;
+
+ session.onSessionStarted(0x2, null);
+ for (int i = 0; i < BUFFER_SIZE; i++) {
+ View view = prepareView(session);
+ session.notifyViewAppeared(session.newViewStructure(view));
+ }
+ mTestableLooper.processAllMessages();
+
+ verify(mMockContentCaptureDirectManager, times(1))
+ .sendEvents(any(), anyInt(), any());
+ assertThat(session.mEvents).isEmpty();
+ assertThat(session.mEventProcessQueue).isEmpty();
+ }
+
+ @Test
+ public void notifyViewAppearedAboveMaximumBufferSize() throws RemoteException {
+ ContentCaptureOptions options =
+ createOptions(
+ /* enableContentCaptureReceiver= */ true,
+ /* enableContentProtectionReceiver= */ true);
+ MainContentCaptureSessionV2 session = createSession(options);
+ session.mDirectServiceInterface = mMockContentCaptureDirectManager;
+
+ session.onSessionStarted(0x2, null);
+ for (int i = 0; i < BUFFER_SIZE * 2 + 1; i++) {
+ View view = prepareView(session);
+ session.notifyViewAppeared(session.newViewStructure(view));
+ }
+ mTestableLooper.processAllMessages();
+
+ verify(mMockContentCaptureDirectManager, times(2))
+ .sendEvents(any(), anyInt(), any());
+ assertThat(session.mEvents).isEmpty();
+ assertThat(session.mEventProcessQueue).hasSize(1);
+ }
+
/** Simulates the regular content capture events sequence. */
private void notifyContentCaptureEvents(final MainContentCaptureSessionV2 session) {
final ArrayList<Object> events = new ArrayList<>(
diff --git a/core/tests/coretests/src/android/widget/RemoteViewsTest.java b/core/tests/coretests/src/android/widget/RemoteViewsTest.java
index 15c9047..543d73b 100644
--- a/core/tests/coretests/src/android/widget/RemoteViewsTest.java
+++ b/core/tests/coretests/src/android/widget/RemoteViewsTest.java
@@ -16,6 +16,8 @@
package android.widget;
+import static android.appwidget.flags.Flags.drawDataParcel;
+
import static com.android.internal.R.id.pending_intent_tag;
import static org.junit.Assert.assertArrayEquals;
@@ -63,6 +65,7 @@
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
@@ -414,6 +417,48 @@
assertNotNull(view.findViewById(R.id.light_background_text));
}
+ @Test
+ public void remoteCanvasCanAccessDrawInstructions() {
+ if (!drawDataParcel()) {
+ return;
+ }
+ final RemoteViews.DrawInstructions drawInstructions = getDrawInstructions();
+ final RemoteViews rv = new RemoteViews(drawInstructions);
+ final View view = rv.apply(mContext, mContainer);
+ assertTrue(view instanceof RemoteCanvas);
+ assertEquals(drawInstructions, view.getTag());
+ }
+
+ @Test
+ public void remoteCanvasWiresClickHandlers() {
+ if (!drawDataParcel()) {
+ return;
+ }
+ final RemoteViews.DrawInstructions drawInstructions = getDrawInstructions();
+ final RemoteViews rv = new RemoteViews(drawInstructions);
+ final PendingIntent pi = PendingIntent.getActivity(mContext, 0,
+ new Intent(Intent.ACTION_VIEW), PendingIntent.FLAG_IMMUTABLE);
+ final Intent i = new Intent().putExtra("TEST", "Success");
+ final int viewId = 1;
+ rv.setPendingIntentTemplate(viewId, pi);
+ rv.setOnClickFillInIntent(viewId, i);
+ final View view = rv.apply(mContext, mContainer);
+ assertTrue(view instanceof RemoteCanvas);
+ RemoteCanvas target = (RemoteCanvas) view;
+ assertEquals(1, target.getCallbacks().size());
+ assertNotNull(target.getCallbacks().get(viewId));
+ }
+
+ private RemoteViews.DrawInstructions getDrawInstructions() {
+ final byte[] first = new byte[] {'f', 'i', 'r', 's', 't'};
+ final byte[] second = new byte[] {'s', 'e', 'c', 'o', 'n', 'd'};
+ final RemoteViews.DrawInstructions drawInstructions =
+ new RemoteViews.DrawInstructions.Builder(
+ Collections.singletonList(first)).build();
+ drawInstructions.appendInstructions(second);
+ return drawInstructions;
+ }
+
private RemoteViews createViewChained(int depth, String... texts) {
RemoteViews result = new RemoteViews(mPackage, R.layout.remote_view_host);
diff --git a/core/tests/coretests/src/com/android/internal/logging/MetricsLoggerTest.java b/core/tests/coretests/src/com/android/internal/logging/MetricsLoggerTest.java
new file mode 100644
index 0000000..7054cc0
--- /dev/null
+++ b/core/tests/coretests/src/com/android/internal/logging/MetricsLoggerTest.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.logging;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.metrics.LogMaker;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.internal.logging.nano.MetricsProto;
+import com.android.internal.logging.testing.FakeMetricsLogger;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class MetricsLoggerTest {
+ private FakeMetricsLogger mLogger;
+
+ private static final int TEST_ACTION = 42;
+
+ @Before
+ public void setUp() throws Exception {
+ mLogger = new FakeMetricsLogger();
+ }
+
+ @Test
+ public void testEmpty() throws Exception {
+ assertThat(mLogger.getLogs().size()).isEqualTo(0);
+ }
+
+ @Test
+ public void testAction() throws Exception {
+ mLogger.action(TEST_ACTION);
+ assertThat(mLogger.getLogs().size()).isEqualTo(1);
+ final LogMaker event = mLogger.getLogs().peek();
+ assertThat(event.getType()).isEqualTo(MetricsProto.MetricsEvent.TYPE_ACTION);
+ assertThat(event.getCategory()).isEqualTo(TEST_ACTION);
+ }
+
+ @Test
+ public void testVisible() throws Exception {
+ // Limited testing to confirm we don't crash
+ mLogger.visible(TEST_ACTION);
+ mLogger.hidden(TEST_ACTION);
+ mLogger.visibility(TEST_ACTION, true);
+ mLogger.visibility(TEST_ACTION, false);
+ }
+}
diff --git a/core/tests/coretests/src/com/android/internal/logging/UiEventLoggerTest.java b/core/tests/coretests/src/com/android/internal/logging/UiEventLoggerTest.java
new file mode 100644
index 0000000..7840f71
--- /dev/null
+++ b/core/tests/coretests/src/com/android/internal/logging/UiEventLoggerTest.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.logging;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.internal.logging.testing.UiEventLoggerFake;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class UiEventLoggerTest {
+ private UiEventLoggerFake mLogger;
+
+ private static final int TEST_EVENT_ID = 42;
+ private static final int TEST_INSTANCE_ID = 21;
+
+ private enum MyUiEventEnum implements UiEventLogger.UiEventEnum {
+ @UiEvent(doc = "Example event")
+ TEST_EVENT(TEST_EVENT_ID);
+
+ private final int mId;
+
+ MyUiEventEnum(int id) {
+ mId = id;
+ }
+
+ @Override
+ public int getId() {
+ return mId;
+ }
+ }
+
+ private InstanceId TEST_INSTANCE = InstanceId.fakeInstanceId(TEST_INSTANCE_ID);
+
+ @Before
+ public void setUp() throws Exception {
+ mLogger = new UiEventLoggerFake();
+ }
+
+ @Test
+ public void testEmpty() throws Exception {
+ assertThat(mLogger.numLogs()).isEqualTo(0);
+ }
+
+ @Test
+ public void testSimple() throws Exception {
+ mLogger.log(MyUiEventEnum.TEST_EVENT);
+ assertThat(mLogger.numLogs()).isEqualTo(1);
+ assertThat(mLogger.eventId(0)).isEqualTo(TEST_EVENT_ID);
+ }
+
+ @Test
+ public void testWithInstance() throws Exception {
+ mLogger.log(MyUiEventEnum.TEST_EVENT, TEST_INSTANCE);
+ assertThat(mLogger.numLogs()).isEqualTo(1);
+ assertThat(mLogger.eventId(0)).isEqualTo(TEST_EVENT_ID);
+ assertThat(mLogger.get(0).instanceId.getId()).isEqualTo(TEST_INSTANCE_ID);
+ }
+}
diff --git a/core/tests/coretests/src/com/android/internal/os/MonotonicClockTest.java b/core/tests/coretests/src/com/android/internal/os/MonotonicClockTest.java
index ec4c563..06d888b 100644
--- a/core/tests/coretests/src/com/android/internal/os/MonotonicClockTest.java
+++ b/core/tests/coretests/src/com/android/internal/os/MonotonicClockTest.java
@@ -18,10 +18,14 @@
import static com.google.common.truth.Truth.assertThat;
+import android.platform.test.annotations.IgnoreUnderRavenwood;
+import android.platform.test.ravenwood.RavenwoodRule;
+
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
import org.junit.Before;
+import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -33,6 +37,9 @@
@RunWith(AndroidJUnit4.class)
@SmallTest
public class MonotonicClockTest {
+ @Rule
+ public final RavenwoodRule mRavenwood = new RavenwoodRule();
+
private final MockClock mClock = new MockClock();
private File mFile;
@@ -70,6 +77,7 @@
}
@Test
+ @IgnoreUnderRavenwood(reason = "b/321832617")
public void corruptedFile() throws IOException {
// Create an invalid binary XML file to cause IOException: "Unexpected magic number"
try (FileWriter w = new FileWriter(mFile)) {
diff --git a/core/tests/coretests/src/com/android/internal/widget/LockPatternUtilsTest.java b/core/tests/coretests/src/com/android/internal/widget/LockPatternUtilsTest.java
index 1a668f7..b90480a 100644
--- a/core/tests/coretests/src/com/android/internal/widget/LockPatternUtilsTest.java
+++ b/core/tests/coretests/src/com/android/internal/widget/LockPatternUtilsTest.java
@@ -16,20 +16,279 @@
package com.android.internal.widget;
+import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_MANAGED;
+import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED;
+
+import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_NOT_REQUIRED;
+import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_LOCKOUT;
+import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN;
+import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_TRUSTAGENT_EXPIRED;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyInt;
+import static org.mockito.Mockito.anyString;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import android.app.trust.TrustManager;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.ContextWrapper;
+import android.content.pm.UserInfo;
+import android.os.Looper;
+import android.os.RemoteException;
import android.os.UserHandle;
+import android.os.UserManager;
+import android.platform.test.annotations.IgnoreUnderRavenwood;
+import android.platform.test.ravenwood.RavenwoodRule;
+import android.provider.Settings;
+import android.test.mock.MockContentResolver;
+import androidx.test.InstrumentationRegistry;
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
+import com.android.internal.util.test.FakeSettingsProvider;
+
+import com.google.android.collect.Lists;
+
+import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+
+import java.nio.charset.StandardCharsets;
+import java.util.List;
@RunWith(AndroidJUnit4.class)
@SmallTest
+@IgnoreUnderRavenwood(blockedBy = LockPatternUtils.class)
public class LockPatternUtilsTest {
+ @Rule
+ public final RavenwoodRule mRavenwood = new RavenwoodRule();
+
+ private ILockSettings mLockSettings;
+ private static final int USER_ID = 1;
+ private static final int DEMO_USER_ID = 5;
+
+ private LockPatternUtils mLockPatternUtils;
+
+ private void configureTest(boolean isSecure, boolean isDemoUser, int deviceDemoMode)
+ throws Exception {
+ mLockSettings = mock(ILockSettings.class);
+ final Context context = spy(new ContextWrapper(InstrumentationRegistry.getTargetContext()));
+
+ final MockContentResolver cr = new MockContentResolver(context);
+ cr.addProvider(Settings.AUTHORITY, new FakeSettingsProvider());
+ when(context.getContentResolver()).thenReturn(cr);
+ Settings.Global.putInt(cr, Settings.Global.DEVICE_DEMO_MODE, deviceDemoMode);
+
+ when(mLockSettings.getCredentialType(DEMO_USER_ID)).thenReturn(
+ isSecure ? LockPatternUtils.CREDENTIAL_TYPE_PASSWORD
+ : LockPatternUtils.CREDENTIAL_TYPE_NONE);
+ when(mLockSettings.getLong("lockscreen.password_type", PASSWORD_QUALITY_UNSPECIFIED,
+ DEMO_USER_ID)).thenReturn((long) PASSWORD_QUALITY_MANAGED);
+ when(mLockSettings.hasSecureLockScreen()).thenReturn(true);
+ mLockPatternUtils = new LockPatternUtils(context, mLockSettings);
+
+ final UserInfo userInfo = mock(UserInfo.class);
+ when(userInfo.isDemo()).thenReturn(isDemoUser);
+ final UserManager um = mock(UserManager.class);
+ when(um.getUserInfo(DEMO_USER_ID)).thenReturn(userInfo);
+ when(context.getSystemService(Context.USER_SERVICE)).thenReturn(um);
+ }
+
+ @Test
+ public void isUserInLockDown() throws Exception {
+ configureTest(true, false, 2);
+
+ // GIVEN strong auth not required
+ when(mLockSettings.getStrongAuthForUser(USER_ID)).thenReturn(STRONG_AUTH_NOT_REQUIRED);
+
+ // THEN user isn't in lockdown
+ assertFalse(mLockPatternUtils.isUserInLockdown(USER_ID));
+
+ // GIVEN lockdown
+ when(mLockSettings.getStrongAuthForUser(USER_ID)).thenReturn(
+ STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN);
+
+ // THEN user is in lockdown
+ assertTrue(mLockPatternUtils.isUserInLockdown(USER_ID));
+
+ // GIVEN lockdown and lockout
+ when(mLockSettings.getStrongAuthForUser(USER_ID)).thenReturn(
+ STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN | STRONG_AUTH_REQUIRED_AFTER_LOCKOUT);
+
+ // THEN user is in lockdown
+ assertTrue(mLockPatternUtils.isUserInLockdown(USER_ID));
+ }
+
+ @Test
+ public void isLockScreenDisabled_isDemoUser_true() throws Exception {
+ configureTest(false, true, 2);
+ assertTrue(mLockPatternUtils.isLockScreenDisabled(DEMO_USER_ID));
+ }
+
+ @Test
+ public void isLockScreenDisabled_isSecureAndDemoUser_false() throws Exception {
+ configureTest(true, true, 2);
+ assertFalse(mLockPatternUtils.isLockScreenDisabled(DEMO_USER_ID));
+ }
+
+ @Test
+ public void isLockScreenDisabled_isNotDemoUser_false() throws Exception {
+ configureTest(false, false, 2);
+ assertFalse(mLockPatternUtils.isLockScreenDisabled(DEMO_USER_ID));
+ }
+
+ @Test
+ public void isLockScreenDisabled_isNotInDemoMode_false() throws Exception {
+ configureTest(false, true, 0);
+ assertFalse(mLockPatternUtils.isLockScreenDisabled(DEMO_USER_ID));
+ }
+
+ @Test
+ public void testAddWeakEscrowToken() throws RemoteException {
+ ILockSettings ils = createTestLockSettings();
+ byte[] testToken = "test_token".getBytes(StandardCharsets.UTF_8);
+ int testUserId = 10;
+ IWeakEscrowTokenActivatedListener listener = createWeakEscrowTokenListener();
+ mLockPatternUtils.addWeakEscrowToken(testToken, testUserId, listener);
+ verify(ils).addWeakEscrowToken(eq(testToken), eq(testUserId), eq(listener));
+ }
+
+ @Test
+ public void testRegisterWeakEscrowTokenRemovedListener() throws RemoteException {
+ ILockSettings ils = createTestLockSettings();
+ IWeakEscrowTokenRemovedListener testListener = createTestAutoEscrowTokenRemovedListener();
+ mLockPatternUtils.registerWeakEscrowTokenRemovedListener(testListener);
+ verify(ils).registerWeakEscrowTokenRemovedListener(eq(testListener));
+ }
+
+ @Test
+ public void testUnregisterWeakEscrowTokenRemovedListener() throws RemoteException {
+ ILockSettings ils = createTestLockSettings();
+ IWeakEscrowTokenRemovedListener testListener = createTestAutoEscrowTokenRemovedListener();
+ mLockPatternUtils.unregisterWeakEscrowTokenRemovedListener(testListener);
+ verify(ils).unregisterWeakEscrowTokenRemovedListener(eq(testListener));
+ }
+
+ @Test
+ public void testRemoveAutoEscrowToken() throws RemoteException {
+ ILockSettings ils = createTestLockSettings();
+ int testUserId = 10;
+ long testHandle = 100L;
+ mLockPatternUtils.removeWeakEscrowToken(testHandle, testUserId);
+ verify(ils).removeWeakEscrowToken(eq(testHandle), eq(testUserId));
+ }
+
+ @Test
+ public void testIsAutoEscrowTokenActive() throws RemoteException {
+ ILockSettings ils = createTestLockSettings();
+ int testUserId = 10;
+ long testHandle = 100L;
+ mLockPatternUtils.isWeakEscrowTokenActive(testHandle, testUserId);
+ verify(ils).isWeakEscrowTokenActive(eq(testHandle), eq(testUserId));
+ }
+
+ @Test
+ public void testIsAutoEscrowTokenValid() throws RemoteException {
+ ILockSettings ils = createTestLockSettings();
+ int testUserId = 10;
+ byte[] testToken = "test_token".getBytes(StandardCharsets.UTF_8);
+ long testHandle = 100L;
+ mLockPatternUtils.isWeakEscrowTokenValid(testHandle, testToken, testUserId);
+ verify(ils).isWeakEscrowTokenValid(eq(testHandle), eq(testToken), eq(testUserId));
+ }
+
+ @Test
+ public void testSetEnabledTrustAgents() throws RemoteException {
+ int testUserId = 10;
+ ILockSettings ils = createTestLockSettings();
+ ArgumentCaptor<String> valueCaptor = ArgumentCaptor.forClass(String.class);
+ doNothing().when(ils).setString(anyString(), valueCaptor.capture(), anyInt());
+ List<ComponentName> enabledTrustAgents = Lists.newArrayList(
+ ComponentName.unflattenFromString("com.android/.TrustAgent"),
+ ComponentName.unflattenFromString("com.test/.TestAgent"));
+
+ mLockPatternUtils.setEnabledTrustAgents(enabledTrustAgents, testUserId);
+
+ assertThat(valueCaptor.getValue()).isEqualTo("com.android/.TrustAgent,com.test/.TestAgent");
+ }
+
+ @Test
+ public void testGetEnabledTrustAgents() throws RemoteException {
+ int testUserId = 10;
+ ILockSettings ils = createTestLockSettings();
+ when(ils.getString(anyString(), any(), anyInt())).thenReturn(
+ "com.android/.TrustAgent,com.test/.TestAgent");
+
+ List<ComponentName> trustAgents = mLockPatternUtils.getEnabledTrustAgents(testUserId);
+
+ assertThat(trustAgents).containsExactly(
+ ComponentName.unflattenFromString("com.android/.TrustAgent"),
+ ComponentName.unflattenFromString("com.test/.TestAgent"));
+ }
+
+ @Test
+ public void testSetKnownTrustAgents() throws RemoteException {
+ int testUserId = 10;
+ ILockSettings ils = createTestLockSettings();
+ ArgumentCaptor<String> valueCaptor = ArgumentCaptor.forClass(String.class);
+ doNothing().when(ils).setString(anyString(), valueCaptor.capture(), anyInt());
+ List<ComponentName> knownTrustAgents = Lists.newArrayList(
+ ComponentName.unflattenFromString("com.android/.TrustAgent"),
+ ComponentName.unflattenFromString("com.test/.TestAgent"));
+
+ mLockPatternUtils.setKnownTrustAgents(knownTrustAgents, testUserId);
+
+ assertThat(valueCaptor.getValue()).isEqualTo("com.android/.TrustAgent,com.test/.TestAgent");
+ }
+
+ @Test
+ public void testGetKnownTrustAgents() throws RemoteException {
+ int testUserId = 10;
+ ILockSettings ils = createTestLockSettings();
+ when(ils.getString(anyString(), any(), anyInt())).thenReturn(
+ "com.android/.TrustAgent,com.test/.TestAgent");
+
+ List<ComponentName> trustAgents = mLockPatternUtils.getKnownTrustAgents(testUserId);
+
+ assertThat(trustAgents).containsExactly(
+ ComponentName.unflattenFromString("com.android/.TrustAgent"),
+ ComponentName.unflattenFromString("com.test/.TestAgent"));
+ }
+
+ @Test
+ public void isBiometricAllowedForUser_afterTrustagentExpired_returnsTrue()
+ throws RemoteException {
+ TestStrongAuthTracker tracker = createStrongAuthTracker();
+ tracker.changeStrongAuth(SOME_AUTH_REQUIRED_AFTER_TRUSTAGENT_EXPIRED);
+
+ assertTrue(tracker.isBiometricAllowedForUser(
+ /* isStrongBiometric = */ true,
+ DEMO_USER_ID));
+ }
+
+ @Test
+ public void isBiometricAllowedForUser_afterLockout_returnsFalse()
+ throws RemoteException {
+ TestStrongAuthTracker tracker = createStrongAuthTracker();
+ tracker.changeStrongAuth(STRONG_AUTH_REQUIRED_AFTER_LOCKOUT);
+
+ assertFalse(tracker.isBiometricAllowedForUser(
+ /* isStrongBiometric = */ true,
+ DEMO_USER_ID));
+ }
@Test
public void testUserFrp_isNotRegularUser() throws Exception {
@@ -56,4 +315,49 @@
assertNotEquals(UserHandle.USER_CURRENT, LockPatternUtils.USER_REPAIR_MODE);
assertNotEquals(UserHandle.USER_CURRENT_OR_SELF, LockPatternUtils.USER_REPAIR_MODE);
}
+
+ private TestStrongAuthTracker createStrongAuthTracker() {
+ final Context context = new ContextWrapper(InstrumentationRegistry.getTargetContext());
+ return new TestStrongAuthTracker(context, Looper.getMainLooper());
+ }
+
+ private static class TestStrongAuthTracker extends LockPatternUtils.StrongAuthTracker {
+
+ TestStrongAuthTracker(Context context, Looper looper) {
+ super(context, looper);
+ }
+
+ public void changeStrongAuth(@StrongAuthFlags int strongAuthFlags) {
+ handleStrongAuthRequiredChanged(strongAuthFlags, DEMO_USER_ID);
+ }
+ }
+
+ private ILockSettings createTestLockSettings() {
+ final Context context = spy(new ContextWrapper(InstrumentationRegistry.getTargetContext()));
+
+ final TrustManager trustManager = mock(TrustManager.class);
+ when(context.getSystemService(Context.TRUST_SERVICE)).thenReturn(trustManager);
+
+ final ILockSettings ils = mock(ILockSettings.class);
+ mLockPatternUtils = new LockPatternUtils(context, ils);
+ return ils;
+ }
+
+ private IWeakEscrowTokenActivatedListener createWeakEscrowTokenListener() {
+ return new IWeakEscrowTokenActivatedListener.Stub() {
+ @Override
+ public void onWeakEscrowTokenActivated(long handle, int userId) {
+ // Do nothing.
+ }
+ };
+ }
+
+ private IWeakEscrowTokenRemovedListener createTestAutoEscrowTokenRemovedListener() {
+ return new IWeakEscrowTokenRemovedListener.Stub() {
+ @Override
+ public void onWeakEscrowTokenRemoved(long handle, int userId) {
+ // Do nothing.
+ }
+ };
+ }
}
diff --git a/core/tests/systemproperties/Android.bp b/core/tests/systemproperties/Android.bp
index 765ca3e..21aa3c44 100644
--- a/core/tests/systemproperties/Android.bp
+++ b/core/tests/systemproperties/Android.bp
@@ -15,6 +15,9 @@
static_libs: [
"android-common",
"frameworks-core-util-lib",
+ "androidx.test.rules",
+ "androidx.test.ext.junit",
+ "ravenwood-junit",
],
libs: [
"android.test.runner",
@@ -23,3 +26,22 @@
platform_apis: true,
certificate: "platform",
}
+
+android_ravenwood_test {
+ name: "FrameworksCoreSystemPropertiesTestsRavenwood",
+ static_libs: [
+ "android-common",
+ "frameworks-core-util-lib",
+ "androidx.test.rules",
+ "androidx.test.ext.junit",
+ "ravenwood-junit",
+ ],
+ libs: [
+ "android.test.runner",
+ "android.test.base",
+ ],
+ srcs: [
+ "src/**/*.java",
+ ],
+ auto_gen_config: true,
+}
diff --git a/core/tests/systemproperties/src/android/os/SystemPropertiesTest.java b/core/tests/systemproperties/src/android/os/SystemPropertiesTest.java
index 67783bf..ea65de0 100644
--- a/core/tests/systemproperties/src/android/os/SystemPropertiesTest.java
+++ b/core/tests/systemproperties/src/android/os/SystemPropertiesTest.java
@@ -16,19 +16,36 @@
package android.os;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import android.platform.test.ravenwood.RavenwoodRule;
import android.test.suitebuilder.annotation.SmallTest;
-import junit.framework.TestCase;
+import org.junit.Rule;
+import org.junit.Test;
import java.util.Objects;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
-public class SystemPropertiesTest extends TestCase {
+public class SystemPropertiesTest {
+ @Rule
+ public final RavenwoodRule mRavenwood = new RavenwoodRule.Builder()
+ .setSystemPropertyMutable(KEY, null)
+ .setSystemPropertyMutable(UNSET_KEY, null)
+ .setSystemPropertyMutable(PERSIST_KEY, null)
+ .build();
+
private static final String KEY = "sys.testkey";
private static final String UNSET_KEY = "Aiw7woh6ie4toh7W";
private static final String PERSIST_KEY = "persist.sys.testkey";
+ @Test
@SmallTest
public void testStressPersistPropertyConsistency() throws Exception {
for (int i = 0; i < 100; ++i) {
@@ -38,6 +55,7 @@
}
}
+ @Test
@SmallTest
public void testStressMemoryPropertyConsistency() throws Exception {
for (int i = 0; i < 100; ++i) {
@@ -47,6 +65,7 @@
}
}
+ @Test
@SmallTest
public void testProperties() throws Exception {
String value;
@@ -93,6 +112,7 @@
assertEquals(expected, value);
}
+ @Test
@SmallTest
public void testHandle() throws Exception {
String value;
@@ -114,6 +134,7 @@
assertEquals(12345, handle.getInt(12345));
}
+ @Test
@SmallTest
public void testIntegralProperties() throws Exception {
testInt("", 123, 123);
@@ -133,6 +154,7 @@
testLong("-3147483647", 124, -3147483647L);
}
+ @Test
@SmallTest
public void testUnset() throws Exception {
assertEquals("abc", SystemProperties.get(UNSET_KEY, "abc"));
@@ -142,6 +164,7 @@
assertEquals(-10, SystemProperties.getLong(UNSET_KEY, -10));
}
+ @Test
@SmallTest
@SuppressWarnings("null")
public void testNullKey() throws Exception {
@@ -176,6 +199,7 @@
}
}
+ @Test
@SmallTest
public void testCallbacks() {
// Latches are not really necessary, but are easy to use.
@@ -220,6 +244,7 @@
}
}
+ @Test
@SmallTest
public void testDigestOf() {
final String empty = SystemProperties.digestOf();
diff --git a/core/tests/utiltests/src/com/android/internal/util/LockPatternUtilsTest.java b/core/tests/utiltests/src/com/android/internal/util/LockPatternUtilsTest.java
deleted file mode 100644
index dcaf676..0000000
--- a/core/tests/utiltests/src/com/android/internal/util/LockPatternUtilsTest.java
+++ /dev/null
@@ -1,339 +0,0 @@
-/*
- * Copyright (C) 2017 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.util;
-
-import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_MANAGED;
-import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED;
-
-import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_NOT_REQUIRED;
-import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_LOCKOUT;
-import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN;
-import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_TRUSTAGENT_EXPIRED;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.any;
-import static org.mockito.Mockito.anyInt;
-import static org.mockito.Mockito.anyString;
-import static org.mockito.Mockito.doNothing;
-import static org.mockito.Mockito.doReturn;
-import static org.mockito.Mockito.spy;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.ContextWrapper;
-import android.content.pm.UserInfo;
-import android.os.Looper;
-import android.os.RemoteException;
-import android.os.UserManager;
-import android.platform.test.annotations.IgnoreUnderRavenwood;
-import android.platform.test.ravenwood.RavenwoodRule;
-import android.provider.Settings;
-import android.test.mock.MockContentResolver;
-
-import androidx.test.InstrumentationRegistry;
-import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
-
-import com.android.internal.util.test.FakeSettingsProvider;
-import com.android.internal.widget.ILockSettings;
-import com.android.internal.widget.IWeakEscrowTokenActivatedListener;
-import com.android.internal.widget.IWeakEscrowTokenRemovedListener;
-import com.android.internal.widget.LockPatternUtils;
-
-import com.google.android.collect.Lists;
-
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.ArgumentCaptor;
-import org.mockito.Mockito;
-
-import java.nio.charset.StandardCharsets;
-import java.util.List;
-
-@RunWith(AndroidJUnit4.class)
-@SmallTest
-@IgnoreUnderRavenwood(blockedBy = LockPatternUtils.class)
-public class LockPatternUtilsTest {
- @Rule
- public final RavenwoodRule mRavenwood = new RavenwoodRule();
-
- private ILockSettings mLockSettings;
- private static final int USER_ID = 1;
- private static final int DEMO_USER_ID = 5;
-
- private LockPatternUtils mLockPatternUtils;
-
- private void configureTest(boolean isSecure, boolean isDemoUser, int deviceDemoMode)
- throws Exception {
- mLockSettings = Mockito.mock(ILockSettings.class);
- final Context context = spy(new ContextWrapper(InstrumentationRegistry.getTargetContext()));
-
- final MockContentResolver cr = new MockContentResolver(context);
- cr.addProvider(Settings.AUTHORITY, new FakeSettingsProvider());
- when(context.getContentResolver()).thenReturn(cr);
- Settings.Global.putInt(cr, Settings.Global.DEVICE_DEMO_MODE, deviceDemoMode);
-
- when(mLockSettings.getCredentialType(DEMO_USER_ID)).thenReturn(
- isSecure ? LockPatternUtils.CREDENTIAL_TYPE_PASSWORD
- : LockPatternUtils.CREDENTIAL_TYPE_NONE);
- when(mLockSettings.getLong("lockscreen.password_type", PASSWORD_QUALITY_UNSPECIFIED,
- DEMO_USER_ID)).thenReturn((long) PASSWORD_QUALITY_MANAGED);
- // TODO(b/63758238): stop spying the class under test
- mLockPatternUtils = spy(new LockPatternUtils(context));
- when(mLockPatternUtils.getLockSettings()).thenReturn(mLockSettings);
- doReturn(true).when(mLockPatternUtils).hasSecureLockScreen();
-
- final UserInfo userInfo = Mockito.mock(UserInfo.class);
- when(userInfo.isDemo()).thenReturn(isDemoUser);
- final UserManager um = Mockito.mock(UserManager.class);
- when(um.getUserInfo(DEMO_USER_ID)).thenReturn(userInfo);
- when(context.getSystemService(Context.USER_SERVICE)).thenReturn(um);
- }
-
- @Test
- public void isUserInLockDown() throws Exception {
- configureTest(true, false, 2);
-
- // GIVEN strong auth not required
- when(mLockSettings.getStrongAuthForUser(USER_ID)).thenReturn(STRONG_AUTH_NOT_REQUIRED);
-
- // THEN user isn't in lockdown
- assertFalse(mLockPatternUtils.isUserInLockdown(USER_ID));
-
- // GIVEN lockdown
- when(mLockSettings.getStrongAuthForUser(USER_ID)).thenReturn(
- STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN);
-
- // THEN user is in lockdown
- assertTrue(mLockPatternUtils.isUserInLockdown(USER_ID));
-
- // GIVEN lockdown and lockout
- when(mLockSettings.getStrongAuthForUser(USER_ID)).thenReturn(
- STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN | STRONG_AUTH_REQUIRED_AFTER_LOCKOUT);
-
- // THEN user is in lockdown
- assertTrue(mLockPatternUtils.isUserInLockdown(USER_ID));
- }
-
- @Test
- public void isLockScreenDisabled_isDemoUser_true() throws Exception {
- configureTest(false, true, 2);
- assertTrue(mLockPatternUtils.isLockScreenDisabled(DEMO_USER_ID));
- }
-
- @Test
- public void isLockScreenDisabled_isSecureAndDemoUser_false() throws Exception {
- configureTest(true, true, 2);
- assertFalse(mLockPatternUtils.isLockScreenDisabled(DEMO_USER_ID));
- }
-
- @Test
- public void isLockScreenDisabled_isNotDemoUser_false() throws Exception {
- configureTest(false, false, 2);
- assertFalse(mLockPatternUtils.isLockScreenDisabled(DEMO_USER_ID));
- }
-
- @Test
- public void isLockScreenDisabled_isNotInDemoMode_false() throws Exception {
- configureTest(false, true, 0);
- assertFalse(mLockPatternUtils.isLockScreenDisabled(DEMO_USER_ID));
- }
-
- @Test
- public void testAddWeakEscrowToken() throws RemoteException {
- ILockSettings ils = createTestLockSettings();
- byte[] testToken = "test_token".getBytes(StandardCharsets.UTF_8);
- int testUserId = 10;
- IWeakEscrowTokenActivatedListener listener = createWeakEscrowTokenListener();
- mLockPatternUtils.addWeakEscrowToken(testToken, testUserId, listener);
- verify(ils).addWeakEscrowToken(eq(testToken), eq(testUserId), eq(listener));
- }
-
- @Test
- public void testRegisterWeakEscrowTokenRemovedListener() throws RemoteException {
- ILockSettings ils = createTestLockSettings();
- IWeakEscrowTokenRemovedListener testListener = createTestAutoEscrowTokenRemovedListener();
- mLockPatternUtils.registerWeakEscrowTokenRemovedListener(testListener);
- verify(ils).registerWeakEscrowTokenRemovedListener(eq(testListener));
- }
-
- @Test
- public void testUnregisterWeakEscrowTokenRemovedListener() throws RemoteException {
- ILockSettings ils = createTestLockSettings();
- IWeakEscrowTokenRemovedListener testListener = createTestAutoEscrowTokenRemovedListener();
- mLockPatternUtils.unregisterWeakEscrowTokenRemovedListener(testListener);
- verify(ils).unregisterWeakEscrowTokenRemovedListener(eq(testListener));
- }
-
- @Test
- public void testRemoveAutoEscrowToken() throws RemoteException {
- ILockSettings ils = createTestLockSettings();
- int testUserId = 10;
- long testHandle = 100L;
- mLockPatternUtils.removeWeakEscrowToken(testHandle, testUserId);
- verify(ils).removeWeakEscrowToken(eq(testHandle), eq(testUserId));
- }
-
- @Test
- public void testIsAutoEscrowTokenActive() throws RemoteException {
- ILockSettings ils = createTestLockSettings();
- int testUserId = 10;
- long testHandle = 100L;
- mLockPatternUtils.isWeakEscrowTokenActive(testHandle, testUserId);
- verify(ils).isWeakEscrowTokenActive(eq(testHandle), eq(testUserId));
- }
-
- @Test
- public void testIsAutoEscrowTokenValid() throws RemoteException {
- ILockSettings ils = createTestLockSettings();
- int testUserId = 10;
- byte[] testToken = "test_token".getBytes(StandardCharsets.UTF_8);
- long testHandle = 100L;
- mLockPatternUtils.isWeakEscrowTokenValid(testHandle, testToken, testUserId);
- verify(ils).isWeakEscrowTokenValid(eq(testHandle), eq(testToken), eq(testUserId));
- }
-
- @Test
- public void testSetEnabledTrustAgents() throws RemoteException {
- int testUserId = 10;
- ILockSettings ils = createTestLockSettings();
- ArgumentCaptor<String> valueCaptor = ArgumentCaptor.forClass(String.class);
- doNothing().when(ils).setString(anyString(), valueCaptor.capture(), anyInt());
- List<ComponentName> enabledTrustAgents = Lists.newArrayList(
- ComponentName.unflattenFromString("com.android/.TrustAgent"),
- ComponentName.unflattenFromString("com.test/.TestAgent"));
-
- mLockPatternUtils.setEnabledTrustAgents(enabledTrustAgents, testUserId);
-
- assertThat(valueCaptor.getValue()).isEqualTo("com.android/.TrustAgent,com.test/.TestAgent");
- }
-
- @Test
- public void testGetEnabledTrustAgents() throws RemoteException {
- int testUserId = 10;
- ILockSettings ils = createTestLockSettings();
- when(ils.getString(anyString(), any(), anyInt())).thenReturn(
- "com.android/.TrustAgent,com.test/.TestAgent");
-
- List<ComponentName> trustAgents = mLockPatternUtils.getEnabledTrustAgents(testUserId);
-
- assertThat(trustAgents).containsExactly(
- ComponentName.unflattenFromString("com.android/.TrustAgent"),
- ComponentName.unflattenFromString("com.test/.TestAgent"));
- }
-
- @Test
- public void testSetKnownTrustAgents() throws RemoteException {
- int testUserId = 10;
- ILockSettings ils = createTestLockSettings();
- ArgumentCaptor<String> valueCaptor = ArgumentCaptor.forClass(String.class);
- doNothing().when(ils).setString(anyString(), valueCaptor.capture(), anyInt());
- List<ComponentName> knownTrustAgents = Lists.newArrayList(
- ComponentName.unflattenFromString("com.android/.TrustAgent"),
- ComponentName.unflattenFromString("com.test/.TestAgent"));
-
- mLockPatternUtils.setKnownTrustAgents(knownTrustAgents, testUserId);
-
- assertThat(valueCaptor.getValue()).isEqualTo("com.android/.TrustAgent,com.test/.TestAgent");
- }
-
- @Test
- public void testGetKnownTrustAgents() throws RemoteException {
- int testUserId = 10;
- ILockSettings ils = createTestLockSettings();
- when(ils.getString(anyString(), any(), anyInt())).thenReturn(
- "com.android/.TrustAgent,com.test/.TestAgent");
-
- List<ComponentName> trustAgents = mLockPatternUtils.getKnownTrustAgents(testUserId);
-
- assertThat(trustAgents).containsExactly(
- ComponentName.unflattenFromString("com.android/.TrustAgent"),
- ComponentName.unflattenFromString("com.test/.TestAgent"));
- }
-
- @Test
- public void isBiometricAllowedForUser_afterTrustagentExpired_returnsTrue()
- throws RemoteException {
- TestStrongAuthTracker tracker = createStrongAuthTracker();
- tracker.changeStrongAuth(SOME_AUTH_REQUIRED_AFTER_TRUSTAGENT_EXPIRED);
-
- assertTrue(tracker.isBiometricAllowedForUser(
- /* isStrongBiometric = */ true,
- DEMO_USER_ID));
- }
-
- @Test
- public void isBiometricAllowedForUser_afterLockout_returnsFalse()
- throws RemoteException {
- TestStrongAuthTracker tracker = createStrongAuthTracker();
- tracker.changeStrongAuth(STRONG_AUTH_REQUIRED_AFTER_LOCKOUT);
-
- assertFalse(tracker.isBiometricAllowedForUser(
- /* isStrongBiometric = */ true,
- DEMO_USER_ID));
- }
-
-
- private TestStrongAuthTracker createStrongAuthTracker() {
- final Context context = new ContextWrapper(InstrumentationRegistry.getTargetContext());
- return new TestStrongAuthTracker(context, Looper.getMainLooper());
- }
-
- private static class TestStrongAuthTracker extends LockPatternUtils.StrongAuthTracker {
-
- TestStrongAuthTracker(Context context, Looper looper) {
- super(context, looper);
- }
-
- public void changeStrongAuth(@StrongAuthFlags int strongAuthFlags) {
- handleStrongAuthRequiredChanged(strongAuthFlags, DEMO_USER_ID);
- }
- }
-
- private ILockSettings createTestLockSettings() {
- final Context context = spy(new ContextWrapper(InstrumentationRegistry.getTargetContext()));
- mLockPatternUtils = spy(new LockPatternUtils(context));
- final ILockSettings ils = Mockito.mock(ILockSettings.class);
- when(mLockPatternUtils.getLockSettings()).thenReturn(ils);
- return ils;
- }
-
- private IWeakEscrowTokenActivatedListener createWeakEscrowTokenListener() {
- return new IWeakEscrowTokenActivatedListener.Stub() {
- @Override
- public void onWeakEscrowTokenActivated(long handle, int userId) {
- // Do nothing.
- }
- };
- }
-
- private IWeakEscrowTokenRemovedListener createTestAutoEscrowTokenRemovedListener() {
- return new IWeakEscrowTokenRemovedListener.Stub() {
- @Override
- public void onWeakEscrowTokenRemoved(long handle, int userId) {
- // Do nothing.
- }
- };
- }
-}
diff --git a/data/etc/privapp-permissions-platform.xml b/data/etc/privapp-permissions-platform.xml
index 294b8ae..2de305f 100644
--- a/data/etc/privapp-permissions-platform.xml
+++ b/data/etc/privapp-permissions-platform.xml
@@ -421,6 +421,8 @@
<permission name="android.permission.REQUEST_COMPANION_PROFILE_NEARBY_DEVICE_STREAMING" />
<permission name="android.permission.REQUEST_COMPANION_PROFILE_COMPUTER" />
<permission name="android.permission.REQUEST_COMPANION_SELF_MANAGED" />
+ <permission name="android.permission.REQUEST_OBSERVE_DEVICE_UUID_PRESENCE" />
+
<!-- Permission required for testing registering pull atom callbacks. -->
<permission name="android.permission.REGISTER_STATS_PULL_ATOM"/>
<!-- Permission required for testing system audio effect APIs. -->
@@ -637,4 +639,8 @@
<permission name="com.android.voicemail.permission.READ_VOICEMAIL"/>
<permission name="android.permission.READ_SYSTEM_GRAMMATICAL_GENDER"/>
</privapp-permissions>
+
+ <privapp-permissions package="com.android.devicediagnostics">
+ <permission name="android.permission.READ_PRIVILEGED_PHONE_STATE"/>
+ </privapp-permissions>
</permissions>
diff --git a/data/etc/services.core.protolog.json b/data/etc/services.core.protolog.json
index c77004d..da91a96 100644
--- a/data/etc/services.core.protolog.json
+++ b/data/etc/services.core.protolog.json
@@ -1867,6 +1867,12 @@
"group": "WM_DEBUG_STATES",
"at": "com\/android\/server\/wm\/ActivityTaskSupervisor.java"
},
+ "-483957611": {
+ "message": "Resuming configuration dispatch for %s",
+ "level": "VERBOSE",
+ "group": "WM_DEBUG_WINDOW_TRANSITIONS_MIN",
+ "at": "com\/android\/server\/wm\/ActivityRecord.java"
+ },
"-481924678": {
"message": "handleNotObscuredLocked w: %s, w.mHasSurface: %b, w.isOnScreen(): %b, w.isDisplayedLw(): %b, w.mAttrs.userActivityTimeout: %d",
"level": "DEBUG",
@@ -4021,6 +4027,12 @@
"group": "WM_DEBUG_WINDOW_TRANSITIONS",
"at": "com\/android\/server\/wm\/Transition.java"
},
+ "1473051122": {
+ "message": "Pausing configuration dispatch for %s",
+ "level": "VERBOSE",
+ "group": "WM_DEBUG_WINDOW_TRANSITIONS_MIN",
+ "at": "com\/android\/server\/wm\/ActivityRecord.java"
+ },
"1494644409": {
"message": " Rejecting as detached: %s",
"level": "VERBOSE",
diff --git a/data/fonts/Android.bp b/data/fonts/Android.bp
index 471acaa..f1a6b69 100644
--- a/data/fonts/Android.bp
+++ b/data/fonts/Android.bp
@@ -80,3 +80,9 @@
},
},
}
+
+/////////////////////////////////
+// Move `fontchain_lint` to `core/tasks/fontchain_lint.mk`.
+// Because `system.img` is a dependency of `fontchain_lint`, it cannot be
+// converted to Android.bp for now.
+// After system.img can be generated by Soong, then it can be converted to Android.bp.
diff --git a/data/fonts/Android.mk b/data/fonts/Android.mk
deleted file mode 100644
index a322b82..0000000
--- a/data/fonts/Android.mk
+++ /dev/null
@@ -1,45 +0,0 @@
-# Copyright (C) 2011 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.
-
-LOCAL_PATH := $(call my-dir)
-
-# Run sanity tests on fonts on checkbuild
-checkbuild: fontchain_lint
-
-FONTCHAIN_LINTER := $(HOST_OUT_EXECUTABLES)/fontchain_linter
-ifeq ($(MINIMAL_FONT_FOOTPRINT),true)
-CHECK_EMOJI := false
-else
-CHECK_EMOJI := true
-endif
-
-fontchain_lint_timestamp := $(call intermediates-dir-for,PACKAGING,fontchain_lint)/stamp
-
-.PHONY: fontchain_lint
-fontchain_lint: $(fontchain_lint_timestamp)
-
-fontchain_lint_deps := \
- external/unicode/DerivedAge.txt \
- external/unicode/emoji-data.txt \
- external/unicode/emoji-sequences.txt \
- external/unicode/emoji-variation-sequences.txt \
- external/unicode/emoji-zwj-sequences.txt \
- external/unicode/additions/emoji-data.txt \
- external/unicode/additions/emoji-sequences.txt \
- external/unicode/additions/emoji-zwj-sequences.txt \
-
-$(fontchain_lint_timestamp): $(FONTCHAIN_LINTER) $(TARGET_OUT)/etc/fonts.xml $(PRODUCT_OUT)/system.img $(fontchain_lint_deps)
- @echo Running fontchain lint
- $(FONTCHAIN_LINTER) $(TARGET_OUT) $(CHECK_EMOJI) external/unicode
- touch $@
diff --git a/graphics/java/android/graphics/BaseRecordingCanvas.java b/graphics/java/android/graphics/BaseRecordingCanvas.java
index d659ddd..4e88b0e 100644
--- a/graphics/java/android/graphics/BaseRecordingCanvas.java
+++ b/graphics/java/android/graphics/BaseRecordingCanvas.java
@@ -607,7 +607,8 @@
}
@Override
- public final void drawMesh(@NonNull Mesh mesh, BlendMode blendMode, @NonNull Paint paint) {
+ public final void drawMesh(@NonNull Mesh mesh, @Nullable BlendMode blendMode,
+ @NonNull Paint paint) {
if (blendMode == null) {
blendMode = BlendMode.MODULATE;
}
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 83d555c..b2e5b75 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
@@ -17,6 +17,7 @@
package androidx.window.extensions.embedding;
import static android.app.ActivityManager.START_SUCCESS;
+import static android.app.ActivityOptions.KEY_LAUNCH_TASK_FRAGMENT_TOKEN;
import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
import static android.view.Display.DEFAULT_DISPLAY;
@@ -41,10 +42,10 @@
import static androidx.window.extensions.embedding.SplitContainer.shouldFinishAssociatedContainerWhenAdjacent;
import static androidx.window.extensions.embedding.SplitContainer.shouldFinishAssociatedContainerWhenStacked;
import static androidx.window.extensions.embedding.SplitPresenter.RESULT_EXPAND_FAILED_NO_TF_INFO;
-import static androidx.window.extensions.embedding.SplitPresenter.boundsSmallerThanMinDimensions;
import static androidx.window.extensions.embedding.SplitPresenter.getActivitiesMinDimensionsPair;
import static androidx.window.extensions.embedding.SplitPresenter.getActivityIntentMinDimensionsPair;
import static androidx.window.extensions.embedding.SplitPresenter.getMinDimensions;
+import static androidx.window.extensions.embedding.SplitPresenter.sanitizeBounds;
import static androidx.window.extensions.embedding.SplitPresenter.shouldShowSplit;
import android.annotation.CallbackExecutor;
@@ -110,6 +111,10 @@
static final boolean ENABLE_SHELL_TRANSITIONS =
SystemProperties.getBoolean("persist.wm.debug.shell_transit", true);
+ // TODO(b/295993745): remove after prebuilt library is updated.
+ private static final String KEY_ACTIVITY_STACK_TOKEN =
+ "androidx.window.extensions.embedding.ActivityStackToken";
+
@VisibleForTesting
@GuardedBy("mLock")
final SplitPresenter mPresenter;
@@ -569,7 +574,8 @@
final TransactionRecord transactionRecord = mTransactionManager.startNewTransaction();
final WindowContainerTransaction wct = transactionRecord.getTransaction();
- mPresenter.applyActivityStackAttributes(wct, container, attributes);
+ mPresenter.applyActivityStackAttributes(wct, container, attributes,
+ container.getMinDimensions());
transactionRecord.apply(false /* shouldApplyIndependently */);
}
}
@@ -1562,7 +1568,8 @@
private TaskFragmentContainer createEmptyExpandedContainer(
@NonNull WindowContainerTransaction wct, @NonNull Intent intent, int taskId,
@Nullable Activity launchingActivity) {
- return createEmptyContainer(wct, intent, taskId, new Rect(), launchingActivity,
+ return createEmptyContainer(wct, intent, taskId,
+ new ActivityStackAttributes.Builder().build(), launchingActivity,
null /* overlayTag */, null /* launchOptions */);
}
@@ -1576,8 +1583,9 @@
@Nullable
TaskFragmentContainer createEmptyContainer(
@NonNull WindowContainerTransaction wct, @NonNull Intent intent, int taskId,
- @NonNull Rect bounds, @Nullable Activity launchingActivity,
- @Nullable String overlayTag, @Nullable Bundle launchOptions) {
+ @NonNull ActivityStackAttributes activityStackAttributes,
+ @Nullable Activity launchingActivity, @Nullable String overlayTag,
+ @Nullable Bundle launchOptions) {
// We need an activity in the organizer process in the same Task to use as the owner
// activity, as well as to get the Task window info.
final Activity activityInTask;
@@ -1600,43 +1608,21 @@
// Note that taskContainer will not exist before calling #newContainer if the container
// is the first embedded TF in the task.
final TaskContainer taskContainer = container.getTaskContainer();
- final Rect taskBounds = taskContainer.getTaskProperties().getTaskMetrics().getBounds();
- final Rect sanitizedBounds = sanitizeBounds(bounds, intent, taskBounds);
+ // TODO(b/265271880): remove redundant logic after all TF operations take fragmentToken.
+ final Rect taskBounds = taskContainer.getBounds();
+ final Rect sanitizedBounds = sanitizeBounds(activityStackAttributes.getRelativeBounds(),
+ getMinDimensions(intent), taskBounds);
final int windowingMode = taskContainer
.getWindowingModeForTaskFragment(sanitizedBounds);
mPresenter.createTaskFragment(wct, taskFragmentToken, activityInTask.getActivityToken(),
sanitizedBounds, windowingMode);
- mPresenter.updateAnimationParams(wct, taskFragmentToken,
- TaskFragmentAnimationParams.DEFAULT);
- mPresenter.setTaskFragmentIsolatedNavigation(wct, taskFragmentToken,
- overlayTag != null && !sanitizedBounds.isEmpty());
+ mPresenter.applyActivityStackAttributes(wct, container, activityStackAttributes,
+ getMinDimensions(intent));
return container;
}
/**
- * Returns the expanded bounds if the {@code bounds} violate minimum dimension or are not fully
- * covered by the task bounds. Otherwise, returns {@code bounds}.
- */
- @NonNull
- private static Rect sanitizeBounds(@NonNull Rect bounds, @NonNull Intent intent,
- @NonNull Rect taskBounds) {
- if (bounds.isEmpty()) {
- // Don't need to check if the bounds follows the task bounds.
- return bounds;
- }
- if (boundsSmallerThanMinDimensions(bounds, getMinDimensions(intent))) {
- // Expand the bounds if the bounds are smaller than minimum dimensions.
- return new Rect();
- }
- if (!taskBounds.contains(bounds)) {
- // Expand the bounds if the bounds exceed the task bounds.
- return new Rect();
- }
- return bounds;
- }
-
- /**
* Returns a container for the new activity intent to launch into as splitting with the primary
* activity.
*/
@@ -1953,6 +1939,12 @@
return;
}
+ if (mActivityStackAttributesCalculator == null) {
+ Log.e(TAG, "ActivityStackAttributesCalculator is not set. Thus the overlay container"
+ + " can not be updated.");
+ return;
+ }
+
if (mActivityStackAttributesCalculator != null) {
final ActivityStackAttributesCalculatorParams params =
new ActivityStackAttributesCalculatorParams(
@@ -1962,7 +1954,8 @@
container.getLaunchOptions());
final ActivityStackAttributes attributes = mActivityStackAttributesCalculator
.apply(params);
- mPresenter.applyActivityStackAttributes(wct, container, attributes);
+ mPresenter.applyActivityStackAttributes(wct, container, attributes,
+ container.getMinDimensions());
}
}
@@ -2598,15 +2591,15 @@
mPresenter.createParentContainerInfoFromTaskProperties(
mPresenter.getTaskProperties(launchActivity)), overlayTag, options);
// Fallback to expand the bounds if there's no activityStackAttributes calculator.
- final Rect relativeBounds = mActivityStackAttributesCalculator != null
- ? new Rect(mActivityStackAttributesCalculator.apply(params).getRelativeBounds())
- : new Rect();
- final boolean shouldExpandContainer = boundsSmallerThanMinDimensions(relativeBounds,
- getMinDimensions(intent));
- // Expand the bounds if the requested bounds are smaller than minimum dimensions.
- if (shouldExpandContainer) {
- relativeBounds.setEmpty();
+ final ActivityStackAttributes attrs;
+ if (mActivityStackAttributesCalculator != null) {
+ attrs = mActivityStackAttributesCalculator.apply(params);
+ } else {
+ attrs = new ActivityStackAttributes.Builder().build();
+ Log.e(TAG, "ActivityStackAttributesCalculator isn't set. Fallback to set overlay "
+ + "container as expected.");
}
+
final int taskId = getTaskId(launchActivity);
if (!overlayContainers.isEmpty()) {
for (final TaskFragmentContainer overlayContainer : overlayContainers) {
@@ -2626,20 +2619,8 @@
}
if (overlayTag.equals(overlayContainer.getOverlayTag())
&& taskId == overlayContainer.getTaskId()) {
- // If there's an overlay container with the same tag and task ID, we treat
- // the OverlayCreateParams as the update to the container.
- final IBinder overlayToken = overlayContainer.getTaskFragmentToken();
- final TaskContainer taskContainer = overlayContainer.getTaskContainer();
- final Rect taskBounds = taskContainer.getTaskProperties().getTaskMetrics()
- .getBounds();
- final Rect sanitizedBounds = sanitizeBounds(relativeBounds, intent, taskBounds);
-
- mPresenter.resizeTaskFragment(wct, overlayToken, sanitizedBounds);
- final int windowingMode = taskContainer
- .getWindowingModeForTaskFragment(sanitizedBounds);
- mPresenter.updateWindowingMode(wct, overlayToken, windowingMode);
- mPresenter.setTaskFragmentIsolatedNavigation(wct, overlayContainer,
- !sanitizedBounds.isEmpty());
+ mPresenter.applyActivityStackAttributes(wct, overlayContainer, attrs,
+ getMinDimensions(intent));
// We can just return the updated overlay container and don't need to
// check other condition since we only have one OverlayCreateParams, and
// if the tag and task are matched, it's impossible to match another task
@@ -2649,7 +2630,7 @@
}
}
// Launch the overlay container to the task with taskId.
- return createEmptyContainer(wct, intent, taskId, relativeBounds, launchActivity, overlayTag,
+ return createEmptyContainer(wct, intent, taskId, attrs, launchActivity, overlayTag,
options);
}
@@ -2779,8 +2760,17 @@
// TODO(b/232042367): Consolidate the activity create handling so that we can handle
// cross-process the same as normal.
+ IBinder activityStackToken = options.getBinder(KEY_ACTIVITY_STACK_TOKEN);
+ if (activityStackToken != null) {
+ // Put activityStack token to #KEY_LAUNCH_TASK_FRAGMENT_TOKEN to launch the activity
+ // into the taskFragment associated with the token.
+ options.putBinder(KEY_LAUNCH_TASK_FRAGMENT_TOKEN, activityStackToken);
+ }
+
// Early return if the launching taskfragment is already been set.
- if (options.getBinder(ActivityOptions.KEY_LAUNCH_TASK_FRAGMENT_TOKEN) != null) {
+ // TODO(b/295993745): Use KEY_LAUNCH_TASK_FRAGMENT_TOKEN after WM Jetpack migrates to
+ // bundle. This is still needed to support #setLaunchingActivityStack.
+ if (options.getBinder(KEY_LAUNCH_TASK_FRAGMENT_TOKEN) != null) {
synchronized (mLock) {
mCurrentIntent = intent;
}
@@ -2837,7 +2827,7 @@
// Amend the request to let the WM know that the activity should be placed in
// the dedicated container.
// TODO(b/229680885): skip override launching TaskFragment token by split-rule
- options.putBinder(ActivityOptions.KEY_LAUNCH_TASK_FRAGMENT_TOKEN,
+ options.putBinder(KEY_LAUNCH_TASK_FRAGMENT_TOKEN,
launchedInTaskFragment.getTaskFragmentToken());
mCurrentIntent = intent;
} else {
@@ -2855,8 +2845,7 @@
if (mCurrentIntent != null && result != START_SUCCESS) {
// Clear the pending appeared intent if the activity was not started
// successfully.
- final IBinder token = bOptions.getBinder(
- ActivityOptions.KEY_LAUNCH_TASK_FRAGMENT_TOKEN);
+ final IBinder token = bOptions.getBinder(KEY_LAUNCH_TASK_FRAGMENT_TOKEN);
if (token != null) {
final TaskFragmentContainer container = getContainer(token);
if (container != null) {
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java
index 8b7fd10..2f2da8c 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java
@@ -16,8 +16,6 @@
package androidx.window.extensions.embedding;
-import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
-import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
import static android.content.pm.PackageManager.MATCH_ALL;
import static androidx.window.extensions.embedding.WindowAttributes.DIM_AREA_ON_TASK;
@@ -426,7 +424,8 @@
* creation has not been reported from the server yet.
*/
// TODO(b/190433398): Handle resize if the fragment hasn't appeared yet.
- private void resizeTaskFragmentIfRegistered(@NonNull WindowContainerTransaction wct,
+ @VisibleForTesting
+ void resizeTaskFragmentIfRegistered(@NonNull WindowContainerTransaction wct,
@NonNull TaskFragmentContainer container,
@Nullable Rect relBounds) {
if (container.getInfo() == null) {
@@ -435,7 +434,8 @@
resizeTaskFragment(wct, container.getTaskFragmentToken(), relBounds);
}
- private void updateTaskFragmentWindowingModeIfRegistered(
+ @VisibleForTesting
+ void updateTaskFragmentWindowingModeIfRegistered(
@NonNull WindowContainerTransaction wct,
@NonNull TaskFragmentContainer container,
@WindowingMode int windowingMode) {
@@ -579,13 +579,53 @@
super.setCompanionTaskFragment(wct, primary, secondary);
}
- void applyActivityStackAttributes(@NonNull WindowContainerTransaction wct,
- @NonNull TaskFragmentContainer container, @NonNull ActivityStackAttributes attributes) {
- final Rect bounds = attributes.getRelativeBounds();
+ void applyActivityStackAttributes(
+ @NonNull WindowContainerTransaction wct,
+ @NonNull TaskFragmentContainer container,
+ @NonNull ActivityStackAttributes attributes,
+ @Nullable Size minDimensions) {
+ final Rect taskBounds = container.getTaskContainer().getBounds();
+ final Rect relativeBounds = sanitizeBounds(attributes.getRelativeBounds(), minDimensions,
+ taskBounds);
+ final boolean isFillParent = relativeBounds.isEmpty();
+ final boolean isIsolatedNavigated = !isFillParent && container.isOverlay();
+ final boolean dimOnTask = !isFillParent
+ && attributes.getWindowAttributes().getDimArea() == DIM_AREA_ON_TASK
+ && Flags.fullscreenDimFlag();
+ final IBinder fragmentToken = container.getTaskFragmentToken();
- resizeTaskFragment(wct, container.getTaskFragmentToken(), bounds);
- updateWindowingMode(wct, container.getTaskFragmentToken(),
- bounds.isEmpty() ? WINDOWING_MODE_FULLSCREEN : WINDOWING_MODE_MULTI_WINDOW);
+ // TODO(b/243518738): Update to resizeTaskFragment after we migrate WCT#setRelativeBounds
+ // and WCT#setWindowingMode to take fragmentToken.
+ resizeTaskFragmentIfRegistered(wct, container, relativeBounds);
+ int windowingMode = container.getTaskContainer().getWindowingModeForTaskFragment(
+ relativeBounds);
+ updateTaskFragmentWindowingModeIfRegistered(wct, container, windowingMode);
+ // Always use default animation for standalone ActivityStack.
+ updateAnimationParams(wct, fragmentToken, TaskFragmentAnimationParams.DEFAULT);
+ setTaskFragmentIsolatedNavigation(wct, container, isIsolatedNavigated);
+ setTaskFragmentDimOnTask(wct, fragmentToken, dimOnTask);
+ }
+
+ /**
+ * Returns the expanded bounds if the {@code bounds} violate minimum dimension or are not fully
+ * covered by the task bounds. Otherwise, returns {@code bounds}.
+ */
+ @NonNull
+ static Rect sanitizeBounds(@NonNull Rect bounds, @Nullable Size minDimension,
+ @NonNull Rect taskBounds) {
+ if (bounds.isEmpty()) {
+ // Don't need to check if the bounds follows the task bounds.
+ return bounds;
+ }
+ if (boundsSmallerThanMinDimensions(bounds, minDimension)) {
+ // Expand the bounds if the bounds are smaller than minimum dimensions.
+ return new Rect();
+ }
+ if (!taskBounds.contains(bounds)) {
+ // Expand the bounds if the bounds exceed the task bounds.
+ return new Rect();
+ }
+ return bounds;
}
@Override
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 71195b6..73109e2 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java
@@ -126,6 +126,11 @@
}
@NonNull
+ Rect getBounds() {
+ return mConfiguration.windowConfiguration.getBounds();
+ }
+
+ @NonNull
TaskProperties getTaskProperties() {
return new TaskProperties(mDisplayId, mConfiguration);
}
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 4e7b760..34d43ad 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
@@ -16,14 +16,17 @@
package androidx.window.extensions.embedding;
+import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
import static android.view.Display.DEFAULT_DISPLAY;
import static androidx.window.extensions.embedding.ActivityEmbeddingOptionsProperties.KEY_OVERLAY_TAG;
import static androidx.window.extensions.embedding.EmbeddingTestUtils.TASK_BOUNDS;
import static androidx.window.extensions.embedding.EmbeddingTestUtils.TASK_ID;
+import static androidx.window.extensions.embedding.EmbeddingTestUtils.TEST_TAG;
import static androidx.window.extensions.embedding.EmbeddingTestUtils.createMockTaskFragmentInfo;
import static androidx.window.extensions.embedding.EmbeddingTestUtils.createSplitPairRuleBuilder;
+import static androidx.window.extensions.embedding.WindowAttributes.DIM_AREA_ON_TASK;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
@@ -36,7 +39,6 @@
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
@@ -56,6 +58,8 @@
import android.os.IBinder;
import android.platform.test.annotations.Presubmit;
import android.platform.test.flag.junit.SetFlagsRule;
+import android.util.Size;
+import android.window.TaskFragmentAnimationParams;
import android.window.TaskFragmentInfo;
import android.window.TaskFragmentParentInfo;
import android.window.WindowContainerTransaction;
@@ -266,62 +270,21 @@
}
@Test
- public void testCreateOrUpdateOverlayTaskFragmentIfNeeded_smallerThanMinDimens_expandOverlay() {
+ public void testSanitizeBounds_smallerThanMinDimens_expandOverlay() {
mIntent.setComponent(new ComponentName(ApplicationProvider.getApplicationContext(),
MinimumDimensionActivity.class));
-
final Rect bounds = new Rect(0, 0, 100, 100);
- mSplitController.setActivityStackAttributesCalculator(params ->
- new ActivityStackAttributes.Builder().setRelativeBounds(bounds).build());
- final TaskFragmentContainer overlayContainer =
- createOrUpdateOverlayTaskFragmentIfNeeded("test");
- final IBinder overlayToken = overlayContainer.getTaskFragmentToken();
- assertThat(mSplitController.getAllOverlayTaskFragmentContainers())
- .containsExactly(overlayContainer);
- assertThat(overlayContainer.areLastRequestedBoundsEqual(new Rect())).isTrue();
- verify(mSplitPresenter).setTaskFragmentIsolatedNavigation(mTransaction, overlayToken,
- false);
-
- // Call createOrUpdateOverlayTaskFragmentIfNeeded again to check the update case.
- clearInvocations(mSplitPresenter);
- createOrUpdateOverlayTaskFragmentIfNeeded("test");
-
- verify(mSplitPresenter).resizeTaskFragment(mTransaction, overlayToken, new Rect());
- verify(mSplitPresenter).updateWindowingMode(mTransaction, overlayToken,
- WINDOWING_MODE_UNDEFINED);
- verify(mSplitPresenter).setTaskFragmentIsolatedNavigation(mTransaction, overlayContainer,
- false);
+ SplitPresenter.sanitizeBounds(bounds, SplitPresenter.getMinDimensions(mIntent),
+ TASK_BOUNDS);
}
@Test
- public void testCreateOrUpdateOverlayTaskFragmentIfNeeded_notInTaskBounds_expandOverlay() {
+ public void testSanitizeBounds_notInTaskBounds_expandOverlay() {
final Rect bounds = new Rect(TASK_BOUNDS);
bounds.offset(10, 10);
- mSplitController.setActivityStackAttributesCalculator(params ->
- new ActivityStackAttributes.Builder().setRelativeBounds(bounds).build());
- final TaskFragmentContainer overlayContainer =
- createOrUpdateOverlayTaskFragmentIfNeeded("test");
- final IBinder overlayToken = overlayContainer.getTaskFragmentToken();
-
- assertThat(mSplitController.getAllOverlayTaskFragmentContainers())
- .containsExactly(overlayContainer);
- assertThat(overlayContainer.areLastRequestedBoundsEqual(new Rect())).isTrue();
- verify(mSplitPresenter).setTaskFragmentIsolatedNavigation(mTransaction, overlayToken,
- false);
-
- // Call createOrUpdateOverlayTaskFragmentIfNeeded again to check the update case.
- clearInvocations(mSplitPresenter);
- createOrUpdateOverlayTaskFragmentIfNeeded("test");
-
- verify(mSplitPresenter).resizeTaskFragment(mTransaction, overlayToken, new Rect());
- verify(mSplitPresenter).updateWindowingMode(mTransaction,
- overlayToken, WINDOWING_MODE_UNDEFINED);
- verify(mSplitPresenter).setTaskFragmentIsolatedNavigation(mTransaction,
- overlayContainer, false);
- assertThat(mSplitController.getAllOverlayTaskFragmentContainers())
- .containsExactly(overlayContainer);
+ SplitPresenter.sanitizeBounds(bounds, null, TASK_BOUNDS);
}
@Test
@@ -331,6 +294,7 @@
new ActivityStackAttributes.Builder().setRelativeBounds(bounds).build());
final TaskFragmentContainer overlayContainer =
createOrUpdateOverlayTaskFragmentIfNeeded("test");
+ setupTaskFragmentInfo(overlayContainer, mActivity);
assertThat(mSplitController.getAllOverlayTaskFragmentContainers())
.containsExactly(overlayContainer);
@@ -437,7 +401,7 @@
assertThrows(NullPointerException.class, () ->
mSplitController.updateActivityStackAttributes(new Binder(), null));
- verify(mSplitPresenter, never()).applyActivityStackAttributes(any(), any(), any());
+ verify(mSplitPresenter, never()).applyActivityStackAttributes(any(), any(), any(), any());
}
@Test
@@ -447,7 +411,7 @@
mSplitController.updateActivityStackAttributes(container.getTaskFragmentToken(),
new ActivityStackAttributes.Builder().build());
- verify(mSplitPresenter, never()).applyActivityStackAttributes(any(), any(), any());
+ verify(mSplitPresenter, never()).applyActivityStackAttributes(any(), any(), any(), any());
}
@Test
@@ -457,19 +421,20 @@
mSplitController.updateActivityStackAttributes(container.getTaskFragmentToken(),
new ActivityStackAttributes.Builder().build());
- verify(mSplitPresenter, never()).applyActivityStackAttributes(any(), any(), any());
+ verify(mSplitPresenter, never()).applyActivityStackAttributes(any(), any(), any(), any());
}
@Test
public void testUpdateActivityStackAttributes() {
final TaskFragmentContainer container = createTestOverlayContainer(TASK_ID, "test");
- doNothing().when(mSplitPresenter).applyActivityStackAttributes(any(), any(), any());
+ doNothing().when(mSplitPresenter).applyActivityStackAttributes(any(), any(), any(), any());
final ActivityStackAttributes attrs = new ActivityStackAttributes.Builder().build();
final IBinder token = container.getTaskFragmentToken();
mSplitController.updateActivityStackAttributes(token, attrs);
- verify(mSplitPresenter).applyActivityStackAttributes(any(), eq(container), eq(attrs));
+ verify(mSplitPresenter).applyActivityStackAttributes(any(), eq(container), eq(attrs),
+ any());
}
@Test
@@ -521,6 +486,89 @@
.that(taskContainer.getOverlayContainer()).isNull();
}
+ @Test
+ public void testApplyActivityStackAttributesForExpandedContainer() {
+ final TaskFragmentContainer container = createMockTaskFragmentContainer(mActivity);
+ final IBinder token = container.getTaskFragmentToken();
+ final ActivityStackAttributes attributes = new ActivityStackAttributes.Builder().build();
+
+ mSplitPresenter.applyActivityStackAttributes(mTransaction, container, attributes,
+ null /* minDimensions */);
+
+ verify(mSplitPresenter).resizeTaskFragmentIfRegistered(mTransaction, container,
+ attributes.getRelativeBounds());
+ verify(mSplitPresenter).updateTaskFragmentWindowingModeIfRegistered(mTransaction, container,
+ WINDOWING_MODE_UNDEFINED);
+ verify(mSplitPresenter).updateAnimationParams(mTransaction, token,
+ TaskFragmentAnimationParams.DEFAULT);
+ verify(mSplitPresenter).setTaskFragmentIsolatedNavigation(mTransaction, container, false);
+ verify(mSplitPresenter).setTaskFragmentDimOnTask(mTransaction, token, false);
+ }
+
+ @Test
+ public void testApplyActivityStackAttributesForOverlayContainer() {
+ final TaskFragmentContainer container = createTestOverlayContainer(TASK_ID, TEST_TAG);
+ final IBinder token = container.getTaskFragmentToken();
+ final ActivityStackAttributes attributes = new ActivityStackAttributes.Builder()
+ .setRelativeBounds(new Rect(0, 0, 200, 200))
+ .setWindowAttributes(new WindowAttributes(DIM_AREA_ON_TASK))
+ .build();
+
+ mSplitPresenter.applyActivityStackAttributes(mTransaction, container, attributes,
+ null /* minDimensions */);
+
+ verify(mSplitPresenter).resizeTaskFragmentIfRegistered(mTransaction, container,
+ attributes.getRelativeBounds());
+ verify(mSplitPresenter).updateTaskFragmentWindowingModeIfRegistered(mTransaction, container,
+ WINDOWING_MODE_MULTI_WINDOW);
+ verify(mSplitPresenter).updateAnimationParams(mTransaction, token,
+ TaskFragmentAnimationParams.DEFAULT);
+ verify(mSplitPresenter).setTaskFragmentIsolatedNavigation(mTransaction, container, true);
+ verify(mSplitPresenter).setTaskFragmentDimOnTask(mTransaction, token, true);
+ }
+
+ @Test
+ public void testApplyActivityStackAttributesForExpandedOverlayContainer() {
+ final TaskFragmentContainer container = createTestOverlayContainer(TASK_ID, TEST_TAG);
+ final IBinder token = container.getTaskFragmentToken();
+ final ActivityStackAttributes attributes = new ActivityStackAttributes.Builder().build();
+
+ mSplitPresenter.applyActivityStackAttributes(mTransaction, container, attributes,
+ null /* minDimensions */);
+
+ verify(mSplitPresenter).resizeTaskFragmentIfRegistered(mTransaction, container,
+ attributes.getRelativeBounds());
+ verify(mSplitPresenter).updateTaskFragmentWindowingModeIfRegistered(mTransaction, container,
+ WINDOWING_MODE_UNDEFINED);
+ verify(mSplitPresenter).updateAnimationParams(mTransaction, token,
+ TaskFragmentAnimationParams.DEFAULT);
+ verify(mSplitPresenter).setTaskFragmentIsolatedNavigation(mTransaction, container, false);
+ verify(mSplitPresenter).setTaskFragmentDimOnTask(mTransaction, token, false);
+ }
+
+ @Test
+ public void testApplyActivityStackAttributesForOverlayContainer_exceedsMinDimensions() {
+ final TaskFragmentContainer container = createTestOverlayContainer(TASK_ID, TEST_TAG);
+ final IBinder token = container.getTaskFragmentToken();
+ final Rect relativeBounds = new Rect(0, 0, 200, 200);
+ final ActivityStackAttributes attributes = new ActivityStackAttributes.Builder()
+ .setRelativeBounds(relativeBounds)
+ .setWindowAttributes(new WindowAttributes(DIM_AREA_ON_TASK))
+ .build();
+
+ mSplitPresenter.applyActivityStackAttributes(mTransaction, container, attributes,
+ new Size(relativeBounds.width() + 1, relativeBounds.height()));
+
+ verify(mSplitPresenter).resizeTaskFragmentIfRegistered(mTransaction, container,
+ new Rect());
+ verify(mSplitPresenter).updateTaskFragmentWindowingModeIfRegistered(mTransaction, container,
+ WINDOWING_MODE_UNDEFINED);
+ verify(mSplitPresenter).updateAnimationParams(mTransaction, token,
+ TaskFragmentAnimationParams.DEFAULT);
+ verify(mSplitPresenter).setTaskFragmentIsolatedNavigation(mTransaction, container, false);
+ verify(mSplitPresenter).setTaskFragmentDimOnTask(mTransaction, token, false);
+ }
+
/**
* A simplified version of {@link SplitController.ActivityStartMonitor
* #createOrUpdateOverlayTaskFragmentIfNeeded}
diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleTaskViewTest.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleTaskViewTest.kt
new file mode 100644
index 0000000..4c76168
--- /dev/null
+++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleTaskViewTest.kt
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.bubbles
+
+import android.content.ComponentName
+import android.content.Context
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.wm.shell.taskview.TaskView
+import com.android.wm.shell.taskview.TaskViewTaskController
+
+import com.google.common.truth.Truth.assertThat
+import com.google.common.util.concurrent.MoreExecutors.directExecutor
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.mock
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class BubbleTaskViewTest {
+
+ private lateinit var bubbleTaskView: BubbleTaskView
+ private val context = ApplicationProvider.getApplicationContext<Context>()
+
+ @Before
+ fun setUp() {
+ val taskView = TaskView(context, mock<TaskViewTaskController>())
+ bubbleTaskView = BubbleTaskView(taskView, directExecutor())
+ }
+
+ @Test
+ fun onTaskCreated_updatesState() {
+ val componentName = ComponentName(context, "TestClass")
+ bubbleTaskView.listener.onTaskCreated(123, componentName)
+
+ assertThat(bubbleTaskView.taskId).isEqualTo(123)
+ assertThat(bubbleTaskView.componentName).isEqualTo(componentName)
+ assertThat(bubbleTaskView.isCreated).isTrue()
+ }
+
+ @Test
+ fun onTaskCreated_callsDelegateListener() {
+ var actualTaskId = -1
+ var actualComponentName: ComponentName? = null
+ val delegateListener = object : TaskView.Listener {
+ override fun onTaskCreated(taskId: Int, name: ComponentName) {
+ actualTaskId = taskId
+ actualComponentName = name
+ }
+ }
+ bubbleTaskView.delegateListener = delegateListener
+
+ val componentName = ComponentName(context, "TestClass")
+ bubbleTaskView.listener.onTaskCreated(123, componentName)
+
+ assertThat(actualTaskId).isEqualTo(123)
+ assertThat(actualComponentName).isEqualTo(componentName)
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java
index f3fe895..9f7d0ac 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java
@@ -74,7 +74,6 @@
import com.android.wm.shell.common.AlphaOptimizedButton;
import com.android.wm.shell.common.TriangleShape;
import com.android.wm.shell.taskview.TaskView;
-import com.android.wm.shell.taskview.TaskViewTaskController;
import java.io.PrintWriter;
@@ -146,7 +145,6 @@
private AlphaOptimizedButton mManageButton;
private TaskView mTaskView;
- private TaskViewTaskController mTaskViewTaskController;
private BubbleOverflowContainerView mOverflowView;
private int mTaskId = INVALID_TASK_ID;
@@ -434,7 +432,8 @@
* Initialize {@link BubbleController} and {@link BubbleStackView} here, this method must need
* to be called after view inflate.
*/
- void initialize(BubbleController controller, BubbleStackView stackView, boolean isOverflow) {
+ void initialize(BubbleController controller, BubbleStackView stackView, boolean isOverflow,
+ @Nullable BubbleTaskView bubbleTaskView) {
mController = controller;
mStackView = stackView;
mIsOverflow = isOverflow;
@@ -451,18 +450,22 @@
bringChildToFront(mOverflowView);
mManageButton.setVisibility(GONE);
} else {
- mTaskViewTaskController = new TaskViewTaskController(mContext,
- mController.getTaskOrganizer(),
- mController.getTaskViewTransitions(), mController.getSyncTransactionQueue());
- mTaskView = new TaskView(mContext, mTaskViewTaskController);
- mTaskView.setListener(mController.getMainExecutor(), mTaskViewListener);
+ mTaskView = bubbleTaskView.getTaskView();
+ bubbleTaskView.setDelegateListener(mTaskViewListener);
// set a fixed width so it is not recalculated as part of a rotation. the width will be
// updated manually after the rotation.
FrameLayout.LayoutParams lp =
new FrameLayout.LayoutParams(getContentWidth(), MATCH_PARENT);
+ if (mTaskView.getParent() != null) {
+ ((ViewGroup) mTaskView.getParent()).removeView(mTaskView);
+ }
mExpandedViewContainer.addView(mTaskView, lp);
bringChildToFront(mTaskView);
+ if (bubbleTaskView.isCreated()) {
+ mTaskViewListener.onTaskCreated(
+ bubbleTaskView.getTaskId(), bubbleTaskView.getComponentName());
+ }
}
}
@@ -876,7 +879,7 @@
return;
}
boolean isNew = mBubble == null || didBackingContentChange(bubble);
- if (isNew || bubble != null && bubble.getKey().equals(mBubble.getKey())) {
+ if (isNew || bubble.getKey().equals(mBubble.getKey())) {
mBubble = bubble;
mManageButton.setContentDescription(getResources().getString(
R.string.bubbles_settings_button_description, bubble.getAppName()));
@@ -1107,7 +1110,8 @@
* has been removed.
*
* If this view should be reused after this method is called, then
- * {@link #initialize(BubbleController, BubbleStackView, boolean)} must be invoked first.
+ * {@link #initialize(BubbleController, BubbleStackView, boolean, BubbleTaskView)}
+ * must be invoked first.
*/
public void cleanUpExpandedState() {
if (DEBUG_BUBBLE_EXPANDED_VIEW) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleOverflow.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleOverflow.kt
index 22e836a..e5d9ace 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleOverflow.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleOverflow.kt
@@ -29,6 +29,7 @@
import android.view.LayoutInflater
import android.view.View.VISIBLE
import android.widget.FrameLayout
+import androidx.core.content.ContextCompat
import com.android.launcher3.icons.BubbleIconFactory
import com.android.wm.shell.R
import com.android.wm.shell.bubbles.bar.BubbleBarExpandedView
@@ -57,10 +58,16 @@
/** Call before use and again if cleanUpExpandedState was called. */
fun initialize(controller: BubbleController, forBubbleBar: Boolean) {
if (forBubbleBar) {
- createBubbleBarExpandedView().initialize(controller, true /* isOverflow */)
+ createBubbleBarExpandedView()
+ .initialize(controller, /* isOverflow= */ true, /* bubbleTaskView= */ null)
} else {
createExpandedView()
- .initialize(controller, controller.stackView, true /* isOverflow */)
+ .initialize(
+ controller,
+ controller.stackView,
+ /* isOverflow= */ true,
+ /* bubbleTaskView= */ null
+ )
}
}
@@ -113,7 +120,10 @@
context,
res.getDimensionPixelSize(R.dimen.bubble_size),
res.getDimensionPixelSize(R.dimen.bubble_badge_size),
- res.getColor(com.android.launcher3.icons.R.color.important_conversation),
+ ContextCompat.getColor(
+ context,
+ com.android.launcher3.icons.R.color.important_conversation
+ ),
res.getDimensionPixelSize(com.android.internal.R.dimen.importance_ring_stroke_width)
)
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskView.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskView.kt
new file mode 100644
index 0000000..2fcd133
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskView.kt
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.bubbles
+
+import android.app.ActivityTaskManager.INVALID_TASK_ID
+import android.content.ComponentName
+import androidx.annotation.VisibleForTesting
+import com.android.wm.shell.taskview.TaskView
+import java.util.concurrent.Executor
+
+/**
+ * A wrapper class around [TaskView] for bubble expanded views.
+ *
+ * [delegateListener] allows callers to change listeners after a task has been created.
+ */
+class BubbleTaskView(val taskView: TaskView, executor: Executor) {
+
+ /** Whether the task is already created. */
+ var isCreated = false
+ private set
+
+ /** The task id. */
+ var taskId = INVALID_TASK_ID
+ private set
+
+ /** The component name of the application running in the task. */
+ var componentName: ComponentName? = null
+ private set
+
+ /** [TaskView.Listener] for users of this class. */
+ var delegateListener: TaskView.Listener? = null
+
+ /** A [TaskView.Listener] that delegates to [delegateListener]. */
+ @get:VisibleForTesting
+ val listener = object : TaskView.Listener {
+ override fun onInitialized() {
+ delegateListener?.onInitialized()
+ }
+
+ override fun onReleased() {
+ delegateListener?.onReleased()
+ }
+
+ override fun onTaskCreated(taskId: Int, name: ComponentName) {
+ delegateListener?.onTaskCreated(taskId, name)
+ this@BubbleTaskView.taskId = taskId
+ isCreated = true
+ componentName = name
+ }
+
+ override fun onTaskVisibilityChanged(taskId: Int, visible: Boolean) {
+ delegateListener?.onTaskVisibilityChanged(taskId, visible)
+ }
+
+ override fun onTaskRemovalStarted(taskId: Int) {
+ delegateListener?.onTaskRemovalStarted(taskId)
+ }
+
+ override fun onBackPressedOnTaskRoot(taskId: Int) {
+ delegateListener?.onBackPressedOnTaskRoot(taskId)
+ }
+ }
+
+ init {
+ taskView.setListener(executor, listener)
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java
index f6c382f..5855a81 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java
@@ -35,10 +35,7 @@
import androidx.annotation.Nullable;
-import com.android.wm.shell.common.ShellExecutor;
-import com.android.wm.shell.common.annotations.ShellMainThread;
import com.android.wm.shell.taskview.TaskView;
-import com.android.wm.shell.taskview.TaskViewTaskController;
/**
* Handles creating and updating the {@link TaskView} associated with a {@link Bubble}.
@@ -65,7 +62,6 @@
private final Context mContext;
private final BubbleController mController;
- private final @ShellMainThread ShellExecutor mMainExecutor;
private final BubbleTaskViewHelper.Listener mListener;
private final View mParentView;
@@ -73,7 +69,6 @@
private Bubble mBubble;
@Nullable
private PendingIntent mPendingIntent;
- private TaskViewTaskController mTaskViewTaskController;
@Nullable
private TaskView mTaskView;
private int mTaskId = INVALID_TASK_ID;
@@ -204,17 +199,18 @@
public BubbleTaskViewHelper(Context context,
BubbleController controller,
BubbleTaskViewHelper.Listener listener,
+ BubbleTaskView bubbleTaskView,
View parent) {
mContext = context;
mController = controller;
- mMainExecutor = mController.getMainExecutor();
mListener = listener;
mParentView = parent;
- mTaskViewTaskController = new TaskViewTaskController(mContext,
- mController.getTaskOrganizer(),
- mController.getTaskViewTransitions(), mController.getSyncTransactionQueue());
- mTaskView = new TaskView(mContext, mTaskViewTaskController);
- mTaskView.setListener(mMainExecutor, mTaskViewListener);
+ mTaskView = bubbleTaskView.getTaskView();
+ bubbleTaskView.setDelegateListener(mTaskViewListener);
+ if (bubbleTaskView.isCreated()) {
+ mTaskId = bubbleTaskView.getTaskId();
+ mListener.onTaskCreated();
+ }
}
/**
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleViewInfoTask.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleViewInfoTask.java
index bb30c5e..c3d899e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleViewInfoTask.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleViewInfoTask.java
@@ -46,6 +46,8 @@
import com.android.wm.shell.R;
import com.android.wm.shell.bubbles.bar.BubbleBarExpandedView;
import com.android.wm.shell.bubbles.bar.BubbleBarLayerView;
+import com.android.wm.shell.taskview.TaskView;
+import com.android.wm.shell.taskview.TaskViewTaskController;
import java.lang.ref.WeakReference;
import java.util.Objects;
@@ -173,10 +175,12 @@
BubbleViewInfo info = new BubbleViewInfo();
if (!skipInflation && !b.isInflated()) {
+ BubbleTaskView bubbleTaskView = createBubbleTaskView(c, controller);
LayoutInflater inflater = LayoutInflater.from(c);
info.bubbleBarExpandedView = (BubbleBarExpandedView) inflater.inflate(
R.layout.bubble_bar_expanded_view, layerView, false /* attachToRoot */);
- info.bubbleBarExpandedView.initialize(controller, false /* isOverflow */);
+ info.bubbleBarExpandedView.initialize(
+ controller, false /* isOverflow */, bubbleTaskView);
}
if (!populateCommonInfo(info, c, b, iconFactory)) {
@@ -201,9 +205,11 @@
R.layout.bubble_view, stackView, false /* attachToRoot */);
info.imageView.initialize(controller.getPositioner());
+ BubbleTaskView bubbleTaskView = createBubbleTaskView(c, controller);
info.expandedView = (BubbleExpandedView) inflater.inflate(
R.layout.bubble_expanded_view, stackView, false /* attachToRoot */);
- info.expandedView.initialize(controller, stackView, false /* isOverflow */);
+ info.expandedView.initialize(
+ controller, stackView, false /* isOverflow */, bubbleTaskView);
}
if (!populateCommonInfo(info, c, b, iconFactory)) {
@@ -219,6 +225,15 @@
}
return info;
}
+
+ private static BubbleTaskView createBubbleTaskView(
+ Context context, BubbleController controller) {
+ TaskViewTaskController taskViewTaskController = new TaskViewTaskController(context,
+ controller.getTaskOrganizer(),
+ controller.getTaskViewTransitions(), controller.getSyncTransactionQueue());
+ TaskView taskView = new TaskView(context, taskViewTaskController);
+ return new BubbleTaskView(taskView, controller.getMainExecutor());
+ }
}
/**
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java
index 66c0c96..3cf23ac 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java
@@ -16,6 +16,8 @@
package com.android.wm.shell.bubbles.bar;
+import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
+
import android.annotation.Nullable;
import android.app.ActivityManager;
import android.content.Context;
@@ -27,6 +29,7 @@
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
+import android.view.ViewGroup;
import android.view.ViewOutlineProvider;
import android.widget.FrameLayout;
@@ -35,6 +38,7 @@
import com.android.wm.shell.bubbles.Bubble;
import com.android.wm.shell.bubbles.BubbleController;
import com.android.wm.shell.bubbles.BubbleOverflowContainerView;
+import com.android.wm.shell.bubbles.BubbleTaskView;
import com.android.wm.shell.bubbles.BubbleTaskViewHelper;
import com.android.wm.shell.bubbles.Bubbles;
import com.android.wm.shell.taskview.TaskView;
@@ -130,7 +134,8 @@
}
/** Set the BubbleController on the view, must be called before doing anything else. */
- public void initialize(BubbleController controller, boolean isOverflow) {
+ public void initialize(BubbleController controller, boolean isOverflow,
+ @Nullable BubbleTaskView bubbleTaskView) {
mController = controller;
mIsOverflow = isOverflow;
@@ -140,14 +145,19 @@
mOverflowView.setBubbleController(mController);
addView(mOverflowView);
} else {
-
+ mTaskView = bubbleTaskView.getTaskView();
mBubbleTaskViewHelper = new BubbleTaskViewHelper(mContext, mController,
- /* listener= */ this,
+ /* listener= */ this, bubbleTaskView,
/* viewParent= */ this);
- mTaskView = mBubbleTaskViewHelper.getTaskView();
- addView(mTaskView);
+ if (mTaskView.getParent() != null) {
+ ((ViewGroup) mTaskView.getParent()).removeView(mTaskView);
+ }
+ FrameLayout.LayoutParams lp =
+ new FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT);
+ addView(mTaskView, lp);
mTaskView.setEnableSurfaceClipping(true);
mTaskView.setCornerRadius(mCornerRadius);
+ mTaskView.setVisibility(VISIBLE);
// Handle view needs to draw on top of task view.
bringChildToFront(mHandleView);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManager.java
index 2dd2743..dbf7186 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManager.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManager.java
@@ -80,8 +80,7 @@
Consumer<Pair<TaskInfo, ShellTaskOrganizer.TaskListener>> onRestartButtonClicked) {
super(context, taskInfo, syncQueue, taskListener, displayLayout);
mCallback = callback;
- mHasSizeCompat = taskInfo.appCompatTaskInfo.topActivityInSizeCompat
- && shouldShowSizeCompatRestartButton(taskInfo);
+ mHasSizeCompat = taskInfo.appCompatTaskInfo.topActivityInSizeCompat;
mCameraCompatControlState = taskInfo.appCompatTaskInfo.cameraCompatControlState;
mCompatUIHintsState = compatUIHintsState;
mCompatUIConfiguration = compatUIConfiguration;
@@ -106,7 +105,8 @@
@Override
protected boolean eligibleToShowLayout() {
- return mHasSizeCompat || shouldShowCameraControl();
+ return (mHasSizeCompat && shouldShowSizeCompatRestartButton(getLastTaskInfo()))
+ || shouldShowCameraControl();
}
@Override
@@ -114,11 +114,6 @@
mLayout = inflateLayout();
mLayout.inject(this);
- final TaskInfo taskInfo = getLastTaskInfo();
- if (taskInfo != null) {
- mHasSizeCompat = mHasSizeCompat && shouldShowSizeCompatRestartButton(taskInfo);
- }
-
updateVisibilityOfViews();
if (mHasSizeCompat) {
@@ -139,8 +134,7 @@
boolean canShow) {
final boolean prevHasSizeCompat = mHasSizeCompat;
final int prevCameraCompatControlState = mCameraCompatControlState;
- mHasSizeCompat = taskInfo.appCompatTaskInfo.topActivityInSizeCompat
- && shouldShowSizeCompatRestartButton(taskInfo);
+ mHasSizeCompat = taskInfo.appCompatTaskInfo.topActivityInSizeCompat;
mCameraCompatControlState = taskInfo.appCompatTaskInfo.cameraCompatControlState;
if (!super.updateCompatInfo(taskInfo, taskListener, canShow)) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManagerAbstract.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManagerAbstract.java
index 180498c..0564c95 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManagerAbstract.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManagerAbstract.java
@@ -332,7 +332,7 @@
updateSurfacePosition();
}
- @Nullable
+ @NonNull
protected TaskInfo getLastTaskInfo() {
return mTaskInfo;
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java
index d023cea..1232baa 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java
@@ -1020,7 +1020,7 @@
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
"RecentsController.finishInner: no valid PiP leash;"
+ "mPipTransaction=%s, mPipTask=%s, mPipTaskId=%d",
- mPipTransaction.toString(), mPipTask.toString(), mPipTaskId);
+ mPipTransaction, mPipTask, mPipTaskId);
} else {
t.show(pipLeash);
PictureInPictureSurfaceTransaction.apply(mPipTransaction, pipLeash, t);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java
index 5a74255..e6faa63 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java
@@ -93,7 +93,7 @@
// On a smaller screen, don't require as much empty space on screen, as offscreen
// drags will be restricted too much.
- final int requiredEmptySpaceId = mDisplayController.getDisplayContext(mTaskInfo.taskId)
+ final int requiredEmptySpaceId = mDisplayController.getDisplayContext(mTaskInfo.displayId)
.getResources().getConfiguration().smallestScreenWidthDp >= 600
? R.dimen.freeform_required_visible_empty_space_in_header :
R.dimen.small_screen_required_visible_empty_space_in_header;
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUILayoutTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUILayoutTest.java
index 4ddc539..dd358e7 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUILayoutTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUILayoutTest.java
@@ -30,6 +30,7 @@
import android.app.ActivityManager;
import android.app.AppCompatTaskInfo.CameraCompatControlState;
import android.app.TaskInfo;
+import android.graphics.Rect;
import android.testing.AndroidTestingRunner;
import android.util.Pair;
import android.view.LayoutInflater;
@@ -83,6 +84,7 @@
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
+ doReturn(100).when(mCompatUIConfiguration).getHideSizeCompatRestartButtonTolerance();
mTaskInfo = createTaskInfo(/* hasSizeCompat= */ false, CAMERA_COMPAT_CONTROL_HIDDEN);
mWindowManager = new CompatUIWindowManager(mContext, mTaskInfo, mSyncTransactionQueue,
mCallback, mTaskListener, new DisplayLayout(), new CompatUIHintsState(),
@@ -127,7 +129,6 @@
@Test
public void testOnClickForSizeCompatHint() {
mWindowManager.mHasSizeCompat = true;
- doReturn(true).when(mWindowManager).shouldShowSizeCompatRestartButton(mTaskInfo);
mWindowManager.createLayout(/* canShow= */ true);
final LinearLayout sizeCompatHint = mLayout.findViewById(R.id.size_compat_hint);
sizeCompatHint.performClick();
@@ -222,6 +223,9 @@
taskInfo.taskId = TASK_ID;
taskInfo.appCompatTaskInfo.topActivityInSizeCompat = hasSizeCompat;
taskInfo.appCompatTaskInfo.cameraCompatControlState = cameraCompatControlState;
+ taskInfo.appCompatTaskInfo.topActivityLetterboxHeight = 1000;
+ taskInfo.appCompatTaskInfo.topActivityLetterboxWidth = 1000;
+ taskInfo.configuration.windowConfiguration.setBounds(new Rect(0, 0, 2000, 2000));
return taskInfo;
}
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIWindowManagerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIWindowManagerTest.java
index 2acfd83..4f261cd 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIWindowManagerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIWindowManagerTest.java
@@ -20,7 +20,6 @@
import static android.app.AppCompatTaskInfo.CAMERA_COMPAT_CONTROL_HIDDEN;
import static android.app.AppCompatTaskInfo.CAMERA_COMPAT_CONTROL_TREATMENT_APPLIED;
import static android.app.AppCompatTaskInfo.CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED;
-import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
import static android.platform.test.flag.junit.SetFlagsRule.DefaultInitValueType.DEVICE_DEFAULT;
import static android.view.WindowInsets.Type.navigationBars;
@@ -86,6 +85,8 @@
public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(DEVICE_DEFAULT);
private static final int TASK_ID = 1;
+ private static final int TASK_WIDTH = 2000;
+ private static final int TASK_HEIGHT = 2000;
@Mock private SyncTransactionQueue mSyncTransactionQueue;
@Mock private CompatUIController.CompatUICallback mCallback;
@@ -101,6 +102,7 @@
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
+ doReturn(100).when(mCompatUIConfiguration).getHideSizeCompatRestartButtonTolerance();
mTaskInfo = createTaskInfo(/* hasSizeCompat= */ false, CAMERA_COMPAT_CONTROL_HIDDEN);
mWindowManager = new CompatUIWindowManager(mContext, mTaskInfo, mSyncTransactionQueue,
mCallback, mTaskListener, new DisplayLayout(), new CompatUIHintsState(),
@@ -115,7 +117,6 @@
public void testCreateSizeCompatButton() {
// Doesn't create layout if show is false.
mWindowManager.mHasSizeCompat = true;
- doReturn(true).when(mWindowManager).shouldShowSizeCompatRestartButton(mTaskInfo);
assertTrue(mWindowManager.createLayout(/* canShow= */ false));
verify(mWindowManager, never()).inflateLayout();
@@ -147,6 +148,13 @@
mWindowManager.mHasSizeCompat = false;
assertFalse(mWindowManager.createLayout(/* canShow= */ true));
+ // Returns false and doesn't create layout if restart button should be hidden.
+ clearInvocations(mWindowManager);
+ mWindowManager.mHasSizeCompat = true;
+ mTaskInfo.appCompatTaskInfo.topActivityLetterboxWidth = TASK_WIDTH;
+ mTaskInfo.appCompatTaskInfo.topActivityLetterboxHeight = TASK_HEIGHT;
+ assertFalse(mWindowManager.createLayout(/* canShow= */ true));
+
verify(mWindowManager, never()).inflateLayout();
}
@@ -293,8 +301,6 @@
@Test
public void testUpdateCompatInfoLayoutNotInflatedYet() {
- mWindowManager.mHasSizeCompat = true;
- doReturn(true).when(mWindowManager).shouldShowSizeCompatRestartButton(any());
mWindowManager.createLayout(/* canShow= */ false);
verify(mWindowManager, never()).inflateLayout();
@@ -314,6 +320,15 @@
mWindowManager.updateCompatInfo(taskInfo, mTaskListener, /* canShow= */ true);
verify(mWindowManager).inflateLayout();
+
+ // Change shouldShowSizeCompatRestartButton to false and pass canShow true, layout
+ // shouldn't be inflated
+ clearInvocations(mWindowManager);
+ taskInfo.appCompatTaskInfo.topActivityLetterboxWidth = TASK_WIDTH;
+ taskInfo.appCompatTaskInfo.topActivityLetterboxHeight = TASK_HEIGHT;
+ mWindowManager.updateCompatInfo(taskInfo, mTaskListener, /* canShow= */ true);
+
+ verify(mWindowManager, never()).inflateLayout();
}
@Test
@@ -364,7 +379,6 @@
// Create button if it is not created.
mWindowManager.mLayout = null;
mWindowManager.mHasSizeCompat = true;
- doReturn(true).when(mWindowManager).shouldShowSizeCompatRestartButton(mTaskInfo);
mWindowManager.updateVisibility(/* canShow= */ true);
verify(mWindowManager).createLayout(/* canShow= */ true);
@@ -489,7 +503,6 @@
TaskInfo taskInfo = createTaskInfo(true, CAMERA_COMPAT_CONTROL_HIDDEN);
taskInfo.configuration.windowConfiguration.setBounds(new Rect(0, 0, 2000, 2000));
taskInfo.appCompatTaskInfo.topActivityLetterboxHeight = 2000;
- taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN);
taskInfo.appCompatTaskInfo.topActivityLetterboxWidth = 1850;
assertFalse(mWindowManager.shouldShowSizeCompatRestartButton(taskInfo));
@@ -514,6 +527,11 @@
taskInfo.appCompatTaskInfo.topActivityInSizeCompat = hasSizeCompat;
taskInfo.appCompatTaskInfo.cameraCompatControlState = cameraCompatControlState;
taskInfo.configuration.uiMode &= ~Configuration.UI_MODE_TYPE_DESK;
+ // Letterboxed activity that takes half the screen should show size compat restart button
+ taskInfo.configuration.windowConfiguration.setBounds(
+ new Rect(0, 0, TASK_WIDTH, TASK_HEIGHT));
+ taskInfo.appCompatTaskInfo.topActivityLetterboxHeight = 1000;
+ taskInfo.appCompatTaskInfo.topActivityLetterboxWidth = 1000;
return taskInfo;
}
}
diff --git a/libs/hwui/Android.bp b/libs/hwui/Android.bp
index b40b73c..0abb6f5 100644
--- a/libs/hwui/Android.bp
+++ b/libs/hwui/Android.bp
@@ -229,15 +229,6 @@
path: "apex/java",
}
-java_api_contribution {
- name: "framework-graphics-public-stubs",
- api_surface: "public",
- api_file: "api/current.txt",
- visibility: [
- "//build/orchestrator/apis",
- ],
-}
-
// ------------------------
// APEX
// ------------------------
diff --git a/libs/hwui/AutoBackendTextureRelease.cpp b/libs/hwui/AutoBackendTextureRelease.cpp
index 4d020c5..5f5ffe9 100644
--- a/libs/hwui/AutoBackendTextureRelease.cpp
+++ b/libs/hwui/AutoBackendTextureRelease.cpp
@@ -21,6 +21,7 @@
#include <include/gpu/GrDirectContext.h>
#include <include/gpu/GrBackendSurface.h>
#include <include/gpu/MutableTextureState.h>
+#include <include/gpu/vk/VulkanMutableTextureState.h>
#include "renderthread/RenderThread.h"
#include "utils/Color.h"
#include "utils/PaintUtils.h"
@@ -142,8 +143,9 @@
LOG_ALWAYS_FATAL_IF(Properties::getRenderPipelineType() != RenderPipelineType::SkiaVulkan);
if (mBackendTexture.isValid()) {
// Passing in VK_IMAGE_LAYOUT_UNDEFINED means we keep the old layout.
- skgpu::MutableTextureState newState(VK_IMAGE_LAYOUT_UNDEFINED,
- VK_QUEUE_FAMILY_FOREIGN_EXT);
+ skgpu::MutableTextureState newState = skgpu::MutableTextureStates::MakeVulkan(
+ VK_IMAGE_LAYOUT_UNDEFINED,
+ VK_QUEUE_FAMILY_FOREIGN_EXT);
// The unref for this ref happens in the releaseProc passed into setBackendTextureState. The
// releaseProc callback will be made when the work to set the new state has finished on the
diff --git a/libs/hwui/RenderNode.cpp b/libs/hwui/RenderNode.cpp
index 0b42c88..f526a28 100644
--- a/libs/hwui/RenderNode.cpp
+++ b/libs/hwui/RenderNode.cpp
@@ -230,7 +230,7 @@
* stencil buffer may be needed. Views that use a functor to draw will be forced onto a layer.
*/
void RenderNode::prepareTreeImpl(TreeObserver& observer, TreeInfo& info, bool functorsNeedLayer) {
- if (mDamageGenerationId == info.damageGenerationId) {
+ if (mDamageGenerationId == info.damageGenerationId && mDamageGenerationId != 0) {
// We hit the same node a second time in the same tree. We don't know the minimal
// damage rect anymore, so just push the biggest we can onto our parent's transform
// We push directly onto parent in case we are clipped to bounds but have moved position.
diff --git a/libs/hwui/RenderNode.h b/libs/hwui/RenderNode.h
index 1f3834be..c904542 100644
--- a/libs/hwui/RenderNode.h
+++ b/libs/hwui/RenderNode.h
@@ -262,7 +262,7 @@
DisplayList mDisplayList;
DisplayList mStagingDisplayList;
- int64_t mDamageGenerationId;
+ int64_t mDamageGenerationId = 0;
friend class AnimatorManager;
AnimatorManager mAnimatorManager;
diff --git a/libs/hwui/jni/PathMeasure.cpp b/libs/hwui/jni/PathMeasure.cpp
index acf893e..79acb6c 100644
--- a/libs/hwui/jni/PathMeasure.cpp
+++ b/libs/hwui/jni/PathMeasure.cpp
@@ -17,7 +17,11 @@
#include "GraphicsJNI.h"
+#include "SkMatrix.h"
+#include "SkPath.h"
#include "SkPathMeasure.h"
+#include "SkPoint.h"
+#include "SkScalar.h"
/* We declare an explicit pair, so that we don't have to rely on the java
client to be sure not to edit the path while we have an active measure
diff --git a/media/java/android/media/LoudnessCodecController.java b/media/java/android/media/LoudnessCodecController.java
index b3e5c52..61c9131 100644
--- a/media/java/android/media/LoudnessCodecController.java
+++ b/media/java/android/media/LoudnessCodecController.java
@@ -32,12 +32,13 @@
import java.util.HashMap;
import java.util.HashSet;
-import java.util.Map;
+import java.util.Map.Entry;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.function.Consumer;
/**
* Class for getting recommended loudness parameter updates for audio decoders as they are used
@@ -320,11 +321,6 @@
* Stops any loudness updates and frees up the resources.
*/
@FlaggedApi(FLAG_LOUDNESS_CONFIGURATOR_API)
- public void release() {
- close();
- }
-
- /** @hide */
@Override
public void close() {
synchronized (mControllerLock) {
@@ -339,9 +335,12 @@
}
/** @hide */
- /*package*/ Map<LoudnessCodecInfo, Set<MediaCodec>> getRegisteredMediaCodecs() {
+ /*package*/ void mediaCodecsConsume(
+ Consumer<Entry<LoudnessCodecInfo, Set<MediaCodec>>> consumer) {
synchronized (mControllerLock) {
- return mMediaCodecs;
+ for (Entry<LoudnessCodecInfo, Set<MediaCodec>> entry : mMediaCodecs.entrySet()) {
+ consumer.accept(entry);
+ }
}
}
diff --git a/media/java/android/media/LoudnessCodecDispatcher.java b/media/java/android/media/LoudnessCodecDispatcher.java
index 46be54b..fa08658 100644
--- a/media/java/android/media/LoudnessCodecDispatcher.java
+++ b/media/java/android/media/LoudnessCodecDispatcher.java
@@ -32,7 +32,6 @@
import java.util.HashMap;
import java.util.Iterator;
-import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Set;
@@ -81,16 +80,15 @@
mConfiguratorListener.computeIfPresent(listener, (l, lcConfig) -> {
// send the appropriate bundle for the user to update
if (lcConfig.getSessionId() == sessionId) {
- final Map<LoudnessCodecInfo, Set<MediaCodec>> mediaCodecsMap =
- lcConfig.getRegisteredMediaCodecs();
- for (LoudnessCodecInfo codecInfo : mediaCodecsMap.keySet()) {
+ lcConfig.mediaCodecsConsume(mcEntry -> {
+ final LoudnessCodecInfo codecInfo = mcEntry.getKey();
final String infoKey = Integer.toString(codecInfo.hashCode());
Bundle bundle = null;
if (params.containsKey(infoKey)) {
bundle = new Bundle(params.getPersistableBundle(infoKey));
}
- final Set<MediaCodec> mediaCodecs = mediaCodecsMap.get(codecInfo);
+ final Set<MediaCodec> mediaCodecs = mcEntry.getValue();
for (MediaCodec mediaCodec : mediaCodecs) {
final String mediaCodecKey = Integer.toString(
mediaCodec.hashCode());
@@ -121,7 +119,7 @@
break;
}
}
- }
+ });
}
return lcConfig;
});
diff --git a/media/java/android/media/MediaRouter2.java b/media/java/android/media/MediaRouter2.java
index 687feef..425db06 100644
--- a/media/java/android/media/MediaRouter2.java
+++ b/media/java/android/media/MediaRouter2.java
@@ -183,30 +183,30 @@
* preference} passed by a proxy router. Use {@link RouteDiscoveryPreference#EMPTY} when
* setting a route callback.
* <li>
- * <p>Methods returning non-system {@link RoutingController controllers} always return
- * new instances with the latest data. Do not attempt to compare or store them. Instead,
- * use {@link #getController(String)} or {@link #getControllers()} to query the most
+ * <p>Methods returning non-system {@link RoutingController controllers} always return new
+ * instances with the latest data. Do not attempt to compare or store them. Instead, use
+ * {@link #getController(String)} or {@link #getControllers()} to query the most
* up-to-date state.
* <li>
* <p>Calls to {@link #setOnGetControllerHintsListener} are ignored.
* </ul>
*
* @param clientPackageName the package name of the app to control
- * @throws SecurityException if the caller doesn't have {@link
- * Manifest.permission#MEDIA_CONTENT_CONTROL MEDIA_CONTENT_CONTROL} permission.
- * @hide
+ * @return a proxy MediaRouter2 instance if {@code clientPackageName} exists or {@code null}.
*/
- // TODO (b/311711420): Deprecate once #getInstance(Context, Looper, String, UserHandle)
- // reaches public SDK.
- @SystemApi
- @RequiresPermission(Manifest.permission.MEDIA_CONTENT_CONTROL)
+ @FlaggedApi(FLAG_ENABLE_CROSS_USER_ROUTING_IN_MEDIA_ROUTER2)
+ @RequiresPermission(
+ anyOf = {
+ Manifest.permission.MEDIA_CONTENT_CONTROL,
+ Manifest.permission.MEDIA_ROUTING_CONTROL
+ })
@Nullable
public static MediaRouter2 getInstance(
@NonNull Context context, @NonNull String clientPackageName) {
// Capturing the IAE here to not break nullability.
try {
return findOrCreateProxyInstanceForCallingUser(
- context, Looper.getMainLooper(), clientPackageName, context.getUser());
+ context, clientPackageName, context.getUser());
} catch (IllegalArgumentException ex) {
Log.e(TAG, "Package " + clientPackageName + " not found. Ignoring.");
return null;
@@ -217,8 +217,6 @@
* Returns a proxy MediaRouter2 instance that allows you to control the routing of an app
* specified by {@code clientPackageName} and {@code user}.
*
- * <p>You can specify any {@link Looper} of choice on which internal state updates will run.
- *
* <p>Proxy MediaRouter2 instances operate differently than regular MediaRouter2 instances:
*
* <ul>
@@ -228,16 +226,15 @@
* {@link RouteDiscoveryPreference.Builder#setPreferredFeatures(List) preferred features}
* when setting a route callback.
* <li>
- * <p>Methods returning non-system {@link RoutingController controllers} always return
- * new instances with the latest data. Do not attempt to compare or store them. Instead,
- * use {@link #getController(String)} or {@link #getControllers()} to query the most
+ * <p>Methods returning non-system {@link RoutingController controllers} always return new
+ * instances with the latest data. Do not attempt to compare or store them. Instead, use
+ * {@link #getController(String)} or {@link #getControllers()} to query the most
* up-to-date state.
* <li>
* <p>Calls to {@link #setOnGetControllerHintsListener} are ignored.
* </ul>
*
* @param context The {@link Context} of the caller.
- * @param looper The {@link Looper} on which to process internal state changes.
* @param clientPackageName The package name of the app you want to control the routing of.
* @param user The {@link UserHandle} of the user running the app for which to get the proxy
* router instance. Must match {@link Process#myUserHandle()} if the caller doesn't hold
@@ -245,8 +242,8 @@
* @throws SecurityException if {@code user} does not match {@link Process#myUserHandle()} and
* the caller does not hold {@code Manifest.permission#INTERACT_ACROSS_USERS_FULL}.
* @throws IllegalArgumentException if {@code clientPackageName} does not exist in {@code user}.
+ * @hide
*/
- @FlaggedApi(FLAG_ENABLE_CROSS_USER_ROUTING_IN_MEDIA_ROUTER2)
@RequiresPermission(
anyOf = {
Manifest.permission.MEDIA_CONTENT_CONTROL,
@@ -254,11 +251,8 @@
})
@NonNull
public static MediaRouter2 getInstance(
- @NonNull Context context,
- @NonNull Looper looper,
- @NonNull String clientPackageName,
- @NonNull UserHandle user) {
- return findOrCreateProxyInstanceForCallingUser(context, looper, clientPackageName, user);
+ @NonNull Context context, @NonNull String clientPackageName, @NonNull UserHandle user) {
+ return findOrCreateProxyInstanceForCallingUser(context, clientPackageName, user);
}
/**
@@ -270,9 +264,8 @@
*/
@NonNull
private static MediaRouter2 findOrCreateProxyInstanceForCallingUser(
- Context context, Looper looper, String clientPackageName, UserHandle user) {
+ Context context, String clientPackageName, UserHandle user) {
Objects.requireNonNull(context, "context must not be null");
- Objects.requireNonNull(looper, "looper must not be null");
Objects.requireNonNull(user, "user must not be null");
if (TextUtils.isEmpty(clientPackageName)) {
@@ -284,7 +277,8 @@
synchronized (sSystemRouterLock) {
MediaRouter2 instance = sAppToProxyRouterMap.get(key);
if (instance == null) {
- instance = new MediaRouter2(context, looper, clientPackageName, user);
+ instance =
+ new MediaRouter2(context, Looper.getMainLooper(), clientPackageName, user);
// Register proxy router after instantiation to avoid race condition.
((ProxyMediaRouter2Impl) instance.mImpl).registerProxyRouter();
sAppToProxyRouterMap.put(key, instance);
diff --git a/media/java/android/media/RoutingSessionInfo.java b/media/java/android/media/RoutingSessionInfo.java
index d28c26d..2202766 100644
--- a/media/java/android/media/RoutingSessionInfo.java
+++ b/media/java/android/media/RoutingSessionInfo.java
@@ -182,7 +182,7 @@
mControlHints = src.readBundle();
mIsSystemSession = src.readBoolean();
mTransferReason = src.readInt();
- mTransferInitiatorUserHandle = src.readParcelable(null, android.os.UserHandle.class);
+ mTransferInitiatorUserHandle = UserHandle.readFromParcel(src);
mTransferInitiatorPackageName = src.readString();
}
@@ -417,11 +417,7 @@
dest.writeBundle(mControlHints);
dest.writeBoolean(mIsSystemSession);
dest.writeInt(mTransferReason);
- if (mTransferInitiatorUserHandle != null) {
- mTransferInitiatorUserHandle.writeToParcel(dest, /* flags= */ 0);
- } else {
- dest.writeParcelable(null, /* flags= */ 0);
- }
+ UserHandle.writeToParcel(mTransferInitiatorUserHandle, dest);
dest.writeString(mTransferInitiatorPackageName);
}
diff --git a/media/java/android/media/audiofx/Virtualizer.java b/media/java/android/media/audiofx/Virtualizer.java
index 74b6fc1..71147f4 100644
--- a/media/java/android/media/audiofx/Virtualizer.java
+++ b/media/java/android/media/audiofx/Virtualizer.java
@@ -46,6 +46,11 @@
* <p>See {@link android.media.MediaPlayer#getAudioSessionId()} for details on audio sessions.
* <p>See {@link android.media.audiofx.AudioEffect} class for more details on controlling
* audio effects.
+ *
+ * @deprecated use the {@link android.media.Spatializer} class to query the capabilities of the
+ * platform with regards to spatialization, a different name for audio channel virtualization,
+ * and the {@link android.media.AudioAttributes.Builder#setSpatializationBehavior(int)} to
+ * characterize how you want your content to be played when spatialization is supported.
*/
public class Virtualizer extends AudioEffect {
diff --git a/media/java/android/media/flags/editing.aconfig b/media/java/android/media/flags/editing.aconfig
new file mode 100644
index 0000000..c3997e9
--- /dev/null
+++ b/media/java/android/media/flags/editing.aconfig
@@ -0,0 +1,8 @@
+package: "com.android.media.editing.flags"
+
+flag {
+ name: "add_media_metrics_editing"
+ namespace: "media_solutions"
+ description: "Add media metrics for transcoding/editing events."
+ bug: "297487694"
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/VolumePanelComponentKey.kt b/media/java/android/media/metrics/EditingEndedEvent.aidl
similarity index 70%
copy from packages/SystemUI/src/com/android/systemui/volume/panel/VolumePanelComponentKey.kt
copy to media/java/android/media/metrics/EditingEndedEvent.aidl
index 22a74d2..e099dea 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/VolumePanelComponentKey.kt
+++ b/media/java/android/media/metrics/EditingEndedEvent.aidl
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2023 The Android Open Source Project
+ * Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,7 +14,6 @@
* limitations under the License.
*/
-package com.android.systemui.volume.panel
+package android.media.metrics;
-/** Uniquely identifies the [com.android.systemui.volume.panel.ui.VolumePanelComponent]. */
-typealias VolumePanelComponentKey = String
+parcelable EditingEndedEvent;
diff --git a/media/java/android/media/metrics/EditingEndedEvent.java b/media/java/android/media/metrics/EditingEndedEvent.java
new file mode 100644
index 0000000..72e6db8
--- /dev/null
+++ b/media/java/android/media/metrics/EditingEndedEvent.java
@@ -0,0 +1,325 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.media.metrics;
+
+import static com.android.media.editing.flags.Flags.FLAG_ADD_MEDIA_METRICS_EDITING;
+
+import android.annotation.FlaggedApi;
+import android.annotation.IntDef;
+import android.annotation.IntRange;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.lang.annotation.Retention;
+import java.util.Objects;
+
+/** Event for an editing operation having ended. */
+@FlaggedApi(FLAG_ADD_MEDIA_METRICS_EDITING)
+public final class EditingEndedEvent extends Event implements Parcelable {
+
+ // The special value 0 is reserved for the field being unspecified in the proto.
+
+ /** The editing operation was successful. */
+ public static final int FINAL_STATE_SUCCEEDED = 1;
+
+ /** The editing operation was canceled. */
+ public static final int FINAL_STATE_CANCELED = 2;
+
+ /** The editing operation failed due to an error. */
+ public static final int FINAL_STATE_ERROR = 3;
+
+ /** @hide */
+ @IntDef(
+ prefix = {"FINAL_STATE_"},
+ value = {
+ FINAL_STATE_SUCCEEDED,
+ FINAL_STATE_CANCELED,
+ FINAL_STATE_ERROR,
+ })
+ @Retention(java.lang.annotation.RetentionPolicy.SOURCE)
+ public @interface FinalState {}
+
+ private final @FinalState int mFinalState;
+
+ // The special value 0 is reserved for the field being unspecified in the proto.
+
+ /** Special value representing that no error occurred. */
+ public static final int ERROR_CODE_NONE = 1;
+
+ /** Error code for unexpected runtime errors. */
+ public static final int ERROR_CODE_FAILED_RUNTIME_CHECK = 2;
+
+ /** Error code for non-specific errors during input/output. */
+ public static final int ERROR_CODE_IO_UNSPECIFIED = 3;
+
+ /** Error code for network connection failures. */
+ public static final int ERROR_CODE_IO_NETWORK_CONNECTION_FAILED = 4;
+
+ /** Error code for network timeouts. */
+ public static final int ERROR_CODE_IO_NETWORK_CONNECTION_TIMEOUT = 5;
+
+ /** Caused by an HTTP server returning an unexpected HTTP response status code. */
+ public static final int ERROR_CODE_IO_BAD_HTTP_STATUS = 6;
+
+ /** Caused by a non-existent file. */
+ public static final int ERROR_CODE_IO_FILE_NOT_FOUND = 7;
+
+ /**
+ * Caused by lack of permission to perform an IO operation. For example, lack of permission to
+ * access internet or external storage.
+ */
+ public static final int ERROR_CODE_IO_NO_PERMISSION = 8;
+
+ /** */
+ public static final int ERROR_CODE_IO_CLEARTEXT_NOT_PERMITTED = 9;
+
+ /** Caused by reading data out of the data bounds. */
+ public static final int ERROR_CODE_IO_READ_POSITION_OUT_OF_RANGE = 10;
+
+ /** Caused by a decoder initialization failure. */
+ public static final int ERROR_CODE_DECODER_INIT_FAILED = 11;
+
+ /** Caused by a failure while trying to decode media samples. */
+ public static final int ERROR_CODE_DECODING_FAILED = 12;
+
+ /** Caused by trying to decode content whose format is not supported. */
+ public static final int ERROR_CODE_DECODING_FORMAT_UNSUPPORTED = 13;
+
+ /** Caused by an encoder initialization failure. */
+ public static final int ERROR_CODE_ENCODER_INIT_FAILED = 14;
+
+ /** Caused by a failure while trying to encode media samples. */
+ public static final int ERROR_CODE_ENCODING_FAILED = 15;
+
+ /** Caused by trying to encode content whose format is not supported. */
+ public static final int ERROR_CODE_ENCODING_FORMAT_UNSUPPORTED = 16;
+
+ /** Caused by a video frame processing failure. */
+ public static final int ERROR_CODE_VIDEO_FRAME_PROCESSING_FAILED = 17;
+
+ /** Caused by an audio processing failure. */
+ public static final int ERROR_CODE_AUDIO_PROCESSING_FAILED = 18;
+
+ /** Caused by a failure while muxing media samples. */
+ public static final int ERROR_CODE_MUXING_FAILED = 19;
+
+ /** @hide */
+ @IntDef(
+ prefix = {"ERROR_CODE_"},
+ value = {
+ ERROR_CODE_NONE,
+ ERROR_CODE_FAILED_RUNTIME_CHECK,
+ ERROR_CODE_IO_UNSPECIFIED,
+ ERROR_CODE_IO_NETWORK_CONNECTION_FAILED,
+ ERROR_CODE_IO_NETWORK_CONNECTION_TIMEOUT,
+ ERROR_CODE_IO_BAD_HTTP_STATUS,
+ ERROR_CODE_IO_FILE_NOT_FOUND,
+ ERROR_CODE_IO_NO_PERMISSION,
+ ERROR_CODE_IO_CLEARTEXT_NOT_PERMITTED,
+ ERROR_CODE_IO_READ_POSITION_OUT_OF_RANGE,
+ ERROR_CODE_DECODER_INIT_FAILED,
+ ERROR_CODE_DECODING_FAILED,
+ ERROR_CODE_DECODING_FORMAT_UNSUPPORTED,
+ ERROR_CODE_ENCODER_INIT_FAILED,
+ ERROR_CODE_ENCODING_FAILED,
+ ERROR_CODE_ENCODING_FORMAT_UNSUPPORTED,
+ ERROR_CODE_VIDEO_FRAME_PROCESSING_FAILED,
+ ERROR_CODE_AUDIO_PROCESSING_FAILED,
+ ERROR_CODE_MUXING_FAILED,
+ })
+ @Retention(java.lang.annotation.RetentionPolicy.SOURCE)
+ public @interface ErrorCode {}
+
+ private final @ErrorCode int mErrorCode;
+ @SuppressWarnings("HidingField") // Hiding field from superclass as for playback events.
+ private final long mTimeSinceCreatedMillis;
+
+ private EditingEndedEvent(
+ @FinalState int finalState,
+ @ErrorCode int errorCode,
+ long timeSinceCreatedMillis,
+ @NonNull Bundle extras) {
+ mFinalState = finalState;
+ mErrorCode = errorCode;
+ mTimeSinceCreatedMillis = timeSinceCreatedMillis;
+ mMetricsBundle = extras.deepCopy();
+ }
+
+ /** Returns the state of the editing session when it ended. */
+ @FinalState
+ public int getFinalState() {
+ return mFinalState;
+ }
+
+ /** Returns the error code for a {@linkplain #FINAL_STATE_ERROR failed} editing session. */
+ @ErrorCode
+ public int getErrorCode() {
+ return mErrorCode;
+ }
+
+ /**
+ * Gets the elapsed time since creating of the editing session, in milliseconds, or -1 if
+ * unknown.
+ *
+ * @return The elapsed time since creating the editing session, in milliseconds, or -1 if
+ * unknown.
+ * @see LogSessionId
+ * @see EditingSession
+ */
+ @Override
+ @IntRange(from = -1)
+ public long getTimeSinceCreatedMillis() {
+ return mTimeSinceCreatedMillis;
+ }
+
+ /**
+ * Gets metrics-related information that is not supported by dedicated methods.
+ *
+ * <p>It is intended to be used for backwards compatibility by the metrics infrastructure.
+ */
+ @Override
+ @NonNull
+ public Bundle getMetricsBundle() {
+ return mMetricsBundle;
+ }
+
+ @Override
+ @NonNull
+ public String toString() {
+ return "PlaybackErrorEvent { "
+ + "finalState = "
+ + mFinalState
+ + ", "
+ + "errorCode = "
+ + mErrorCode
+ + ", "
+ + "timeSinceCreatedMillis = "
+ + mTimeSinceCreatedMillis
+ + " }";
+ }
+
+ @Override
+ public boolean equals(@Nullable Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ EditingEndedEvent that = (EditingEndedEvent) o;
+ return mFinalState == that.mFinalState
+ && mErrorCode == that.mErrorCode
+ && mTimeSinceCreatedMillis == that.mTimeSinceCreatedMillis;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mFinalState, mErrorCode, mTimeSinceCreatedMillis);
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ dest.writeInt(mFinalState);
+ dest.writeInt(mErrorCode);
+ dest.writeLong(mTimeSinceCreatedMillis);
+ dest.writeBundle(mMetricsBundle);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ private EditingEndedEvent(@NonNull Parcel in) {
+ int finalState = in.readInt();
+ int errorCode = in.readInt();
+ long timeSinceCreatedMillis = in.readLong();
+ Bundle metricsBundle = in.readBundle();
+
+ mFinalState = finalState;
+ mErrorCode = errorCode;
+ mTimeSinceCreatedMillis = timeSinceCreatedMillis;
+ mMetricsBundle = metricsBundle;
+ }
+
+ public static final @NonNull Creator<EditingEndedEvent> CREATOR =
+ new Creator<>() {
+ @Override
+ public EditingEndedEvent[] newArray(int size) {
+ return new EditingEndedEvent[size];
+ }
+
+ @Override
+ public EditingEndedEvent createFromParcel(@NonNull Parcel in) {
+ return new EditingEndedEvent(in);
+ }
+ };
+
+ /** Builder for {@link EditingEndedEvent} */
+ @FlaggedApi(FLAG_ADD_MEDIA_METRICS_EDITING)
+ public static final class Builder {
+ private final @FinalState int mFinalState;
+ private @ErrorCode int mErrorCode;
+ private long mTimeSinceCreatedMillis;
+ private Bundle mMetricsBundle;
+
+ /**
+ * Creates a new Builder.
+ *
+ * @param finalState The state of the editing session when it ended.
+ */
+ public Builder(@FinalState int finalState) {
+ mFinalState = finalState;
+ mErrorCode = ERROR_CODE_NONE;
+ mTimeSinceCreatedMillis = -1;
+ mMetricsBundle = new Bundle();
+ }
+
+ /**
+ * Sets the elapsed time since creating the editing session, in milliseconds.
+ *
+ * @param timeSinceCreatedMillis The elapsed time since creating the editing session, in
+ * milliseconds, or -1 if the value is unknown.
+ * @see #getTimeSinceCreatedMillis()
+ */
+ public @NonNull Builder setTimeSinceCreatedMillis(
+ @IntRange(from = -1) long timeSinceCreatedMillis) {
+ mTimeSinceCreatedMillis = timeSinceCreatedMillis;
+ return this;
+ }
+
+ /** Sets the error code for a {@linkplain #FINAL_STATE_ERROR failed} editing session. */
+ public @NonNull Builder setErrorCode(@ErrorCode int value) {
+ mErrorCode = value;
+ return this;
+ }
+
+ /**
+ * Sets metrics-related information that is not supported by dedicated methods.
+ *
+ * <p>Used for backwards compatibility by the metrics infrastructure.
+ */
+ public @NonNull Builder setMetricsBundle(@NonNull Bundle metricsBundle) {
+ mMetricsBundle = metricsBundle;
+ return this;
+ }
+
+ /** Builds an instance. */
+ public @NonNull EditingEndedEvent build() {
+ return new EditingEndedEvent(
+ mFinalState, mErrorCode, mTimeSinceCreatedMillis, mMetricsBundle);
+ }
+ }
+}
diff --git a/media/java/android/media/metrics/EditingSession.java b/media/java/android/media/metrics/EditingSession.java
index 2ddf623b..964e12c 100644
--- a/media/java/android/media/metrics/EditingSession.java
+++ b/media/java/android/media/metrics/EditingSession.java
@@ -16,6 +16,9 @@
package android.media.metrics;
+import static com.android.media.editing.flags.Flags.FLAG_ADD_MEDIA_METRICS_EDITING;
+
+import android.annotation.FlaggedApi;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -24,7 +27,8 @@
import java.util.Objects;
/**
- * An instances of this class represents a session of media editing.
+ * Represents a session of media editing, for example, transcoding between formats, transmuxing or
+ * applying trimming or audio/video effects to a stream.
*/
public final class EditingSession implements AutoCloseable {
private final @NonNull String mId;
@@ -40,6 +44,13 @@
mLogSessionId = new LogSessionId(mId);
}
+ /** Reports that an editing operation ended. */
+ @FlaggedApi(FLAG_ADD_MEDIA_METRICS_EDITING)
+ public void reportEditingEndedEvent(@NonNull EditingEndedEvent editingEndedEvent) {
+ mManager.reportEditingEndedEvent(mId, editingEndedEvent);
+ }
+
+ /** Returns the identifier for logging this session. */
public @NonNull LogSessionId getSessionId() {
return mLogSessionId;
}
diff --git a/media/java/android/media/metrics/IMediaMetricsManager.aidl b/media/java/android/media/metrics/IMediaMetricsManager.aidl
index 51b1cc2..e07ca67 100644
--- a/media/java/android/media/metrics/IMediaMetricsManager.aidl
+++ b/media/java/android/media/metrics/IMediaMetricsManager.aidl
@@ -16,6 +16,7 @@
package android.media.metrics;
+import android.media.metrics.EditingEndedEvent;
import android.media.metrics.NetworkEvent;
import android.media.metrics.PlaybackErrorEvent;
import android.media.metrics.PlaybackMetrics;
@@ -24,7 +25,7 @@
import android.os.PersistableBundle;
/**
- * Interface to the playback manager service.
+ * Interface to the media metrics manager service.
* @hide
*/
interface IMediaMetricsManager {
@@ -37,6 +38,8 @@
void reportPlaybackStateEvent(in String sessionId, in PlaybackStateEvent event, int userId);
void reportTrackChangeEvent(in String sessionId, in TrackChangeEvent event, int userId);
+ void reportEditingEndedEvent(in String sessionId, in EditingEndedEvent event, int userId);
+
String getTranscodingSessionId(int userId);
String getEditingSessionId(int userId);
String getBundleSessionId(int userId);
diff --git a/media/java/android/media/metrics/MediaMetricsManager.java b/media/java/android/media/metrics/MediaMetricsManager.java
index 0898874..622b0c1 100644
--- a/media/java/android/media/metrics/MediaMetricsManager.java
+++ b/media/java/android/media/metrics/MediaMetricsManager.java
@@ -193,4 +193,18 @@
throw e.rethrowFromSystemServer();
}
}
+
+ /**
+ * Reports the event of an editing session ending.
+ *
+ * @hide
+ */
+ public void reportEditingEndedEvent(
+ @NonNull String sessionId, EditingEndedEvent editingEndedEvent) {
+ try {
+ mService.reportEditingEndedEvent(sessionId, editingEndedEvent, mUserId);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
}
diff --git a/media/java/android/media/tv/BroadcastInfoRequest.java b/media/java/android/media/tv/BroadcastInfoRequest.java
index cbd8c1f..694756c 100644
--- a/media/java/android/media/tv/BroadcastInfoRequest.java
+++ b/media/java/android/media/tv/BroadcastInfoRequest.java
@@ -32,7 +32,8 @@
public abstract class BroadcastInfoRequest implements Parcelable {
/** @hide */
@Retention(RetentionPolicy.SOURCE)
- @IntDef({REQUEST_OPTION_REPEAT, REQUEST_OPTION_AUTO_UPDATE})
+ @IntDef({REQUEST_OPTION_REPEAT, REQUEST_OPTION_AUTO_UPDATE,
+ REQUEST_OPTION_ONEWAY, REQUEST_OPTION_ONESHOT})
public @interface RequestOption {}
/**
@@ -47,6 +48,18 @@
* first time, new values are detected.
*/
public static final int REQUEST_OPTION_AUTO_UPDATE = 1;
+ /**
+ * Request option: one-way
+ * <p> With this option, no response is expected after sending the request.
+ * @hide
+ */
+ public static final int REQUEST_OPTION_ONEWAY = 2;
+ /**
+ * Request option: one-shot
+ * <p> With this option, only one response will be given per request.
+ * @hide
+ */
+ public static final int REQUEST_OPTION_ONESHOT = 3;
public static final @NonNull Parcelable.Creator<BroadcastInfoRequest> CREATOR =
new Parcelable.Creator<BroadcastInfoRequest>() {
diff --git a/media/java/android/media/tv/ITvInputClient.aidl b/media/java/android/media/tv/ITvInputClient.aidl
index 0f8a00a..8978277 100644
--- a/media/java/android/media/tv/ITvInputClient.aidl
+++ b/media/java/android/media/tv/ITvInputClient.aidl
@@ -44,6 +44,7 @@
void onTrackSelected(int type, in String trackId, int seq);
void onVideoAvailable(int seq);
void onVideoUnavailable(int reason, int seq);
+ void onVideoFreezeUpdated(boolean isFrozen, int seq);
void onContentAllowed(int seq);
void onContentBlocked(in String rating, int seq);
void onLayoutSurface(int left, int top, int right, int bottom, int seq);
diff --git a/media/java/android/media/tv/ITvInputSessionCallback.aidl b/media/java/android/media/tv/ITvInputSessionCallback.aidl
index a52e9a5..8e2702a 100644
--- a/media/java/android/media/tv/ITvInputSessionCallback.aidl
+++ b/media/java/android/media/tv/ITvInputSessionCallback.aidl
@@ -41,6 +41,7 @@
void onTrackSelected(int type, in String trackId);
void onVideoAvailable();
void onVideoUnavailable(int reason);
+ void onVideoFreezeUpdated(boolean isFrozen);
void onContentAllowed();
void onContentBlocked(in String rating);
void onLayoutSurface(int left, int top, int right, int bottom);
diff --git a/media/java/android/media/tv/TvInputManager.java b/media/java/android/media/tv/TvInputManager.java
index 51b2542..2b31bfe 100644
--- a/media/java/android/media/tv/TvInputManager.java
+++ b/media/java/android/media/tv/TvInputManager.java
@@ -33,6 +33,7 @@
import android.media.AudioFormat.Encoding;
import android.media.AudioPresentation;
import android.media.PlaybackParams;
+import android.media.tv.ad.TvAdManager;
import android.media.tv.interactive.TvInteractiveAppManager;
import android.net.Uri;
import android.os.Binder;
@@ -740,6 +741,15 @@
}
/**
+ * This is called when the video freeze state has been updated.
+ * If {@code true}, the video is frozen on the last frame while audio playback continues.
+ * @param session A {@link TvInputManager.Session} associated with this callback.
+ * @param isFrozen Whether the video is frozen
+ */
+ public void onVideoFreezeUpdated(Session session, boolean isFrozen) {
+ }
+
+ /**
* This is called when the current program content turns out to be allowed to watch since
* its content rating is not blocked by parental controls.
*
@@ -1030,6 +1040,19 @@
});
}
+ void postVideoFreezeUpdated(boolean isFrozen) {
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ mSessionCallback.onVideoFreezeUpdated(mSession, isFrozen);
+ if (mSession.mIAppNotificationEnabled
+ && mSession.getInteractiveAppSession() != null) {
+ mSession.getInteractiveAppSession().notifyVideoFreezeUpdated(isFrozen);
+ }
+ }
+ });
+ }
+
void postContentAllowed() {
mHandler.post(new Runnable() {
@Override
@@ -1546,6 +1569,18 @@
}
@Override
+ public void onVideoFreezeUpdated(boolean isFrozen, int seq) {
+ synchronized (mSessionCallbackRecordMap) {
+ SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq);
+ if (record == null) {
+ Log.e(TAG, "Callback not found for seq " + seq);
+ return;
+ }
+ record.postVideoFreezeUpdated(isFrozen);
+ }
+ }
+
+ @Override
public void onContentAllowed(int seq) {
synchronized (mSessionCallbackRecordMap) {
SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq);
@@ -2710,6 +2745,7 @@
private int mVideoHeight;
private TvInteractiveAppManager.Session mIAppSession;
+ private TvAdManager.Session mAdSession;
private boolean mIAppNotificationEnabled = false;
private Session(IBinder token, InputChannel channel, ITvInputManager service, int userId,
@@ -2730,6 +2766,14 @@
this.mIAppSession = iAppSession;
}
+ public TvAdManager.Session getAdSession() {
+ return mAdSession;
+ }
+
+ public void setAdSession(TvAdManager.Session adSession) {
+ this.mAdSession = adSession;
+ }
+
/**
* Releases this session.
*/
diff --git a/media/java/android/media/tv/TvInputService.java b/media/java/android/media/tv/TvInputService.java
index 76d8e50..6301a27 100644
--- a/media/java/android/media/tv/TvInputService.java
+++ b/media/java/android/media/tv/TvInputService.java
@@ -763,6 +763,34 @@
}
/**
+ * Informs the application that the video freeze state has been updated.
+ *
+ * When {@code true}, the video is frozen on the last frame but audio playback remains
+ * active.
+ *
+ * @param isFrozen Whether or not the video is frozen
+ * @hide
+ */
+ public void notifyVideoFreezeUpdated(boolean isFrozen) {
+ executeOrPostRunnableOnMainThread(new Runnable() {
+ @MainThread
+ @Override
+ public void run() {
+ try {
+ if (DEBUG) {
+ Log.d(TAG, "notifyVideoFreezeUpdated");
+ }
+ if (mSessionCallback != null) {
+ mSessionCallback.onVideoFreezeUpdated(isFrozen);
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, "error in notifyVideoFreezeUpdated", e);
+ }
+ }
+ });
+ }
+
+ /**
* Sends an updated list of all audio presentations available from a Next Generation Audio
* service. This is used by the framework to maintain the audio presentation information for
* a given track of {@link TvTrackInfo#TYPE_AUDIO}, which in turn is used by
diff --git a/media/java/android/media/tv/ad/ITvAdManager.aidl b/media/java/android/media/tv/ad/ITvAdManager.aidl
index a747e49..9620065 100644
--- a/media/java/android/media/tv/ad/ITvAdManager.aidl
+++ b/media/java/android/media/tv/ad/ITvAdManager.aidl
@@ -16,9 +16,13 @@
package android.media.tv.ad;
+import android.graphics.Rect;
+import android.media.tv.TvTrackInfo;
import android.media.tv.ad.ITvAdClient;
import android.media.tv.ad.ITvAdManagerCallback;
import android.media.tv.ad.TvAdServiceInfo;
+import android.net.Uri;
+import android.os.Bundle;
import android.view.Surface;
/**
@@ -31,10 +35,27 @@
in ITvAdClient client, in String serviceId, in String type, int seq, int userId);
void releaseSession(in IBinder sessionToken, int userId);
void startAdService(in IBinder sessionToken, int userId);
+ void stopAdService(in IBinder sessionToken, int userId);
+ void resetAdService(in IBinder sessionToken, int userId);
void setSurface(in IBinder sessionToken, in Surface surface, int userId);
void dispatchSurfaceChanged(in IBinder sessionToken, int format, int width, int height,
int userId);
+ void sendCurrentVideoBounds(in IBinder sessionToken, in Rect bounds, int userId);
+ void sendCurrentChannelUri(in IBinder sessionToken, in Uri channelUri, int userId);
+ void sendTrackInfoList(in IBinder sessionToken, in List<TvTrackInfo> tracks, int userId);
+ void sendCurrentTvInputId(in IBinder sessionToken, in String inputId, int userId);
+ void sendSigningResult(in IBinder sessionToken, in String signingId, in byte[] result,
+ int userId);
+
+ void notifyError(in IBinder sessionToken, in String errMsg, in Bundle params, int userId);
+ void notifyTvMessage(in IBinder sessionToken, in int type, in Bundle data, int userId);
+
void registerCallback(in ITvAdManagerCallback callback, int userId);
void unregisterCallback(in ITvAdManagerCallback callback, int userId);
+
+ void createMediaView(in IBinder sessionToken, in IBinder windowToken, in Rect frame,
+ int userId);
+ void relayoutMediaView(in IBinder sessionToken, in Rect frame, int userId);
+ void removeMediaView(in IBinder sessionToken, int userId);
}
diff --git a/media/java/android/media/tv/ad/ITvAdSession.aidl b/media/java/android/media/tv/ad/ITvAdSession.aidl
index 751257c..69afb17 100644
--- a/media/java/android/media/tv/ad/ITvAdSession.aidl
+++ b/media/java/android/media/tv/ad/ITvAdSession.aidl
@@ -16,6 +16,10 @@
package android.media.tv.ad;
+import android.graphics.Rect;
+import android.media.tv.TvTrackInfo;
+import android.net.Uri;
+import android.os.Bundle;
import android.view.Surface;
/**
@@ -25,6 +29,21 @@
oneway interface ITvAdSession {
void release();
void startAdService();
+ void stopAdService();
+ void resetAdService();
void setSurface(in Surface surface);
void dispatchSurfaceChanged(int format, int width, int height);
+
+ void sendCurrentVideoBounds(in Rect bounds);
+ void sendCurrentChannelUri(in Uri channelUri);
+ void sendTrackInfoList(in List<TvTrackInfo> tracks);
+ void sendCurrentTvInputId(in String inputId);
+ void sendSigningResult(in String signingId, in byte[] result);
+
+ void notifyError(in String errMsg, in Bundle params);
+ void notifyTvMessage(int type, in Bundle data);
+
+ void createMediaView(in IBinder windowToken, in Rect frame);
+ void relayoutMediaView(in Rect frame);
+ void removeMediaView();
}
diff --git a/media/java/android/media/tv/ad/ITvAdSessionWrapper.java b/media/java/android/media/tv/ad/ITvAdSessionWrapper.java
index 4df2783..251351d 100644
--- a/media/java/android/media/tv/ad/ITvAdSessionWrapper.java
+++ b/media/java/android/media/tv/ad/ITvAdSessionWrapper.java
@@ -16,7 +16,14 @@
package android.media.tv.ad;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.content.Context;
+import android.graphics.Rect;
+import android.media.tv.TvTrackInfo;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.IBinder;
import android.os.Looper;
import android.os.Message;
import android.os.RemoteException;
@@ -29,6 +36,8 @@
import com.android.internal.os.HandlerCaller;
import com.android.internal.os.SomeArgs;
+import java.util.List;
+
/**
* Implements the internal ITvAdSession interface.
* @hide
@@ -43,6 +52,19 @@
private static final int DO_RELEASE = 1;
private static final int DO_SET_SURFACE = 2;
private static final int DO_DISPATCH_SURFACE_CHANGED = 3;
+ private static final int DO_CREATE_MEDIA_VIEW = 4;
+ private static final int DO_RELAYOUT_MEDIA_VIEW = 5;
+ private static final int DO_REMOVE_MEDIA_VIEW = 6;
+ private static final int DO_START_AD_SERVICE = 7;
+ private static final int DO_STOP_AD_SERVICE = 8;
+ private static final int DO_RESET_AD_SERVICE = 9;
+ private static final int DO_SEND_CURRENT_VIDEO_BOUNDS = 10;
+ private static final int DO_SEND_CURRENT_CHANNEL_URI = 11;
+ private static final int DO_SEND_TRACK_INFO_LIST = 12;
+ private static final int DO_SEND_CURRENT_TV_INPUT_ID = 13;
+ private static final int DO_SEND_SIGNING_RESULT = 14;
+ private static final int DO_NOTIFY_ERROR = 15;
+ private static final int DO_NOTIFY_TV_MESSAGE = 16;
private final HandlerCaller mCaller;
private TvAdService.Session mSessionImpl;
@@ -61,6 +83,7 @@
@Override
public void release() {
+ mSessionImpl.scheduleMediaViewCleanup();
mCaller.executeOrSendMessage(mCaller.obtainMessage(DO_RELEASE));
}
@@ -97,6 +120,66 @@
args.recycle();
break;
}
+ case DO_CREATE_MEDIA_VIEW: {
+ SomeArgs args = (SomeArgs) msg.obj;
+ mSessionImpl.createMediaView((IBinder) args.arg1, (Rect) args.arg2);
+ args.recycle();
+ break;
+ }
+ case DO_RELAYOUT_MEDIA_VIEW: {
+ mSessionImpl.relayoutMediaView((Rect) msg.obj);
+ break;
+ }
+ case DO_REMOVE_MEDIA_VIEW: {
+ mSessionImpl.removeMediaView(true);
+ break;
+ }
+ case DO_START_AD_SERVICE: {
+ mSessionImpl.startAdService();
+ break;
+ }
+ case DO_STOP_AD_SERVICE: {
+ mSessionImpl.stopAdService();
+ break;
+ }
+ case DO_RESET_AD_SERVICE: {
+ mSessionImpl.resetAdService();
+ break;
+ }
+ case DO_SEND_CURRENT_VIDEO_BOUNDS: {
+ mSessionImpl.sendCurrentVideoBounds((Rect) msg.obj);
+ break;
+ }
+ case DO_SEND_CURRENT_CHANNEL_URI: {
+ mSessionImpl.sendCurrentChannelUri((Uri) msg.obj);
+ break;
+ }
+ case DO_SEND_TRACK_INFO_LIST: {
+ mSessionImpl.sendTrackInfoList((List<TvTrackInfo>) msg.obj);
+ break;
+ }
+ case DO_SEND_CURRENT_TV_INPUT_ID: {
+ mSessionImpl.sendCurrentTvInputId((String) msg.obj);
+ break;
+ }
+ case DO_SEND_SIGNING_RESULT: {
+ SomeArgs args = (SomeArgs) msg.obj;
+ mSessionImpl.sendSigningResult((String) args.arg1, (byte[]) args.arg2);
+ args.recycle();
+ break;
+ }
+ case DO_NOTIFY_ERROR: {
+ SomeArgs args = (SomeArgs) msg.obj;
+ mSessionImpl.notifyError((String) args.arg1, (Bundle) args.arg2);
+ args.recycle();
+ break;
+ }
+ case DO_NOTIFY_TV_MESSAGE: {
+ SomeArgs args = (SomeArgs) msg.obj;
+ mSessionImpl.notifyTvMessage((Integer) args.arg1, (Bundle) args.arg2);
+ args.recycle();
+ break;
+ }
default: {
Log.w(TAG, "Unhandled message code: " + msg.what);
break;
@@ -115,7 +198,17 @@
@Override
public void startAdService() throws RemoteException {
+ mCaller.executeOrSendMessage(mCaller.obtainMessage(DO_START_AD_SERVICE));
+ }
+ @Override
+ public void stopAdService() {
+ mCaller.executeOrSendMessage(mCaller.obtainMessage(DO_STOP_AD_SERVICE));
+ }
+
+ @Override
+ public void resetAdService() {
+ mCaller.executeOrSendMessage(mCaller.obtainMessage(DO_RESET_AD_SERVICE));
}
@Override
@@ -129,6 +222,64 @@
mCaller.obtainMessageIIII(DO_DISPATCH_SURFACE_CHANGED, format, width, height, 0));
}
+ @Override
+ public void sendCurrentVideoBounds(@Nullable Rect bounds) {
+ mCaller.executeOrSendMessage(
+ mCaller.obtainMessageO(DO_SEND_CURRENT_VIDEO_BOUNDS, bounds));
+ }
+
+ @Override
+ public void sendCurrentChannelUri(@Nullable Uri channelUri) {
+ mCaller.executeOrSendMessage(
+ mCaller.obtainMessageO(DO_SEND_CURRENT_CHANNEL_URI, channelUri));
+ }
+
+ @Override
+ public void sendTrackInfoList(@NonNull List<TvTrackInfo> tracks) {
+ mCaller.executeOrSendMessage(
+ mCaller.obtainMessageO(DO_SEND_TRACK_INFO_LIST, tracks));
+ }
+
+ @Override
+ public void sendCurrentTvInputId(@Nullable String inputId) {
+ mCaller.executeOrSendMessage(
+ mCaller.obtainMessageO(DO_SEND_CURRENT_TV_INPUT_ID, inputId));
+ }
+
+ @Override
+ public void sendSigningResult(@NonNull String signingId, @NonNull byte[] result) {
+ mCaller.executeOrSendMessage(
+ mCaller.obtainMessageOO(DO_SEND_SIGNING_RESULT, signingId, result));
+ }
+
+ @Override
+ public void notifyError(@NonNull String errMsg, @NonNull Bundle params) {
+ mCaller.executeOrSendMessage(
+ mCaller.obtainMessageOO(DO_NOTIFY_ERROR, errMsg, params));
+ }
+
+ @Override
+ public void notifyTvMessage(int type, Bundle data) {
+ mCaller.executeOrSendMessage(
+ mCaller.obtainMessageOO(DO_NOTIFY_TV_MESSAGE, type, data));
+ }
+
+ @Override
+ public void createMediaView(IBinder windowToken, Rect frame) {
+ mCaller.executeOrSendMessage(
+ mCaller.obtainMessageOO(DO_CREATE_MEDIA_VIEW, windowToken, frame));
+ }
+
+ @Override
+ public void relayoutMediaView(Rect frame) {
+ mCaller.executeOrSendMessage(mCaller.obtainMessageO(DO_RELAYOUT_MEDIA_VIEW, frame));
+ }
+
+ @Override
+ public void removeMediaView() {
+ mCaller.executeOrSendMessage(mCaller.obtainMessage(DO_REMOVE_MEDIA_VIEW));
+ }
+
private final class TvAdEventReceiver extends InputEventReceiver {
TvAdEventReceiver(InputChannel inputChannel, Looper looper) {
super(inputChannel, looper);
diff --git a/media/java/android/media/tv/ad/TvAdManager.java b/media/java/android/media/tv/ad/TvAdManager.java
index 9c75051..4dce72f 100644
--- a/media/java/android/media/tv/ad/TvAdManager.java
+++ b/media/java/android/media/tv/ad/TvAdManager.java
@@ -21,8 +21,12 @@
import android.annotation.Nullable;
import android.annotation.SystemService;
import android.content.Context;
+import android.graphics.Rect;
import android.media.tv.TvInputManager;
+import android.media.tv.TvTrackInfo;
import android.media.tv.flags.Flags;
+import android.net.Uri;
+import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
@@ -35,6 +39,7 @@
import android.view.InputEvent;
import android.view.InputEventSender;
import android.view.Surface;
+import android.view.View;
import com.android.internal.util.Preconditions;
@@ -250,6 +255,14 @@
mSessionCallbackRecordMap = sessionCallbackRecordMap;
}
+ public TvInputManager.Session getInputSession() {
+ return mInputSession;
+ }
+
+ public void setInputSession(TvInputManager.Session inputSession) {
+ mInputSession = inputSession;
+ }
+
/**
* Releases this session.
*/
@@ -286,6 +299,67 @@
}
/**
+ * Creates a media view. Once the media view is created, {@link #relayoutMediaView}
+ * should be called whenever the layout of its containing view is changed.
+ * {@link #removeMediaView()} should be called to remove the media view.
+ * Since a session can have only one media view, this method should be called only once
+ * or it can be called again after calling {@link #removeMediaView()}.
+ *
+ * @param view A view for AD service.
+ * @param frame A position of the media view.
+ * @throws IllegalStateException if {@code view} is not attached to a window.
+ */
+ void createMediaView(@NonNull View view, @NonNull Rect frame) {
+ Preconditions.checkNotNull(view);
+ Preconditions.checkNotNull(frame);
+ if (view.getWindowToken() == null) {
+ throw new IllegalStateException("view must be attached to a window");
+ }
+ if (mToken == null) {
+ Log.w(TAG, "The session has been already released");
+ return;
+ }
+ try {
+ mService.createMediaView(mToken, view.getWindowToken(), frame, mUserId);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Relayouts the current media view.
+ *
+ * @param frame A new position of the media view.
+ */
+ void relayoutMediaView(@NonNull Rect frame) {
+ Preconditions.checkNotNull(frame);
+ if (mToken == null) {
+ Log.w(TAG, "The session has been already released");
+ return;
+ }
+ try {
+ mService.relayoutMediaView(mToken, frame, mUserId);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Removes the current media view.
+ */
+ void removeMediaView() {
+ if (mToken == null) {
+ Log.w(TAG, "The session has been already released");
+ return;
+ }
+ try {
+ mService.removeMediaView(mToken, mUserId);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
* Notifies of any structural changes (format or size) of the surface passed in
* {@link #setSurface}.
*
@@ -348,6 +422,117 @@
}
}
+ void stopAdService() {
+ if (mToken == null) {
+ Log.w(TAG, "The session has been already released");
+ return;
+ }
+ try {
+ mService.stopAdService(mToken, mUserId);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ void resetAdService() {
+ if (mToken == null) {
+ Log.w(TAG, "The session has been already released");
+ return;
+ }
+ try {
+ mService.resetAdService(mToken, mUserId);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ void sendCurrentVideoBounds(@NonNull Rect bounds) {
+ if (mToken == null) {
+ Log.w(TAG, "The session has been already released");
+ return;
+ }
+ try {
+ mService.sendCurrentVideoBounds(mToken, bounds, mUserId);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ void sendCurrentChannelUri(@Nullable Uri channelUri) {
+ if (mToken == null) {
+ Log.w(TAG, "The session has been already released");
+ return;
+ }
+ try {
+ mService.sendCurrentChannelUri(mToken, channelUri, mUserId);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ void sendTrackInfoList(@NonNull List<TvTrackInfo> tracks) {
+ if (mToken == null) {
+ Log.w(TAG, "The session has been already released");
+ return;
+ }
+ try {
+ mService.sendTrackInfoList(mToken, tracks, mUserId);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ void sendCurrentTvInputId(@Nullable String inputId) {
+ if (mToken == null) {
+ Log.w(TAG, "The session has been already released");
+ return;
+ }
+ try {
+ mService.sendCurrentTvInputId(mToken, inputId, mUserId);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ void sendSigningResult(@NonNull String signingId, @NonNull byte[] result) {
+ if (mToken == null) {
+ Log.w(TAG, "The session has been already released");
+ return;
+ }
+ try {
+ mService.sendSigningResult(mToken, signingId, result, mUserId);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ void notifyError(@NonNull String errMsg, @NonNull Bundle params) {
+ if (mToken == null) {
+ Log.w(TAG, "The session has been already released");
+ return;
+ }
+ try {
+ mService.notifyError(mToken, errMsg, params, mUserId);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Notifies AD service session when a new TV message is received.
+ */
+ public void notifyTvMessage(int type, Bundle data) {
+ if (mToken == null) {
+ Log.w(TAG, "The session has been already released");
+ return;
+ }
+ try {
+ mService.notifyTvMessage(mToken, type, data, mUserId);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
private final class InputEventHandler extends Handler {
public static final int MSG_SEND_INPUT_EVENT = 1;
public static final int MSG_TIMEOUT_INPUT_EVENT = 2;
diff --git a/media/java/android/media/tv/ad/TvAdService.java b/media/java/android/media/tv/ad/TvAdService.java
index 6995703..5d81837 100644
--- a/media/java/android/media/tv/ad/TvAdService.java
+++ b/media/java/android/media/tv/ad/TvAdService.java
@@ -20,19 +20,28 @@
import android.annotation.MainThread;
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.annotation.Px;
import android.annotation.SdkConstant;
import android.annotation.SuppressLint;
+import android.app.ActivityManager;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.graphics.PixelFormat;
+import android.graphics.Rect;
+import android.media.tv.TvInputManager;
+import android.media.tv.TvTrackInfo;
+import android.net.Uri;
+import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
+import android.os.Process;
import android.os.RemoteCallbackList;
import android.os.RemoteException;
import android.util.Log;
+import android.view.Gravity;
import android.view.InputChannel;
import android.view.InputDevice;
import android.view.InputEvent;
@@ -42,6 +51,7 @@
import android.view.Surface;
import android.view.View;
import android.view.WindowManager;
+import android.widget.FrameLayout;
import com.android.internal.os.SomeArgs;
@@ -56,6 +66,8 @@
private static final boolean DEBUG = false;
private static final String TAG = "TvAdService";
+ private static final int DETACH_MEDIA_VIEW_TIMEOUT_MS = 5000;
+
/**
* Name under which a TvAdService component publishes information about itself. This meta-data
* must reference an XML resource containing an
@@ -151,7 +163,14 @@
private final Context mContext;
final Handler mHandler;
private final WindowManager mWindowManager;
+ private WindowManager.LayoutParams mWindowParams;
private Surface mSurface;
+ private FrameLayout mMediaViewContainer;
+ private View mMediaView;
+ private MediaViewCleanUpTask mMediaViewCleanUpTask;
+ private boolean mMediaViewEnabled;
+ private IBinder mWindowToken;
+ private Rect mMediaFrame;
/**
@@ -166,6 +185,48 @@
}
/**
+ * Enables or disables the media view.
+ *
+ * <p>By default, the media view is disabled. Must be called explicitly after the
+ * session is created to enable the media view.
+ *
+ * <p>The TV AD service can disable its media view when needed.
+ *
+ * @param enable {@code true} if you want to enable the media view. {@code false}
+ * otherwise.
+ * @hide
+ */
+ @CallSuper
+ public void setMediaViewEnabled(final boolean enable) {
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ if (enable == mMediaViewEnabled) {
+ return;
+ }
+ mMediaViewEnabled = enable;
+ if (enable) {
+ if (mWindowToken != null) {
+ createMediaView(mWindowToken, mMediaFrame);
+ }
+ } else {
+ removeMediaView(false);
+ }
+ }
+ });
+ }
+
+ /**
+ * Returns {@code true} if media view is enabled, {@code false} otherwise.
+ *
+ * @see #setMediaViewEnabled(boolean)
+ * @hide
+ */
+ public boolean isMediaViewEnabled() {
+ return mMediaViewEnabled;
+ }
+
+ /**
* Releases TvAdService session.
*/
public abstract void onRelease();
@@ -180,18 +241,44 @@
mSessionCallback = null;
mPendingActions.clear();
}
+ // Removes the media view lastly so that any hanging on the main thread can be handled
+ // in {@link #scheduleMediaViewCleanup}.
+ removeMediaView(true);
}
/**
* Starts TvAdService session.
+ * @hide
*/
public void onStartAdService() {
}
+ /**
+ * Stops TvAdService session.
+ * @hide
+ */
+ public void onStopAdService() {
+ }
+
+ /**
+ * Resets TvAdService session.
+ * @hide
+ */
+ public void onResetAdService() {
+ }
+
void startAdService() {
onStartAdService();
}
+ void stopAdService() {
+ onStopAdService();
+ }
+
+ void resetAdService() {
+ onResetAdService();
+ }
+
@Override
public boolean onKeyDown(int keyCode, @NonNull KeyEvent event) {
return false;
@@ -307,6 +394,109 @@
}
/**
+ * Receives current video bounds.
+ *
+ * @param bounds the rectangle area for rendering the current video.
+ * @hide
+ */
+ public void onCurrentVideoBounds(@NonNull Rect bounds) {
+ }
+
+ /**
+ * Receives current channel URI.
+ * @hide
+ */
+ public void onCurrentChannelUri(@Nullable Uri channelUri) {
+ }
+
+ /**
+ * Receives track list.
+ * @hide
+ */
+ public void onTrackInfoList(@NonNull List<TvTrackInfo> tracks) {
+ }
+
+ /**
+ * Receives current TV input ID.
+ * @hide
+ */
+ public void onCurrentTvInputId(@Nullable String inputId) {
+ }
+
+ /**
+ * Receives signing result.
+ *
+ * @param signingId the ID to identify the request. It's the same as the corresponding ID in
+ * {@link Session#requestSigning(String, String, String, byte[])}
+ * @param result the signed result.
+ *
+ * @see #requestSigning(String, String, String, byte[])
+ * @hide
+ */
+ public void onSigningResult(@NonNull String signingId, @NonNull byte[] result) {
+ }
+
+ /**
+ * Called when the application sends information of an error.
+ *
+ * @param errMsg the message of the error.
+ * @param params additional parameters of the error. For example, the signingId of {@link
+ * TvAdView.TvAdCallback#onRequestSigning(String, String, String, String, byte[])}
+ * can be included to identify the related signing request, and the method name
+ * "onRequestSigning" can also be added to the params.
+ *
+ * @see TvAdView#ERROR_KEY_METHOD_NAME
+ * @hide
+ */
+ public void onError(@NonNull String errMsg, @NonNull Bundle params) {
+ }
+
+ /**
+ * Called when a TV message is received
+ *
+ * @param type The type of message received, such as
+ * {@link TvInputManager#TV_MESSAGE_TYPE_WATERMARK}
+ * @param data The raw data of the message. The bundle keys are:
+ * {@link TvInputManager#TV_MESSAGE_KEY_STREAM_ID},
+ * {@link TvInputManager#TV_MESSAGE_KEY_GROUP_ID},
+ * {@link TvInputManager#TV_MESSAGE_KEY_SUBTYPE},
+ * {@link TvInputManager#TV_MESSAGE_KEY_RAW_DATA}.
+ * See {@link TvInputManager#TV_MESSAGE_KEY_SUBTYPE} for more information on
+ * how to parse this data.
+ * @hide
+ */
+ public void onTvMessage(@TvInputManager.TvMessageType int type,
+ @NonNull Bundle data) {
+ }
+
+ /**
+ * Called when the size of the media view is changed by the application.
+ *
+ * <p>This is always called at least once when the session is created regardless of whether
+ * the media view is enabled or not. The media view container size is the same as the
+ * containing {@link TvAdView}. Note that the size of the underlying surface can
+ * be different if the surface was changed by calling {@link #layoutSurface}.
+ *
+ * @param width The width of the media view, in pixels.
+ * @param height The height of the media view, in pixels.
+ * @hide
+ */
+ public void onMediaViewSizeChanged(@Px int width, @Px int height) {
+ }
+
+ /**
+ * Called when the application requests to create a media view. Each session
+ * implementation can override this method and return its own view.
+ *
+ * @return a view attached to the media window. {@code null} if no media view is created.
+ * @hide
+ */
+ @Nullable
+ public View onCreateMediaView() {
+ return null;
+ }
+
+ /**
* Takes care of dispatching incoming input events and tells whether the event was handled.
*/
int dispatchInputEvent(InputEvent event, InputEventReceiver receiver) {
@@ -373,6 +563,37 @@
onSurfaceChanged(format, width, height);
}
+ void sendCurrentVideoBounds(@NonNull Rect bounds) {
+ onCurrentVideoBounds(bounds);
+ }
+
+ void sendCurrentChannelUri(@Nullable Uri channelUri) {
+ onCurrentChannelUri(channelUri);
+ }
+
+ void sendTrackInfoList(@NonNull List<TvTrackInfo> tracks) {
+ onTrackInfoList(tracks);
+ }
+
+ void sendCurrentTvInputId(@Nullable String inputId) {
+ onCurrentTvInputId(inputId);
+ }
+
+ void sendSigningResult(String signingId, byte[] result) {
+ onSigningResult(signingId, result);
+ }
+
+ void notifyError(String errMsg, Bundle params) {
+ onError(errMsg, params);
+ }
+
+ void notifyTvMessage(int type, Bundle data) {
+ if (DEBUG) {
+ Log.d(TAG, "notifyTvMessage (type=" + type + ", data= " + data + ")");
+ }
+ onTvMessage(type, data);
+ }
+
private void executeOrPostRunnableOnMainThread(Runnable action) {
synchronized (mLock) {
if (mSessionCallback == null) {
@@ -388,6 +609,137 @@
}
}
}
+
+ /**
+ * Creates a media view. This calls {@link #onCreateMediaView} to get a view to attach
+ * to the media window.
+ *
+ * @param windowToken A window token of the application.
+ * @param frame A position of the media view.
+ */
+ void createMediaView(IBinder windowToken, Rect frame) {
+ if (mMediaViewContainer != null) {
+ removeMediaView(false);
+ }
+ if (DEBUG) Log.d(TAG, "create media view(" + frame + ")");
+ mWindowToken = windowToken;
+ mMediaFrame = frame;
+ onMediaViewSizeChanged(frame.right - frame.left, frame.bottom - frame.top);
+ if (!mMediaViewEnabled) {
+ return;
+ }
+ mMediaView = onCreateMediaView();
+ if (mMediaView == null) {
+ return;
+ }
+ if (mMediaViewCleanUpTask != null) {
+ mMediaViewCleanUpTask.cancel(true);
+ mMediaViewCleanUpTask = null;
+ }
+ // Creates a container view to check hanging on the media view detaching.
+ // Adding/removing the media view to/from the container make the view attach/detach
+ // logic run on the main thread.
+ mMediaViewContainer = new FrameLayout(mContext.getApplicationContext());
+ mMediaViewContainer.addView(mMediaView);
+
+ int type = WindowManager.LayoutParams.TYPE_APPLICATION_MEDIA;
+ // We make the overlay view non-focusable and non-touchable so that
+ // the application that owns the window token can decide whether to consume or
+ // dispatch the input events.
+ int flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
+ | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE
+ | WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS;
+ if (ActivityManager.isHighEndGfx()) {
+ flags |= WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED;
+ }
+ mWindowParams = new WindowManager.LayoutParams(
+ frame.right - frame.left, frame.bottom - frame.top,
+ frame.left, frame.top, type, flags, PixelFormat.TRANSPARENT);
+ mWindowParams.privateFlags |=
+ WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMATION;
+ mWindowParams.gravity = Gravity.START | Gravity.TOP;
+ mWindowParams.token = windowToken;
+ mWindowManager.addView(mMediaViewContainer, mWindowParams);
+ }
+
+ /**
+ * Relayouts the current media view.
+ *
+ * @param frame A new position of the media view.
+ */
+ void relayoutMediaView(Rect frame) {
+ if (DEBUG) Log.d(TAG, "relayoutMediaView(" + frame + ")");
+ if (mMediaFrame == null || mMediaFrame.width() != frame.width()
+ || mMediaFrame.height() != frame.height()) {
+ // Note: relayoutMediaView is called whenever TvAdView's layout is
+ // changed regardless of setMediaViewEnabled.
+ onMediaViewSizeChanged(frame.right - frame.left, frame.bottom - frame.top);
+ }
+ mMediaFrame = frame;
+ if (!mMediaViewEnabled || mMediaViewContainer == null) {
+ return;
+ }
+ mWindowParams.x = frame.left;
+ mWindowParams.y = frame.top;
+ mWindowParams.width = frame.right - frame.left;
+ mWindowParams.height = frame.bottom - frame.top;
+ mWindowManager.updateViewLayout(mMediaViewContainer, mWindowParams);
+ }
+
+ /**
+ * Removes the current media view.
+ */
+ void removeMediaView(boolean clearWindowToken) {
+ if (DEBUG) Log.d(TAG, "removeMediaView(" + mMediaViewContainer + ")");
+ if (clearWindowToken) {
+ mWindowToken = null;
+ mMediaFrame = null;
+ }
+ if (mMediaViewContainer != null) {
+ // Removes the media view from the view hierarchy in advance so that it can be
+ // cleaned up in the {@link MediaViewCleanUpTask} if the remove process is
+ // hanging.
+ mMediaViewContainer.removeView(mMediaView);
+ mMediaView = null;
+ mWindowManager.removeView(mMediaViewContainer);
+ mMediaViewContainer = null;
+ mWindowParams = null;
+ }
+ }
+
+ /**
+ * Schedules a task which checks whether the media view is detached and kills the process
+ * if it is not. Note that this method is expected to be called in a non-main thread.
+ */
+ void scheduleMediaViewCleanup() {
+ View mediaViewParent = mMediaViewContainer;
+ if (mediaViewParent != null) {
+ mMediaViewCleanUpTask = new MediaViewCleanUpTask();
+ mMediaViewCleanUpTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR,
+ mediaViewParent);
+ }
+ }
+ }
+
+ private static final class MediaViewCleanUpTask extends AsyncTask<View, Void, Void> {
+ @Override
+ protected Void doInBackground(View... views) {
+ View mediaViewParent = views[0];
+ try {
+ Thread.sleep(DETACH_MEDIA_VIEW_TIMEOUT_MS);
+ } catch (InterruptedException e) {
+ return null;
+ }
+ if (isCancelled()) {
+ return null;
+ }
+ if (mediaViewParent.isAttachedToWindow()) {
+ Log.e(TAG, "Time out on releasing media view. Killing "
+ + mediaViewParent.getContext().getPackageName());
+ android.os.Process.killProcess(Process.myPid());
+ }
+ return null;
+ }
}
diff --git a/media/java/android/media/tv/ad/TvAdView.java b/media/java/android/media/tv/ad/TvAdView.java
index 5e67fe9..ec23b7c 100644
--- a/media/java/android/media/tv/ad/TvAdView.java
+++ b/media/java/android/media/tv/ad/TvAdView.java
@@ -16,12 +16,20 @@
package android.media.tv.ad;
+import android.annotation.CallbackExecutor;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
import android.content.res.Resources;
import android.content.res.XmlResourceParser;
import android.graphics.PixelFormat;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.media.tv.TvInputManager;
+import android.media.tv.TvTrackInfo;
+import android.media.tv.TvView;
+import android.net.Uri;
+import android.os.Bundle;
import android.os.Handler;
import android.util.AttributeSet;
import android.util.Log;
@@ -32,6 +40,9 @@
import android.view.View;
import android.view.ViewGroup;
+import java.util.List;
+import java.util.concurrent.Executor;
+
/**
* Displays contents of TV AD services.
* @hide
@@ -40,11 +51,22 @@
private static final String TAG = "TvAdView";
private static final boolean DEBUG = false;
+ /**
+ * The name of the method where the error happened, if applicable. For example, if there is an
+ * error during signing, the request name is "onRequestSigning".
+ * @see #notifyError(String, Bundle)
+ * @hide
+ */
+ public static final String ERROR_KEY_METHOD_NAME = "method_name";
+
private final TvAdManager mTvAdManager;
private final Handler mHandler = new Handler();
+ private final Object mCallbackLock = new Object();
private TvAdManager.Session mSession;
private MySessionCallback mSessionCallback;
+ private TvAdCallback mCallback;
+ private Executor mCallbackExecutor;
private final AttributeSet mAttrs;
private final int mDefStyleAttr;
@@ -64,6 +86,9 @@
private int mSurfaceViewTop;
private int mSurfaceViewBottom;
+ private boolean mMediaViewCreated;
+ private Rect mMediaViewFrame;
+
private final SurfaceHolder.Callback mSurfaceHolderCallback = new SurfaceHolder.Callback() {
@@ -121,6 +146,51 @@
mTvAdManager = (TvAdManager) getContext().getSystemService(Context.TV_AD_SERVICE);
}
+ /**
+ * Sets the TvAdView to receive events from TvInputService. This method links the session of
+ * TvAdManager to TvInputManager session, so the TvAdService can get the TvInputService events.
+ *
+ * @param tvView the TvView to be linked to this TvAdView via linking of Sessions. {@code null}
+ * to unlink the TvView.
+ * @return {@code true} if it's linked successfully; {@code false} otherwise.
+ * @hide
+ */
+ public boolean setTvView(@Nullable TvView tvView) {
+ if (tvView == null) {
+ return unsetTvView();
+ }
+ TvInputManager.Session inputSession = tvView.getInputSession();
+ if (inputSession == null || mSession == null) {
+ return false;
+ }
+ mSession.setInputSession(inputSession);
+ inputSession.setAdSession(mSession);
+ return true;
+ }
+
+ private boolean unsetTvView() {
+ if (mSession == null || mSession.getInputSession() == null) {
+ return false;
+ }
+ mSession.getInputSession().setAdSession(null);
+ mSession.setInputSession(null);
+ return true;
+ }
+
+ /** @hide */
+ @Override
+ public void onAttachedToWindow() {
+ super.onAttachedToWindow();
+ createSessionMediaView();
+ }
+
+ /** @hide */
+ @Override
+ public void onDetachedFromWindow() {
+ removeSessionMediaView();
+ super.onDetachedFromWindow();
+ }
+
@Override
public void onLayout(boolean changed, int left, int top, int right, int bottom) {
if (DEBUG) {
@@ -150,6 +220,11 @@
public void onVisibilityChanged(@NonNull View changedView, int visibility) {
super.onVisibilityChanged(changedView, visibility);
mSurfaceView.setVisibility(visibility);
+ if (visibility == View.VISIBLE) {
+ createSessionMediaView();
+ } else {
+ removeSessionMediaView();
+ }
}
private void resetSurfaceView() {
@@ -162,6 +237,7 @@
@Override
protected void updateSurface() {
super.updateSurface();
+ relayoutSessionMediaView();
}};
// The surface view's content should be treated as secure all the time.
mSurfaceView.setSecure(true);
@@ -174,6 +250,69 @@
addView(mSurfaceView);
}
+ /**
+ * Resets this TvAdView to release its resources.
+ *
+ * <p>It can be reused by call {@link #prepareAdService(String, String)}.
+ * @hide
+ */
+ public void reset() {
+ if (DEBUG) Log.d(TAG, "reset()");
+ resetInternal();
+ }
+
+ private void resetInternal() {
+ mSessionCallback = null;
+ if (mSession != null) {
+ setSessionSurface(null);
+ removeSessionMediaView();
+ mUseRequestedSurfaceLayout = false;
+ mSession.release();
+ mSession = null;
+ resetSurfaceView();
+ }
+ }
+
+ private void createSessionMediaView() {
+ // TODO: handle z-order
+ if (mSession == null || !isAttachedToWindow() || mMediaViewCreated) {
+ return;
+ }
+ mMediaViewFrame = getViewFrameOnScreen();
+ mSession.createMediaView(this, mMediaViewFrame);
+ mMediaViewCreated = true;
+ }
+
+ private void removeSessionMediaView() {
+ if (mSession == null || !mMediaViewCreated) {
+ return;
+ }
+ mSession.removeMediaView();
+ mMediaViewCreated = false;
+ mMediaViewFrame = null;
+ }
+
+ private void relayoutSessionMediaView() {
+ if (mSession == null || !isAttachedToWindow() || !mMediaViewCreated) {
+ return;
+ }
+ Rect viewFrame = getViewFrameOnScreen();
+ if (viewFrame.equals(mMediaViewFrame)) {
+ return;
+ }
+ mSession.relayoutMediaView(viewFrame);
+ mMediaViewFrame = viewFrame;
+ }
+
+ private Rect getViewFrameOnScreen() {
+ Rect frame = new Rect();
+ getGlobalVisibleRect(frame);
+ RectF frameF = new RectF(frame);
+ getMatrix().mapRect(frameF);
+ frameF.round(frame);
+ return frame;
+ }
+
private void setSessionSurface(Surface surface) {
if (mSession == null) {
return;
@@ -185,7 +324,7 @@
if (mSession == null) {
return;
}
- //mSession.dispatchSurfaceChanged(format, width, height);
+ mSession.dispatchSurfaceChanged(format, width, height);
}
/**
@@ -205,16 +344,196 @@
/**
* Starts the AD service.
+ * @hide
*/
public void startAdService() {
if (DEBUG) {
- Log.d(TAG, "start");
+ Log.d(TAG, "startAdService");
}
if (mSession != null) {
mSession.startAdService();
}
}
+ /**
+ * Stops the AD service.
+ */
+ public void stopAdService() {
+ if (DEBUG) {
+ Log.d(TAG, "stopAdService");
+ }
+ if (mSession != null) {
+ mSession.stopAdService();
+ }
+ }
+
+ /**
+ * Resets the AD service.
+ *
+ * <p>This releases the resources of the corresponding {@link TvAdService.Session}.
+ */
+ public void resetAdService() {
+ if (DEBUG) {
+ Log.d(TAG, "resetAdService");
+ }
+ if (mSession != null) {
+ mSession.resetAdService();
+ }
+ }
+
+ /**
+ * Sends current video bounds to related TV AD service.
+ *
+ * @param bounds the rectangle area for rendering the current video.
+ */
+ public void sendCurrentVideoBounds(@NonNull Rect bounds) {
+ if (DEBUG) {
+ Log.d(TAG, "sendCurrentVideoBounds");
+ }
+ if (mSession != null) {
+ mSession.sendCurrentVideoBounds(bounds);
+ }
+ }
+
+ /**
+ * Sends current channel URI to related TV AD service.
+ *
+ * @param channelUri The current channel URI; {@code null} if there is no currently tuned
+ * channel.
+ */
+ public void sendCurrentChannelUri(@Nullable Uri channelUri) {
+ if (DEBUG) {
+ Log.d(TAG, "sendCurrentChannelUri");
+ }
+ if (mSession != null) {
+ mSession.sendCurrentChannelUri(channelUri);
+ }
+ }
+
+ /**
+ * Sends track info list to related TV AD service.
+ */
+ public void sendTrackInfoList(@Nullable List<TvTrackInfo> tracks) {
+ if (DEBUG) {
+ Log.d(TAG, "sendTrackInfoList");
+ }
+ if (mSession != null) {
+ mSession.sendTrackInfoList(tracks);
+ }
+ }
+
+ /**
+ * Sends current TV input ID to related TV AD service.
+ *
+ * @param inputId The current TV input ID whose channel is tuned. {@code null} if no channel is
+ * tuned.
+ * @see android.media.tv.TvInputInfo
+ */
+ public void sendCurrentTvInputId(@Nullable String inputId) {
+ if (DEBUG) {
+ Log.d(TAG, "sendCurrentTvInputId");
+ }
+ if (mSession != null) {
+ mSession.sendCurrentTvInputId(inputId);
+ }
+ }
+
+ /**
+ * Sends signing result to related TV AD service.
+ *
+ * <p>This is used when the corresponding server of the ADs requires signing during handshaking,
+ * and the AD service doesn't have the built-in private key. The private key is provided by the
+ * content providers and pre-built in the related app, such as TV app.
+ *
+ * @param signingId the ID to identify the request. It's the same as the corresponding ID in
+ * {@link TvAdService.Session#requestSigning(String, String, String, byte[])}
+ * @param result the signed result.
+ * @hide
+ */
+ public void sendSigningResult(@NonNull String signingId, @NonNull byte[] result) {
+ if (DEBUG) {
+ Log.d(TAG, "sendSigningResult");
+ }
+ if (mSession != null) {
+ mSession.sendSigningResult(signingId, result);
+ }
+ }
+
+ /**
+ * Notifies the corresponding {@link TvAdService} when there is an error.
+ *
+ * @param errMsg the message of the error.
+ * @param params additional parameters of the error. For example, the signingId of {@link
+ * TvAdView.TvAdCallback#onRequestSigning(String, String, String, String, byte[])} can be
+ * included to identify the related signing request, and the method name "onRequestSigning"
+ * can also be added to the params.
+ *
+ * @see #ERROR_KEY_METHOD_NAME
+ */
+ public void notifyError(@NonNull String errMsg, @NonNull Bundle params) {
+ if (DEBUG) {
+ Log.d(TAG, "notifyError msg=" + errMsg + "; params=" + params);
+ }
+ if (mSession != null) {
+ mSession.notifyError(errMsg, params);
+ }
+ }
+
+ /**
+ * This is called to notify the corresponding TV AD service when a new TV message is received.
+ *
+ * @param type The type of message received, such as
+ * {@link TvInputManager#TV_MESSAGE_TYPE_WATERMARK}
+ * @param data The raw data of the message. The bundle keys are:
+ * {@link TvInputManager#TV_MESSAGE_KEY_STREAM_ID},
+ * {@link TvInputManager#TV_MESSAGE_KEY_GROUP_ID},
+ * {@link TvInputManager#TV_MESSAGE_KEY_SUBTYPE},
+ * {@link TvInputManager#TV_MESSAGE_KEY_RAW_DATA}.
+ * See {@link TvInputManager#TV_MESSAGE_KEY_SUBTYPE} for more information on
+ * how to parse this data.
+ */
+ public void notifyTvMessage(@NonNull @TvInputManager.TvMessageType int type,
+ @NonNull Bundle data) {
+ if (DEBUG) {
+ Log.d(TAG, "notifyTvMessage type=" + type
+ + "; data=" + data);
+ }
+ if (mSession != null) {
+ mSession.notifyTvMessage(type, data);
+ }
+ }
+
+ /**
+ * Sets the callback to be invoked when an event is dispatched to this TvAdView.
+ *
+ * @param callback the callback to receive events. MUST NOT be {@code null}.
+ *
+ * @see #clearCallback()
+ * @hide
+ */
+ public void setCallback(
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull TvAdCallback callback) {
+ com.android.internal.util.AnnotationValidations.validate(NonNull.class, null, callback);
+ synchronized (mCallbackLock) {
+ mCallbackExecutor = executor;
+ mCallback = callback;
+ }
+ }
+
+ /**
+ * Clears the callback.
+ *
+ * @see #setCallback(Executor, TvAdCallback)
+ * @hide
+ */
+ public void clearCallback() {
+ synchronized (mCallbackLock) {
+ mCallback = null;
+ mCallbackExecutor = null;
+ }
+ }
+
private class MySessionCallback extends TvAdManager.SessionCallback {
final String mServiceId;
@@ -246,6 +565,7 @@
dispatchSurfaceChanged(mSurfaceFormat, mSurfaceWidth, mSurfaceHeight);
}
}
+ createSessionMediaView();
} else {
// Failed to create
// Todo: forward error to Tv App
@@ -262,6 +582,8 @@
Log.w(TAG, "onSessionReleased - session not created");
return;
}
+ mMediaViewCreated = false;
+ mMediaViewFrame = null;
mSessionCallback = null;
mSession = null;
}
@@ -285,4 +607,11 @@
requestLayout();
}
}
+
+ /**
+ * Callback used to receive various status updates on the {@link TvAdView}.
+ * @hide
+ */
+ public abstract static class TvAdCallback {
+ }
}
diff --git a/media/java/android/media/tv/interactive/ITvInteractiveAppClient.aidl b/media/java/android/media/tv/interactive/ITvInteractiveAppClient.aidl
index e3dba03..7b58531 100644
--- a/media/java/android/media/tv/interactive/ITvInteractiveAppClient.aidl
+++ b/media/java/android/media/tv/interactive/ITvInteractiveAppClient.aidl
@@ -61,7 +61,8 @@
void onSetTvRecordingInfo(in String recordingId, in TvRecordingInfo recordingInfo, int seq);
void onRequestTvRecordingInfo(in String recordingId, int seq);
void onRequestTvRecordingInfoList(in int type, int seq);
- void onRequestSigning(
- in String id, in String algorithm, in String alias, in byte[] data, int seq);
+ void onRequestSigning(in String id, in String algorithm, in String alias, in byte[] data,
+ int seq);
+ void onRequestCertificate(in String host, int port, int seq);
void onAdRequest(in AdRequest request, int Seq);
}
diff --git a/media/java/android/media/tv/interactive/ITvInteractiveAppManager.aidl b/media/java/android/media/tv/interactive/ITvInteractiveAppManager.aidl
index 4316d05..1b9450b 100644
--- a/media/java/android/media/tv/interactive/ITvInteractiveAppManager.aidl
+++ b/media/java/android/media/tv/interactive/ITvInteractiveAppManager.aidl
@@ -58,6 +58,8 @@
void sendAvailableSpeeds(in IBinder sessionToken, in float[] speeds, int userId);
void sendSigningResult(in IBinder sessionToken, in String signingId, in byte[] result,
int userId);
+ void sendCertificate(in IBinder sessionToken, in String host, int port,
+ in Bundle certBundle, int userId);
void sendTvRecordingInfo(in IBinder sessionToken, in TvRecordingInfo recordingInfo, int userId);
void sendTvRecordingInfoList(in IBinder sessionToken,
in List<TvRecordingInfo> recordingInfoList, int userId);
@@ -88,6 +90,7 @@
void notifyTracksChanged(in IBinder sessionToken, in List<TvTrackInfo> tracks, int userId);
void notifyVideoAvailable(in IBinder sessionToken, int userId);
void notifyVideoUnavailable(in IBinder sessionToken, int reason, int userId);
+ void notifyVideoFreezeUpdated(in IBinder sessionToken, boolean isFrozen, int userId);
void notifyContentAllowed(in IBinder sessionToken, int userId);
void notifyContentBlocked(in IBinder sessionToken, in String rating, int userId);
void notifySignalStrength(in IBinder sessionToken, int stength, int userId);
diff --git a/media/java/android/media/tv/interactive/ITvInteractiveAppSession.aidl b/media/java/android/media/tv/interactive/ITvInteractiveAppSession.aidl
index ba7cf13..3969315 100644
--- a/media/java/android/media/tv/interactive/ITvInteractiveAppSession.aidl
+++ b/media/java/android/media/tv/interactive/ITvInteractiveAppSession.aidl
@@ -49,6 +49,7 @@
void sendTimeShiftMode(int mode);
void sendAvailableSpeeds(in float[] speeds);
void sendSigningResult(in String signingId, in byte[] result);
+ void sendCertificate(in String host, int port, in Bundle certBundle);
void sendTvRecordingInfo(in TvRecordingInfo recordingInfo);
void sendTvRecordingInfoList(in List<TvRecordingInfo> recordingInfoList);
void notifyError(in String errMsg, in Bundle params);
@@ -67,6 +68,7 @@
void notifyTracksChanged(in List<TvTrackInfo> tracks);
void notifyVideoAvailable();
void notifyVideoUnavailable(int reason);
+ void notifyVideoFreezeUpdated(boolean isFrozen);
void notifyContentAllowed();
void notifyContentBlocked(in String rating);
void notifySignalStrength(int strength);
diff --git a/media/java/android/media/tv/interactive/ITvInteractiveAppSessionCallback.aidl b/media/java/android/media/tv/interactive/ITvInteractiveAppSessionCallback.aidl
index 416b8f1..cb89181 100644
--- a/media/java/android/media/tv/interactive/ITvInteractiveAppSessionCallback.aidl
+++ b/media/java/android/media/tv/interactive/ITvInteractiveAppSessionCallback.aidl
@@ -61,5 +61,6 @@
void onRequestTvRecordingInfo(in String recordingId);
void onRequestTvRecordingInfoList(in int type);
void onRequestSigning(in String id, in String algorithm, in String alias, in byte[] data);
+ void onRequestCertificate(in String host, int port);
void onAdRequest(in AdRequest request);
}
diff --git a/media/java/android/media/tv/interactive/ITvInteractiveAppSessionWrapper.java b/media/java/android/media/tv/interactive/ITvInteractiveAppSessionWrapper.java
index 518b08a..ec6c2bf 100644
--- a/media/java/android/media/tv/interactive/ITvInteractiveAppSessionWrapper.java
+++ b/media/java/android/media/tv/interactive/ITvInteractiveAppSessionWrapper.java
@@ -103,6 +103,8 @@
private static final int DO_SEND_TIME_SHIFT_MODE = 46;
private static final int DO_SEND_AVAILABLE_SPEEDS = 47;
private static final int DO_SEND_SELECTED_TRACK_INFO = 48;
+ private static final int DO_NOTIFY_VIDEO_FREEZE_UPDATED = 49;
+ private static final int DO_SEND_CERTIFICATE = 50;
private final HandlerCaller mCaller;
private Session mSessionImpl;
@@ -364,6 +366,17 @@
args.recycle();
break;
}
+ case DO_NOTIFY_VIDEO_FREEZE_UPDATED: {
+ mSessionImpl.notifyVideoFreezeUpdated((Boolean) msg.obj);
+ break;
+ }
+ case DO_SEND_CERTIFICATE: {
+ SomeArgs args = (SomeArgs) msg.obj;
+ mSessionImpl.sendCertificate((String) args.arg1, (Integer) args.arg2,
+ (Bundle) args.arg3);
+ args.recycle();
+ break;
+ }
default: {
Log.w(TAG, "Unhandled message code: " + msg.what);
break;
@@ -478,6 +491,12 @@
}
@Override
+ public void sendCertificate(@NonNull String host, int port, @NonNull Bundle certBundle) {
+ mCaller.executeOrSendMessage(
+ mCaller.obtainMessageOOO(DO_SEND_CERTIFICATE, host, port, certBundle));
+ }
+
+ @Override
public void notifyError(@NonNull String errMsg, @NonNull Bundle params) {
mCaller.executeOrSendMessage(
mCaller.obtainMessageOO(DO_NOTIFY_ERROR, errMsg, params));
@@ -552,6 +571,12 @@
}
@Override
+ public void notifyVideoFreezeUpdated(boolean isFrozen) {
+ mCaller.executeOrSendMessage(mCaller.obtainMessageO(DO_NOTIFY_VIDEO_FREEZE_UPDATED,
+ isFrozen));
+ }
+
+ @Override
public void notifyContentAllowed() {
mCaller.executeOrSendMessage(mCaller.obtainMessage(DO_NOTIFY_CONTENT_ALLOWED));
}
diff --git a/media/java/android/media/tv/interactive/TvInteractiveAppManager.java b/media/java/android/media/tv/interactive/TvInteractiveAppManager.java
index bf4379f..011744f 100755
--- a/media/java/android/media/tv/interactive/TvInteractiveAppManager.java
+++ b/media/java/android/media/tv/interactive/TvInteractiveAppManager.java
@@ -34,6 +34,7 @@
import android.media.tv.TvRecordingInfo;
import android.media.tv.TvTrackInfo;
import android.net.Uri;
+import android.net.http.SslCertificate;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
@@ -656,6 +657,18 @@
}
@Override
+ public void onRequestCertificate(String host, int port, int seq) {
+ synchronized (mSessionCallbackRecordMap) {
+ SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq);
+ if (record == null) {
+ Log.e(TAG, "Callback not found for seq " + seq);
+ return;
+ }
+ record.postRequestCertificate(host, port);
+ }
+ }
+
+ @Override
public void onSessionStateChanged(int state, int err, int seq) {
synchronized (mSessionCallbackRecordMap) {
SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq);
@@ -1328,6 +1341,19 @@
}
}
+ void sendCertificate(@NonNull String host, int port, @NonNull SslCertificate cert) {
+ if (mToken == null) {
+ Log.w(TAG, "The session has been already released");
+ return;
+ }
+ try {
+ mService.sendCertificate(mToken, host, port, SslCertificate.saveState(cert),
+ mUserId);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
void notifyError(@NonNull String errMsg, @NonNull Bundle params) {
if (mToken == null) {
Log.w(TAG, "The session has been already released");
@@ -1731,6 +1757,22 @@
}
/**
+ * Notifies Interactive app session when the video freeze state is updated
+ * @param isFrozen Whether or not the video is frozen
+ */
+ public void notifyVideoFreezeUpdated(boolean isFrozen) {
+ if (mToken == null) {
+ Log.w(TAG, "The session has been already released");
+ return;
+ }
+ try {
+ mService.notifyVideoFreezeUpdated(mToken, isFrozen, mUserId);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
* Notifies Interactive APP session when content is allowed.
*/
public void notifyContentAllowed() {
@@ -2216,6 +2258,15 @@
});
}
+ void postRequestCertificate(String host, int port) {
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ mSessionCallback.onRequestCertificate(mSession, host, port);
+ }
+ });
+ }
+
void postRequestTvRecordingInfo(String recordingId) {
mHandler.post(new Runnable() {
@Override
@@ -2558,6 +2609,17 @@
}
/**
+ * This is called when the service requests a SSL certificate for client validation.
+ *
+ * @param session A {@link TvInteractiveAppService.Session} associated with this callback.
+ * @param host the host name of the SSL authentication server.
+ * @param port the port of the SSL authentication server. E.g., 443
+ * @hide
+ */
+ public void onRequestCertificate(Session session, String host, int port) {
+ }
+
+ /**
* This is called when {@link TvInteractiveAppService.Session#notifySessionStateChanged} is
* called.
*
diff --git a/media/java/android/media/tv/interactive/TvInteractiveAppService.java b/media/java/android/media/tv/interactive/TvInteractiveAppService.java
index 7936403..054b272 100755
--- a/media/java/android/media/tv/interactive/TvInteractiveAppService.java
+++ b/media/java/android/media/tv/interactive/TvInteractiveAppService.java
@@ -46,6 +46,7 @@
import android.media.tv.TvView;
import android.media.tv.interactive.TvInteractiveAppView.TvInteractiveAppCallback;
import android.net.Uri;
+import android.net.http.SslCertificate;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Handler;
@@ -734,6 +735,17 @@
}
/**
+ * Receives the requested Certificate
+ *
+ * @param host the host name of the SSL authentication server.
+ * @param port the port of the SSL authentication server. E.g., 443
+ * @param cert the SSL certificate received.
+ * @hide
+ */
+ public void onCertificate(@NonNull String host, int port, @NonNull SslCertificate cert) {
+ }
+
+ /**
* Called when the application sends information of an error.
*
* @param errMsg the message of the error.
@@ -882,6 +894,15 @@
}
/**
+ * Called when video becomes frozen or unfrozen. Audio playback will continue while
+ * video will be frozen to the last frame if {@code true}.
+ * @param isFrozen Whether or not the video is frozen.
+ * @hide
+ */
+ public void onVideoFreezeUpdated(boolean isFrozen) {
+ }
+
+ /**
* Called when content is allowed.
*/
public void onContentAllowed() {
@@ -1624,6 +1645,32 @@
}
/**
+ * Requests a SSL certificate for client validation.
+ *
+ * @param host the host name of the SSL authentication server.
+ * @param port the port of the SSL authentication server. E.g., 443
+ * @hide
+ */
+ public void requestCertificate(@NonNull String host, int port) {
+ executeOrPostRunnableOnMainThread(new Runnable() {
+ @MainThread
+ @Override
+ public void run() {
+ try {
+ if (DEBUG) {
+ Log.d(TAG, "requestCertificate");
+ }
+ if (mSessionCallback != null) {
+ mSessionCallback.onRequestCertificate(host, port);
+ }
+ } catch (RemoteException e) {
+ Log.w(TAG, "error in requestCertificate", e);
+ }
+ }
+ });
+ }
+
+ /**
* Sends an advertisement request to be processed by the related TV input.
*
* @param request The advertisement request
@@ -1716,6 +1763,11 @@
onSigningResult(signingId, result);
}
+ void sendCertificate(String host, int port, Bundle certBundle) {
+ SslCertificate cert = SslCertificate.restoreState(certBundle);
+ onCertificate(host, port, cert);
+ }
+
void notifyError(String errMsg, Bundle params) {
onError(errMsg, params);
}
@@ -1770,6 +1822,13 @@
onVideoUnavailable(reason);
}
+ void notifyVideoFreezeUpdated(boolean isFrozen) {
+ if (DEBUG) {
+ Log.d(TAG, "notifyVideoFreezeUpdated (isFrozen=" + isFrozen + ")");
+ }
+ onVideoFreezeUpdated(isFrozen);
+ }
+
void notifyContentAllowed() {
if (DEBUG) {
Log.d(TAG, "notifyContentAllowed");
diff --git a/media/java/android/media/tv/interactive/TvInteractiveAppView.java b/media/java/android/media/tv/interactive/TvInteractiveAppView.java
index 40a12e4..3b29574 100755
--- a/media/java/android/media/tv/interactive/TvInteractiveAppView.java
+++ b/media/java/android/media/tv/interactive/TvInteractiveAppView.java
@@ -34,6 +34,7 @@
import android.media.tv.interactive.TvInteractiveAppManager.Session.FinishedInputEventCallback;
import android.media.tv.interactive.TvInteractiveAppManager.SessionCallback;
import android.net.Uri;
+import android.net.http.SslCertificate;
import android.os.Bundle;
import android.os.Handler;
import android.util.AttributeSet;
@@ -719,6 +720,22 @@
}
/**
+ * Alerts the TV Interactive app that the video freeze state has been updated.
+ * If {@code true}, the video is frozen on the last frame while audio playback continues.
+ *
+ * @param isFrozen Whether the video is frozen.
+ * @hide
+ */
+ public void notifyVideoFreezeUpdated(boolean isFrozen) {
+ if (DEBUG) {
+ Log.d(TAG, "notifyVideoFreezeUpdated");
+ }
+ if (mSession != null) {
+ mSession.notifyVideoFreezeUpdated(isFrozen);
+ }
+ }
+
+ /**
* Sends signing result to related TV interactive app.
*
* <p>This is used when the corresponding server of the broadcast-independent interactive
@@ -740,6 +757,22 @@
}
/**
+ * Send the requested SSL certificate to the TV Interactive App
+ * @param host the host name of the SSL authentication server.
+ * @param port the port of the SSL authentication server. E.g., 443
+ * @param cert the SSL certificate requested
+ * @hide
+ */
+ public void sendCertificate(@NonNull String host, int port, @NonNull SslCertificate cert) {
+ if (DEBUG) {
+ Log.d(TAG, "sendCertificate");
+ }
+ if (mSession != null) {
+ mSession.sendCertificate(host, port, cert);
+ }
+ }
+
+ /**
* Notifies the corresponding {@link TvInteractiveAppService} when there is an error.
*
* @param errMsg the message of the error.
diff --git a/media/jni/soundpool/StreamManager.cpp b/media/jni/soundpool/StreamManager.cpp
index 52060f1..66fec1c 100644
--- a/media/jni/soundpool/StreamManager.cpp
+++ b/media/jni/soundpool/StreamManager.cpp
@@ -35,10 +35,9 @@
// In R, we change this to true, as it is the correct way per SoundPool documentation.
static constexpr bool kStealActiveStream_OldestFirst = true;
-// kPlayOnCallingThread = true prior to R.
// Changing to false means calls to play() are almost instantaneous instead of taking around
// ~10ms to launch the AudioTrack. It is perhaps 100x faster.
-static constexpr bool kPlayOnCallingThread = true;
+static constexpr bool kPlayOnCallingThread = false;
// Amount of time for a StreamManager thread to wait before closing.
static constexpr int64_t kWaitTimeBeforeCloseNs = 9 * NANOS_PER_SECOND;
diff --git a/media/jni/soundpool/StreamManager.h b/media/jni/soundpool/StreamManager.h
index adbab4b..340b49b 100644
--- a/media/jni/soundpool/StreamManager.h
+++ b/media/jni/soundpool/StreamManager.h
@@ -48,7 +48,7 @@
public:
JavaThread(std::function<void()> f, const char *name)
: mF{std::move(f)} {
- createThreadEtc(staticFunction, this, name);
+ createThreadEtc(staticFunction, this, name, ANDROID_PRIORITY_AUDIO);
}
JavaThread(JavaThread &&) = delete; // uses "this" ptr, not moveable.
diff --git a/media/jni/soundpool/android_media_SoundPool.cpp b/media/jni/soundpool/android_media_SoundPool.cpp
index 25040a9..e872a58 100644
--- a/media/jni/soundpool/android_media_SoundPool.cpp
+++ b/media/jni/soundpool/android_media_SoundPool.cpp
@@ -86,7 +86,7 @@
}
// Retrieves the associated object, returns nullValue T if not available.
- T get(JNIEnv *env, jobject thiz) {
+ T get(JNIEnv *env, jobject thiz) const {
std::lock_guard lg(mLock);
// NOLINTNEXTLINE(performance-no-int-to-ptr)
auto ptr = reinterpret_cast<T*>(env->GetLongField(thiz, mFieldId));
@@ -167,8 +167,10 @@
// is possible by checking if the WeakGlobalRef is null equivalent.
auto& getSoundPoolManager() {
- static ObjectManager<std::shared_ptr<SoundPool>> soundPoolManager(fields.mNativeContext);
- return soundPoolManager;
+ // never-delete singleton
+ static auto soundPoolManager =
+ new ObjectManager<std::shared_ptr<SoundPool>>(fields.mNativeContext);
+ return *soundPoolManager;
}
inline auto getSoundPool(JNIEnv *env, jobject thiz) {
@@ -274,8 +276,9 @@
auto& getSoundPoolJavaRefManager() {
// Note this can store shared_ptrs to either jweak and jobject,
// as the underlying type is identical.
- static ConcurrentHashMap<SoundPool *, std::shared_ptr<JWeakValue>> concurrentHashMap;
- return concurrentHashMap;
+ static auto concurrentHashMap =
+ new ConcurrentHashMap<SoundPool *, std::shared_ptr<JWeakValue>>();
+ return *concurrentHashMap;
}
// make_shared_globalref_from_localref() creates a sharable Java global
diff --git a/media/tests/LoudnessCodecApiTest/src/com/android/loudnesscodecapitest/LoudnessCodecControllerTest.java b/media/tests/LoudnessCodecApiTest/src/com/android/loudnesscodecapitest/LoudnessCodecControllerTest.java
index 4f6ede5..46256ba 100644
--- a/media/tests/LoudnessCodecApiTest/src/com/android/loudnesscodecapitest/LoudnessCodecControllerTest.java
+++ b/media/tests/LoudnessCodecApiTest/src/com/android/loudnesscodecapitest/LoudnessCodecControllerTest.java
@@ -126,7 +126,7 @@
try {
mLcc.addMediaCodec(mediaCodec);
- mLcc.release(); // stops updats
+ mLcc.close(); // stops updates
verify(mAudioService).stopLoudnessCodecUpdates(eq(mSessionId));
} finally {
diff --git a/native/android/performance_hint.cpp b/native/android/performance_hint.cpp
index abe4a3d..c572944 100644
--- a/native/android/performance_hint.cpp
+++ b/native/android/performance_hint.cpp
@@ -228,11 +228,6 @@
}
int APerformanceHintSession::reportActualWorkDuration(int64_t actualDurationNanos) {
- if (actualDurationNanos <= 0) {
- ALOGE("%s: actualDurationNanos must be positive", __FUNCTION__);
- return EINVAL;
- }
-
WorkDuration workDuration(0, actualDurationNanos, actualDurationNanos, 0);
return reportActualWorkDurationInternal(&workDuration);
@@ -320,23 +315,6 @@
int APerformanceHintSession::reportActualWorkDuration(AWorkDuration* aWorkDuration) {
WorkDuration* workDuration = static_cast<WorkDuration*>(aWorkDuration);
- if (workDuration->workPeriodStartTimestampNanos <= 0) {
- ALOGE("%s: workPeriodStartTimestampNanos must be positive", __FUNCTION__);
- return EINVAL;
- }
- if (workDuration->actualTotalDurationNanos <= 0) {
- ALOGE("%s: actualDurationNanos must be positive", __FUNCTION__);
- return EINVAL;
- }
- if (workDuration->actualCpuDurationNanos <= 0) {
- ALOGE("%s: cpuDurationNanos must be positive", __FUNCTION__);
- return EINVAL;
- }
- if (workDuration->actualGpuDurationNanos < 0) {
- ALOGE("%s: gpuDurationNanos must be non negative", __FUNCTION__);
- return EINVAL;
- }
-
return reportActualWorkDurationInternal(workDuration);
}
@@ -428,62 +406,87 @@
return APerformanceHintManager::getInstance();
}
+#define VALIDATE_PTR(ptr) \
+ LOG_ALWAYS_FATAL_IF(ptr == nullptr, "%s: " #ptr " is nullptr", __FUNCTION__);
+
+#define VALIDATE_INT(value, cmp) \
+ if (!(value cmp)) { \
+ ALOGE("%s: Invalid value. Check failed: (" #value " " #cmp ") with value: %" PRIi64, \
+ __FUNCTION__, value); \
+ return EINVAL; \
+ }
+
+#define WARN_INT(value, cmp) \
+ if (!(value cmp)) { \
+ ALOGE("%s: Invalid value. Check failed: (" #value " " #cmp ") with value: %" PRIi64, \
+ __FUNCTION__, value); \
+ }
+
APerformanceHintSession* APerformanceHint_createSession(APerformanceHintManager* manager,
const int32_t* threadIds, size_t size,
int64_t initialTargetWorkDurationNanos) {
+ VALIDATE_PTR(manager)
+ VALIDATE_PTR(threadIds)
return manager->createSession(threadIds, size, initialTargetWorkDurationNanos);
}
int64_t APerformanceHint_getPreferredUpdateRateNanos(APerformanceHintManager* manager) {
+ VALIDATE_PTR(manager)
return manager->getPreferredRateNanos();
}
int APerformanceHint_updateTargetWorkDuration(APerformanceHintSession* session,
int64_t targetDurationNanos) {
+ VALIDATE_PTR(session)
return session->updateTargetWorkDuration(targetDurationNanos);
}
int APerformanceHint_reportActualWorkDuration(APerformanceHintSession* session,
int64_t actualDurationNanos) {
+ VALIDATE_PTR(session)
+ VALIDATE_INT(actualDurationNanos, > 0)
return session->reportActualWorkDuration(actualDurationNanos);
}
void APerformanceHint_closeSession(APerformanceHintSession* session) {
+ VALIDATE_PTR(session)
delete session;
}
int APerformanceHint_sendHint(void* session, SessionHint hint) {
+ VALIDATE_PTR(session)
return reinterpret_cast<APerformanceHintSession*>(session)->sendHint(hint);
}
int APerformanceHint_setThreads(APerformanceHintSession* session, const pid_t* threadIds,
size_t size) {
- if (session == nullptr) {
- return EINVAL;
- }
+ VALIDATE_PTR(session)
+ VALIDATE_PTR(threadIds)
return session->setThreads(threadIds, size);
}
int APerformanceHint_getThreadIds(void* aPerformanceHintSession, int32_t* const threadIds,
size_t* const size) {
- if (aPerformanceHintSession == nullptr) {
- return EINVAL;
- }
+ VALIDATE_PTR(aPerformanceHintSession)
return static_cast<APerformanceHintSession*>(aPerformanceHintSession)
->getThreadIds(threadIds, size);
}
int APerformanceHint_setPreferPowerEfficiency(APerformanceHintSession* session, bool enabled) {
+ VALIDATE_PTR(session)
return session->setPreferPowerEfficiency(enabled);
}
int APerformanceHint_reportActualWorkDuration2(APerformanceHintSession* session,
- AWorkDuration* workDuration) {
- if (session == nullptr || workDuration == nullptr) {
- ALOGE("Invalid value: (session %p, workDuration %p)", session, workDuration);
- return EINVAL;
- }
- return session->reportActualWorkDuration(workDuration);
+ AWorkDuration* workDurationPtr) {
+ VALIDATE_PTR(session)
+ VALIDATE_PTR(workDurationPtr)
+ WorkDuration& workDuration = *static_cast<WorkDuration*>(workDurationPtr);
+ VALIDATE_INT(workDuration.workPeriodStartTimestampNanos, > 0)
+ VALIDATE_INT(workDuration.actualTotalDurationNanos, > 0)
+ VALIDATE_INT(workDuration.actualCpuDurationNanos, > 0)
+ VALIDATE_INT(workDuration.actualGpuDurationNanos, >= 0)
+ return session->reportActualWorkDuration(workDurationPtr);
}
AWorkDuration* AWorkDuration_create() {
@@ -492,46 +495,36 @@
}
void AWorkDuration_release(AWorkDuration* aWorkDuration) {
- if (aWorkDuration == nullptr) {
- ALOGE("%s: aWorkDuration is nullptr", __FUNCTION__);
- }
+ VALIDATE_PTR(aWorkDuration)
delete aWorkDuration;
}
void AWorkDuration_setWorkPeriodStartTimestampNanos(AWorkDuration* aWorkDuration,
int64_t workPeriodStartTimestampNanos) {
- if (aWorkDuration == nullptr || workPeriodStartTimestampNanos <= 0) {
- ALOGE("%s: Invalid value. (AWorkDuration: %p, workPeriodStartTimestampNanos: %" PRIi64 ")",
- __FUNCTION__, aWorkDuration, workPeriodStartTimestampNanos);
- }
+ VALIDATE_PTR(aWorkDuration)
+ WARN_INT(workPeriodStartTimestampNanos, > 0)
static_cast<WorkDuration*>(aWorkDuration)->workPeriodStartTimestampNanos =
workPeriodStartTimestampNanos;
}
void AWorkDuration_setActualTotalDurationNanos(AWorkDuration* aWorkDuration,
int64_t actualTotalDurationNanos) {
- if (aWorkDuration == nullptr || actualTotalDurationNanos <= 0) {
- ALOGE("%s: Invalid value. (AWorkDuration: %p, actualTotalDurationNanos: %" PRIi64 ")",
- __FUNCTION__, aWorkDuration, actualTotalDurationNanos);
- }
+ VALIDATE_PTR(aWorkDuration)
+ WARN_INT(actualTotalDurationNanos, > 0)
static_cast<WorkDuration*>(aWorkDuration)->actualTotalDurationNanos = actualTotalDurationNanos;
}
void AWorkDuration_setActualCpuDurationNanos(AWorkDuration* aWorkDuration,
int64_t actualCpuDurationNanos) {
- if (aWorkDuration == nullptr || actualCpuDurationNanos <= 0) {
- ALOGE("%s: Invalid value. (AWorkDuration: %p, actualCpuDurationNanos: %" PRIi64 ")",
- __FUNCTION__, aWorkDuration, actualCpuDurationNanos);
- }
+ VALIDATE_PTR(aWorkDuration)
+ WARN_INT(actualCpuDurationNanos, > 0)
static_cast<WorkDuration*>(aWorkDuration)->actualCpuDurationNanos = actualCpuDurationNanos;
}
void AWorkDuration_setActualGpuDurationNanos(AWorkDuration* aWorkDuration,
int64_t actualGpuDurationNanos) {
- if (aWorkDuration == nullptr || actualGpuDurationNanos < 0) {
- ALOGE("%s: Invalid value. (AWorkDuration: %p, actualGpuDurationNanos: %" PRIi64 ")",
- __FUNCTION__, aWorkDuration, actualGpuDurationNanos);
- }
+ VALIDATE_PTR(aWorkDuration)
+ WARN_INT(actualGpuDurationNanos, >= 0)
static_cast<WorkDuration*>(aWorkDuration)->actualGpuDurationNanos = actualGpuDurationNanos;
}
diff --git a/native/graphics/jni/Android.bp b/native/graphics/jni/Android.bp
index 10c570b..8ea4632 100644
--- a/native/graphics/jni/Android.bp
+++ b/native/graphics/jni/Android.bp
@@ -72,6 +72,9 @@
],
},
},
+ stubs: {
+ symbol_file: "libjnigraphics.map.txt",
+ },
}
// The headers module is in frameworks/native/Android.bp.
@@ -93,15 +96,18 @@
],
static_libs: ["libarect"],
fuzz_config: {
- cc: ["dichenzhang@google.com","scroggo@google.com"],
+ cc: [
+ "dichenzhang@google.com",
+ "scroggo@google.com",
+ ],
asan_options: [
"detect_odr_violation=1",
],
hwasan_options: [
- // Image decoders may attempt to allocate a large amount of memory
- // (especially if the encoded image is large). This doesn't
- // necessarily mean there is a bug. Set allocator_may_return_null=1
- // for hwasan so the fuzzer can continue running.
+ // Image decoders may attempt to allocate a large amount of memory
+ // (especially if the encoded image is large). This doesn't
+ // necessarily mean there is a bug. Set allocator_may_return_null=1
+ // for hwasan so the fuzzer can continue running.
"allocator_may_return_null = 1",
],
},
diff --git a/nfc/api/system-current.txt b/nfc/api/system-current.txt
index dc2a625..3524f8c 100644
--- a/nfc/api/system-current.txt
+++ b/nfc/api/system-current.txt
@@ -45,7 +45,7 @@
package android.nfc.cardemulation {
public final class CardEmulation {
- method @FlaggedApi("android.permission.flags.wallet_role_enabled") @Nullable @RequiresPermission(android.Manifest.permission.NFC_PREFERRED_PAYMENT_INFO) public android.nfc.cardemulation.ApduServiceInfo getPreferredPaymentService();
+ method @FlaggedApi("android.permission.flags.wallet_role_enabled") @Nullable @RequiresPermission(android.Manifest.permission.NFC_PREFERRED_PAYMENT_INFO) public static android.content.ComponentName getPreferredPaymentService(@NonNull android.content.Context);
method @FlaggedApi("android.nfc.enable_nfc_mainline") @NonNull public java.util.List<android.nfc.cardemulation.ApduServiceInfo> getServices(@NonNull String, int);
}
diff --git a/nfc/java/android/nfc/cardemulation/CardEmulation.java b/nfc/java/android/nfc/cardemulation/CardEmulation.java
index 0943392..9d38e4c 100644
--- a/nfc/java/android/nfc/cardemulation/CardEmulation.java
+++ b/nfc/java/android/nfc/cardemulation/CardEmulation.java
@@ -16,6 +16,7 @@
package android.nfc.cardemulation;
+import android.Manifest;
import android.annotation.FlaggedApi;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -23,6 +24,7 @@
import android.annotation.SdkConstant;
import android.annotation.SdkConstant.SdkConstantType;
import android.annotation.SystemApi;
+import android.annotation.UserHandleAware;
import android.annotation.UserIdInt;
import android.app.Activity;
import android.content.ComponentName;
@@ -1138,31 +1140,28 @@
}
/**
- * Returns the {@link Settings.Secure#NFC_PAYMENT_DEFAULT_COMPONENT} for the given user.
+ * Returns the value of {@link Settings.Secure#NFC_PAYMENT_DEFAULT_COMPONENT}.
+ *
+ * @param context A context
+ * @return A ComponentName for the setting value, or null.
*
* @hide
*/
@SystemApi
+ @UserHandleAware
@RequiresPermission(android.Manifest.permission.NFC_PREFERRED_PAYMENT_INFO)
+ @SuppressWarnings("AndroidFrameworkClientSidePermissionCheck")
@FlaggedApi(android.permission.flags.Flags.FLAG_WALLET_ROLE_ENABLED)
@Nullable
- public ApduServiceInfo getPreferredPaymentService() {
- try {
- return sService.getPreferredPaymentService(mContext.getUser().getIdentifier());
- } catch (RemoteException e) {
- // Try one more time
- recoverService();
- if (sService == null) {
- Log.e(TAG, "Failed to recover CardEmulationService.");
- return null;
- }
- try {
- return sService.getPreferredPaymentService(mContext.getUser().getIdentifier());
- } catch (RemoteException ee) {
- Log.e(TAG, "Failed to reach CardEmulationService.");
- return null;
- }
- }
- }
+ public static ComponentName getPreferredPaymentService(@NonNull Context context) {
+ context.checkCallingOrSelfPermission(Manifest.permission.NFC_PREFERRED_PAYMENT_INFO);
+ String defaultPaymentComponent = Settings.Secure.getString(context.getContentResolver(),
+ Constants.SETTINGS_SECURE_NFC_PAYMENT_DEFAULT_COMPONENT);
+ if (defaultPaymentComponent == null) {
+ return null;
+ }
+
+ return ComponentName.unflattenFromString(defaultPaymentComponent);
+ }
}
diff --git a/packages/CredentialManager/res/drawable/fill_dialog_dynamic_list_item_one.xml b/packages/CredentialManager/res/drawable/fill_dialog_dynamic_list_item_one.xml
index 5becc86..f13402c 100644
--- a/packages/CredentialManager/res/drawable/fill_dialog_dynamic_list_item_one.xml
+++ b/packages/CredentialManager/res/drawable/fill_dialog_dynamic_list_item_one.xml
@@ -23,7 +23,7 @@
android:shape="rectangle"
android:top="1dp">
<shape>
- <corners android:radius="16dp" />
+ <corners android:radius="4dp" />
<solid android:color="@color/dropdown_container" />
</shape>
</item>
diff --git a/packages/CredentialManager/res/drawable/more_options_list_item.xml b/packages/CredentialManager/res/drawable/more_options_list_item.xml
new file mode 100644
index 0000000..d7b509e
--- /dev/null
+++ b/packages/CredentialManager/res/drawable/more_options_list_item.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2024 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<ripple xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools" tools:ignore="NewApi"
+ android:color="@android:color/transparent">
+ <item
+ android:bottom="1dp"
+ android:shape="rectangle"
+ android:top="1dp">
+ <shape>
+ <corners android:bottomLeftRadius="4dp"
+ android:bottomRightRadius="4dp"/>
+ <solid android:color="@color/sign_in_options_container" />
+ </shape>
+ </item>
+</ripple>
\ No newline at end of file
diff --git a/packages/CredentialManager/res/layout/credman_dropdown_bottom_sheet.xml b/packages/CredentialManager/res/layout/credman_dropdown_bottom_sheet.xml
new file mode 100644
index 0000000..929756c
--- /dev/null
+++ b/packages/CredentialManager/res/layout/credman_dropdown_bottom_sheet.xml
@@ -0,0 +1,42 @@
+<!--
+ ~ Copyright (C) 2024 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@android:id/content"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:layout_marginEnd="@dimen/dropdown_layout_horizontal_margin"
+ android:elevation="3dp">
+
+ <ImageView
+ android:id="@android:id/icon1"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_centerVertical="true"
+ android:layout_alignParentStart="true"
+ android:contentDescription="@string/provider_icon_content_description"
+ android:background="@null"/>
+ <TextView
+ android:id="@android:id/text1"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentTop="true"
+ android:layout_toEndOf="@android:id/icon1"
+ android:minWidth="@dimen/autofill_dropdown_textview_min_width"
+ android:maxWidth="@dimen/autofill_dropdown_textview_max_width"
+ style="@style/autofill.TextTitle"/>
+
+</RelativeLayout>
diff --git a/packages/CredentialManager/res/layout/credman_dropdown_presentation_layout.xml b/packages/CredentialManager/res/layout/credman_dropdown_presentation_layout.xml
index cb6c6b4..1fe5e0e 100644
--- a/packages/CredentialManager/res/layout/credman_dropdown_presentation_layout.xml
+++ b/packages/CredentialManager/res/layout/credman_dropdown_presentation_layout.xml
@@ -17,22 +17,25 @@
android:id="@android:id/content"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:maxWidth="@dimen/autofill_dropdown_layout_width"
+ android:layout_marginEnd="@dimen/dropdown_layout_horizontal_margin"
android:elevation="3dp">
<ImageView
android:id="@android:id/icon1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
+ android:contentDescription="@string/provider_icon_content_description"
android:layout_centerVertical="true"
android:layout_alignParentStart="true"
android:background="@null"/>
<TextView
android:id="@android:id/text1"
- android:layout_width="@dimen/autofill_dropdown_text_width"
+ android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_toEndOf="@android:id/icon1"
+ android:minWidth="@dimen/autofill_dropdown_textview_min_width"
+ android:maxWidth="@dimen/autofill_dropdown_textview_max_width"
style="@style/autofill.TextTitle"/>
<TextView
android:id="@android:id/text2"
@@ -40,6 +43,8 @@
android:layout_height="wrap_content"
android:layout_below="@android:id/text1"
android:layout_toEndOf="@android:id/icon1"
+ android:minWidth="@dimen/autofill_dropdown_textview_min_width"
+ android:maxWidth="@dimen/autofill_dropdown_textview_max_width"
style="@style/autofill.TextSubtitle"/>
</RelativeLayout>
diff --git a/packages/CredentialManager/res/values/colors.xml b/packages/CredentialManager/res/values/colors.xml
index dcb7ef9..7cb1d01 100644
--- a/packages/CredentialManager/res/values/colors.xml
+++ b/packages/CredentialManager/res/values/colors.xml
@@ -20,4 +20,6 @@
<color name="text_primary">#1A1B20</color>
<color name="text_secondary">#44474F</color>
<color name="dropdown_container">#F3F3FA</color>
+ <color name="sign_in_options_container">#DADADA</color>
+ <color name="sign_in_options_icon_color">#1B1B1B</color>
</resources>
\ No newline at end of file
diff --git a/packages/CredentialManager/res/values/dimens.xml b/packages/CredentialManager/res/values/dimens.xml
index 2a4719d..3a8c78f 100644
--- a/packages/CredentialManager/res/values/dimens.xml
+++ b/packages/CredentialManager/res/values/dimens.xml
@@ -18,11 +18,13 @@
<resources>
<dimen name="autofill_view_top_padding">12dp</dimen>
- <dimen name="autofill_view_right_padding">24dp</dimen>
+ <dimen name="autofill_view_right_padding">12dp</dimen>
<dimen name="autofill_view_bottom_padding">12dp</dimen>
<dimen name="autofill_view_left_padding">16dp</dimen>
<dimen name="autofill_view_icon_to_text_padding">10dp</dimen>
<dimen name="autofill_icon_size">24dp</dimen>
- <dimen name="autofill_dropdown_layout_width">296dp</dimen>
- <dimen name="autofill_dropdown_text_width">240dp</dimen>
+ <dimen name="autofill_dropdown_textview_min_width">112dp</dimen>
+ <dimen name="autofill_dropdown_textview_max_width">230dp</dimen>
+ <dimen name="dropdown_layout_horizontal_margin">24dp</dimen>
+ <integer name="autofill_max_visible_datasets">3</integer>
</resources>
\ No newline at end of file
diff --git a/packages/CredentialManager/res/values/strings.xml b/packages/CredentialManager/res/values/strings.xml
index 605e77b..f98164b 100644
--- a/packages/CredentialManager/res/values/strings.xml
+++ b/packages/CredentialManager/res/values/strings.xml
@@ -168,4 +168,9 @@
<string name="get_dialog_option_headline_use_a_different_device">Use a different device</string>
<!-- Text shown on a snackbar when the app cancelled the UI. [CHAR LIMIT=120] -->
<string name="request_cancelled_by">Request cancelled by <xliff:g id="app_name" example="YouTube">%1$s</xliff:g></string>
+
+ <!-- Strings for dropdown presentation. -->
+ <!-- Text shown in the dropdown presentation to select more sign in options. [CHAR LIMIT=120] -->
+ <string name="dropdown_presentation_more_sign_in_options_text">Sign-in options</string>
+ <string name="provider_icon_content_description">Credential provider icon</string>
</resources>
\ No newline at end of file
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/autofill/CredentialAutofillService.kt b/packages/CredentialManager/src/com/android/credentialmanager/autofill/CredentialAutofillService.kt
index 03ac605..985f322 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/autofill/CredentialAutofillService.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/autofill/CredentialAutofillService.kt
@@ -30,6 +30,7 @@
import android.os.Bundle
import android.os.CancellationSignal
import android.os.OutcomeReceiver
+import android.provider.Settings
import android.credentials.Credential
import android.service.autofill.AutofillService
import android.service.autofill.Dataset
@@ -48,7 +49,9 @@
import android.view.autofill.AutofillId
import android.widget.inline.InlinePresentationSpec
import android.credentials.CredentialManager
+import android.widget.RemoteViews
import androidx.autofill.inline.v1.InlineSuggestionUi
+import androidx.core.content.ContextCompat
import androidx.credentials.provider.CustomCredentialEntry
import androidx.credentials.provider.PasswordCredentialEntry
import androidx.credentials.provider.PublicKeyCredentialEntry
@@ -115,7 +118,7 @@
}
val getCredRequest: GetCredentialRequest? = getCredManRequest(structure, sessionId,
- requestId)
+ requestId)
if (getCredRequest == null) {
Log.i(TAG, "No credential manager request found")
callback.onFailure("No credential manager request found")
@@ -307,10 +310,14 @@
val inlineMaxSuggestedCount = inlineSuggestionsRequest?.maxSuggestionCount ?: 0
val inlinePresentationSpecs = inlineSuggestionsRequest?.inlinePresentationSpecs
val inlinePresentationSpecsCount = inlinePresentationSpecs?.size ?: 0
- var maxItemCount = totalEntryCount
- if (inlineMaxSuggestedCount > 0) {
- maxItemCount = maxItemCount.coerceAtMost(inlineMaxSuggestedCount)
- }
+ val maxDropdownDisplayLimit = this.resources.getInteger(
+ com.android.credentialmanager.R.integer.autofill_max_visible_datasets)
+ var maxInlineItemCount = totalEntryCount
+ maxInlineItemCount = maxInlineItemCount.coerceAtMost(inlineMaxSuggestedCount)
+ val lastDropdownDatasetIndex = Settings.Global.getInt(this.contentResolver,
+ Settings.Global.AUTOFILL_MAX_VISIBLE_DATASETS,
+ (maxDropdownDisplayLimit - 1).coerceAtMost(totalEntryCount - 1))
+
var i = 0
var datasetAdded = false
@@ -333,13 +340,8 @@
Log.e(TAG, "PendingIntent was missing from the entry.")
return@usernameLoop
}
- if (inlinePresentationSpecs == null) {
- Log.i(TAG, "Inline presentation spec is null, " +
- "building dropdown presentation only")
- }
- if (i >= maxItemCount) {
- Log.e(TAG, "Skipping because reached the max item count.")
- return@usernameLoop
+ if (i >= maxInlineItemCount && i >= lastDropdownDatasetIndex) {
+ return@usernameLoop;
}
val icon: Icon = if (primaryEntry.icon == null) {
// The empty entry icon has non-null icon reference but null drawable reference.
@@ -351,38 +353,26 @@
}
// Create inline presentation
var inlinePresentation: InlinePresentation? = null
- var spec: InlinePresentationSpec?
- if (inlinePresentationSpecs != null) {
- if (i < inlinePresentationSpecsCount) {
- spec = inlinePresentationSpecs[i]
+ if (inlinePresentationSpecs != null && i < maxInlineItemCount) {
+ val spec: InlinePresentationSpec? = if (i < inlinePresentationSpecsCount) {
+ inlinePresentationSpecs[i]
} else {
- spec = inlinePresentationSpecs[inlinePresentationSpecsCount - 1]
+ inlinePresentationSpecs[inlinePresentationSpecsCount - 1]
}
- val displayName: String = if (primaryEntry.credentialType ==
- CredentialType.PASSKEY && primaryEntry.displayName != null) {
- primaryEntry.displayName!!
- } else {
- primaryEntry.userName
- }
- val sliceBuilder = InlineSuggestionUi
- .newContentBuilder(pendingIntent)
- .setTitle(displayName)
- sliceBuilder.setStartIcon(icon)
- if (primaryEntry.credentialType ==
- CredentialType.PASSKEY && duplicateDisplayNamesForPasskeys[displayName]
- == true) {
- sliceBuilder.setSubtitle(primaryEntry.userName)
- }
- inlinePresentation = InlinePresentation(
- sliceBuilder.build().slice, spec, /* pinned= */ false)
+ inlinePresentation = createInlinePresentation(primaryEntry, pendingIntent, icon,
+ spec!!, duplicateDisplayNamesForPasskeys)
}
- val dropdownPresentation = RemoteViewsFactory.createDropdownPresentation(
- this, icon, primaryEntry)
- i++
+ var dropdownPresentation: RemoteViews? = null
+ if (i < lastDropdownDatasetIndex) {
+ dropdownPresentation = RemoteViewsFactory
+ .createDropdownPresentation(this, icon, primaryEntry)
+ }
val dataSetBuilder = Dataset.Builder()
val presentationBuilder = Presentations.Builder()
- .setMenuPresentation(dropdownPresentation)
+ if (dropdownPresentation != null) {
+ presentationBuilder.setMenuPresentation(dropdownPresentation)
+ }
if (inlinePresentation != null) {
presentationBuilder.setInlinePresentation(inlinePresentation)
}
@@ -398,6 +388,12 @@
.setAuthenticationExtras(fillInIntent.extras)
.build())
datasetAdded = true
+ i++
+
+ if (i == lastDropdownDatasetIndex && bottomSheetPendingIntent != null) {
+ addDropdownMoreOptionsPresentation(bottomSheetPendingIntent, autofillId,
+ fillResponseBuilder)
+ }
}
val pinnedSpec = getLastInlinePresentationSpec(inlinePresentationSpecs,
inlinePresentationSpecsCount)
@@ -408,6 +404,49 @@
return datasetAdded
}
+ private fun createInlinePresentation(primaryEntry: CredentialEntryInfo,
+ pendingIntent: PendingIntent,
+ icon: Icon,
+ spec: InlinePresentationSpec,
+ duplicateDisplayNameForPasskeys: MutableMap<String, Boolean>):
+ InlinePresentation {
+ val displayName: String = if (primaryEntry.credentialType == CredentialType.PASSKEY
+ && primaryEntry.displayName != null) {
+ primaryEntry.displayName!!
+ } else {
+ primaryEntry.userName
+ }
+ val sliceBuilder = InlineSuggestionUi
+ .newContentBuilder(pendingIntent)
+ .setTitle(displayName)
+ sliceBuilder.setStartIcon(icon)
+ if (primaryEntry.credentialType ==
+ CredentialType.PASSKEY && duplicateDisplayNameForPasskeys[displayName] == true) {
+ sliceBuilder.setSubtitle(primaryEntry.userName)
+ }
+ return InlinePresentation(
+ sliceBuilder.build().slice, spec, /* pinned= */ false)
+ }
+
+ private fun addDropdownMoreOptionsPresentation(
+ bottomSheetPendingIntent: PendingIntent,
+ autofillId: AutofillId,
+ fillResponseBuilder: FillResponse.Builder) {
+ val presentationBuilder = Presentations.Builder()
+ .setMenuPresentation(RemoteViewsFactory.createMoreSignInOptionsPresentation(this))
+
+ fillResponseBuilder.addDataset(
+ Dataset.Builder()
+ .setField(
+ autofillId,
+ Field.Builder().setPresentations(
+ presentationBuilder.build())
+ .build())
+ .setAuthentication(bottomSheetPendingIntent.intentSender)
+ .build()
+ )
+ }
+
private fun getLastInlinePresentationSpec(
inlinePresentationSpecs: List<InlinePresentationSpec>?,
inlinePresentationSpecsCount: Int
@@ -534,9 +573,9 @@
}
private fun getCredManRequest(
- structure: AssistStructure,
- sessionId: Int,
- requestId: Int
+ structure: AssistStructure,
+ sessionId: Int,
+ requestId: Int
): GetCredentialRequest? {
val credentialOptions: MutableList<CredentialOption> = mutableListOf()
traverseStructure(structure, credentialOptions)
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/RemoteViewsFactory.kt b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/RemoteViewsFactory.kt
index e039dea..68f1c86 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/RemoteViewsFactory.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/RemoteViewsFactory.kt
@@ -44,7 +44,7 @@
if (credentialEntryInfo.credentialType == CredentialType.UNKNOWN) {
return remoteViews
}
- setRemoteViewsPaddings(remoteViews, context)
+ setRemoteViewsPaddings(remoteViews, context, /* primaryTextBottomPadding=*/0)
if (credentialEntryInfo.credentialType == CredentialType.PASSKEY) {
val displayName = credentialEntryInfo.displayName ?: credentialEntryInfo.userName
remoteViews.setTextViewText(android.R.id.text1, displayName)
@@ -81,8 +81,46 @@
return remoteViews
}
+ fun createMoreSignInOptionsPresentation(context: Context): RemoteViews {
+ var layoutId: Int = com.android.credentialmanager.R.layout
+ .credman_dropdown_bottom_sheet
+ val remoteViews = RemoteViews(context.packageName, layoutId)
+ setRemoteViewsPaddings(remoteViews, context)
+ remoteViews.setTextViewText(android.R.id.text1, ContextCompat.getString(context,
+ com.android.credentialmanager
+ .R.string.dropdown_presentation_more_sign_in_options_text))
+
+ val textColorPrimary = ContextCompat.getColor(context,
+ com.android.credentialmanager.R.color.text_primary)
+ remoteViews.setTextColor(android.R.id.text1, textColorPrimary)
+ val icon = Icon.createWithResource(context, com
+ .android.credentialmanager.R.drawable.more_horiz_24px)
+ icon.setTint(ContextCompat.getColor(context,
+ com.android.credentialmanager.R.color.sign_in_options_icon_color))
+ remoteViews.setImageViewIcon(android.R.id.icon1, icon)
+ remoteViews.setBoolean(
+ android.R.id.icon1, setAdjustViewBoundsMethodName, true);
+ remoteViews.setInt(
+ android.R.id.icon1,
+ setMaxHeightMethodName,
+ context.resources.getDimensionPixelSize(
+ com.android.credentialmanager.R.dimen.autofill_icon_size));
+ val drawableId =
+ com.android.credentialmanager.R.drawable.more_options_list_item
+ remoteViews.setInt(
+ android.R.id.content, setBackgroundResourceMethodName, drawableId);
+ return remoteViews
+ }
+
private fun setRemoteViewsPaddings(
remoteViews: RemoteViews, context: Context) {
+ val bottomPadding = context.resources.getDimensionPixelSize(
+ com.android.credentialmanager.R.dimen.autofill_view_bottom_padding)
+ setRemoteViewsPaddings(remoteViews, context, bottomPadding)
+ }
+
+ private fun setRemoteViewsPaddings(
+ remoteViews: RemoteViews, context: Context, primaryTextBottomPadding: Int) {
val leftPadding = context.resources.getDimensionPixelSize(
com.android.credentialmanager.R.dimen.autofill_view_left_padding)
val iconToTextPadding = context.resources.getDimensionPixelSize(
@@ -104,7 +142,7 @@
iconToTextPadding,
/* top=*/topPadding,
/* right=*/rightPadding,
- /* bottom=*/0)
+ primaryTextBottomPadding)
remoteViews.setViewPadding(
android.R.id.text2,
iconToTextPadding,
diff --git a/packages/SettingsLib/Android.bp b/packages/SettingsLib/Android.bp
index c2cb757..bd56aae 100644
--- a/packages/SettingsLib/Android.bp
+++ b/packages/SettingsLib/Android.bp
@@ -16,6 +16,7 @@
static_libs: [
"androidx.localbroadcastmanager_localbroadcastmanager",
"androidx.room_room-runtime",
+ "androidx.sqlite_sqlite",
"zxing-core",
"guava",
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/MoreOptions.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/MoreOptions.kt
index 84fea15..d92a863 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/MoreOptions.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/MoreOptions.kt
@@ -35,8 +35,8 @@
/**
* Scope for the children of [MoreOptionsAction].
*/
-interface MoreOptionsScope {
- fun dismiss()
+abstract class MoreOptionsScope {
+ abstract fun dismiss()
@Composable
fun MenuItem(text: String, enabled: Boolean = true, onClick: () -> Unit) {
@@ -60,7 +60,7 @@
val onDismiss = { expanded = false }
DropdownMenu(expanded = expanded, onDismissRequest = onDismiss) {
val moreOptionsScope = remember(this) {
- object : MoreOptionsScope {
+ object : MoreOptionsScope() {
override fun dismiss() {
onDismiss()
}
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppRepository.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppRepository.kt
index 81a8b324..cea3d13 100644
--- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppRepository.kt
+++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppRepository.kt
@@ -37,6 +37,7 @@
interface AppRepository {
fun loadLabel(app: ApplicationInfo): String
+ @Suppress("ABSTRACT_COMPOSABLE_DEFAULT_PARAMETER_VALUE")
@Composable
fun produceLabel(app: ApplicationInfo, isClonedAppPage: Boolean = false): State<String> {
val context = LocalContext.current
diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/scaffold/RestrictedMenuItemTest.kt b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/scaffold/RestrictedMenuItemTest.kt
index 983284c..2ccf323 100644
--- a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/scaffold/RestrictedMenuItemTest.kt
+++ b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/scaffold/RestrictedMenuItemTest.kt
@@ -130,7 +130,7 @@
}
private fun setContent(restrictions: Restrictions) {
- val fakeMoreOptionsScope = object : MoreOptionsScope {
+ val fakeMoreOptionsScope = object : MoreOptionsScope() {
override fun dismiss() {}
}
composeTestRule.setContent {
diff --git a/packages/SettingsLib/res/drawable/ic_external_display.xml b/packages/SettingsLib/res/drawable/ic_external_display.xml
new file mode 100644
index 0000000..de50de8
--- /dev/null
+++ b/packages/SettingsLib/res/drawable/ic_external_display.xml
@@ -0,0 +1,28 @@
+<!--
+ ~ Copyright (C) 2024 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="25dp"
+ android:viewportWidth="24"
+ android:viewportHeight="25">
+ <group>
+ <clip-path
+ android:pathData="M0,0.307h24v24h-24z"/>
+ <path
+ android:pathData="M8,21.307V19.307H10V17.307H4C3.45,17.307 2.975,17.115 2.575,16.732C2.192,16.332 2,15.857 2,15.307V5.307C2,4.757 2.192,4.29 2.575,3.907C2.975,3.507 3.45,3.307 4,3.307H20C20.55,3.307 21.017,3.507 21.4,3.907C21.8,4.29 22,4.757 22,5.307V15.307C22,15.857 21.8,16.332 21.4,16.732C21.017,17.115 20.55,17.307 20,17.307H14V19.307H16V21.307H8ZM4,15.307H20V5.307H4V15.307ZM4,15.307V5.307V15.307Z"
+ android:fillColor="#E5E3D6"/>
+ </group>
+</vector>
diff --git a/packages/SettingsLib/src/com/android/settingslib/applications/InterestingConfigChanges.java b/packages/SettingsLib/src/com/android/settingslib/applications/InterestingConfigChanges.java
index 5d520ce..7e2d0af 100644
--- a/packages/SettingsLib/src/com/android/settingslib/applications/InterestingConfigChanges.java
+++ b/packages/SettingsLib/src/com/android/settingslib/applications/InterestingConfigChanges.java
@@ -21,6 +21,8 @@
import android.content.res.Configuration;
import android.content.res.Resources;
+import androidx.annotation.NonNull;
+
/**
* A class for applying config changes and determing if doing so resulting in any "interesting"
* changes.
@@ -48,8 +50,15 @@
*/
@SuppressLint("NewApi")
public boolean applyNewConfig(Resources res) {
+ return applyNewConfig(res.getConfiguration());
+ }
+
+ /**
+ * Applies the given config change and returns whether an "interesting" change happened.
+ */
+ public boolean applyNewConfig(@NonNull Configuration configuration) {
int configChanges = mLastConfiguration.updateFrom(
- Configuration.generateDelta(mLastConfiguration, res.getConfiguration()));
+ Configuration.generateDelta(mLastConfiguration, configuration));
return (configChanges & (mFlags)) != 0;
}
}
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/HapClientProfile.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/HapClientProfile.java
index cf4d6be..0613676 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/HapClientProfile.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/HapClientProfile.java
@@ -99,6 +99,8 @@
device.refresh();
}
+ // Check current list of CachedDevices to see if any are hearing aid devices.
+ mDeviceManager.updateHearingAidsDevices();
mIsProfileReady = true;
mProfileManager.callServiceConnectedListeners();
}
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidDeviceManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidDeviceManager.java
index 3a15b71..9fd174d 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidDeviceManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidDeviceManager.java
@@ -15,8 +15,8 @@
*/
package com.android.settingslib.bluetooth;
-import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothHearingAid;
+import android.bluetooth.BluetoothLeAudio;
import android.bluetooth.BluetoothProfile;
import android.bluetooth.BluetoothUuid;
import android.bluetooth.le.ScanFilter;
@@ -68,14 +68,9 @@
void initHearingAidDeviceIfNeeded(CachedBluetoothDevice newDevice,
List<ScanFilter> leScanFilters) {
- long hiSyncId = getHiSyncId(newDevice.getDevice());
- if (isValidHiSyncId(hiSyncId)) {
- // Once hiSyncId is valid, assign hearing aid info
- final HearingAidInfo.Builder infoBuilder = new HearingAidInfo.Builder()
- .setAshaDeviceSide(getDeviceSide(newDevice.getDevice()))
- .setAshaDeviceMode(getDeviceMode(newDevice.getDevice()))
- .setHiSyncId(hiSyncId);
- newDevice.setHearingAidInfo(infoBuilder.build());
+ HearingAidInfo info = generateHearingAidInfo(newDevice);
+ if (info != null) {
+ newDevice.setHearingAidInfo(info);
} else if (leScanFilters != null && !newDevice.isHearingAidDevice()) {
// If the device is added with hearing aid scan filter during pairing, set an empty
// hearing aid info to indicate it's a hearing aid device. The info will be updated
@@ -94,38 +89,6 @@
}
}
- private long getHiSyncId(BluetoothDevice device) {
- final LocalBluetoothProfileManager profileManager = mBtManager.getProfileManager();
- final HearingAidProfile profileProxy = profileManager.getHearingAidProfile();
- if (profileProxy == null) {
- return BluetoothHearingAid.HI_SYNC_ID_INVALID;
- }
-
- return profileProxy.getHiSyncId(device);
- }
-
- private int getDeviceSide(BluetoothDevice device) {
- final LocalBluetoothProfileManager profileManager = mBtManager.getProfileManager();
- final HearingAidProfile profileProxy = profileManager.getHearingAidProfile();
- if (profileProxy == null) {
- Log.w(TAG, "HearingAidProfile is not supported and not ready to fetch device side");
- return HearingAidProfile.DeviceSide.SIDE_INVALID;
- }
-
- return profileProxy.getDeviceSide(device);
- }
-
- private int getDeviceMode(BluetoothDevice device) {
- final LocalBluetoothProfileManager profileManager = mBtManager.getProfileManager();
- final HearingAidProfile profileProxy = profileManager.getHearingAidProfile();
- if (profileProxy == null) {
- Log.w(TAG, "HearingAidProfile is not supported and not ready to fetch device mode");
- return HearingAidProfile.DeviceMode.MODE_INVALID;
- }
-
- return profileProxy.getDeviceMode(device);
- }
-
boolean setSubDeviceIfNeeded(CachedBluetoothDevice newDevice) {
final long hiSyncId = newDevice.getHiSyncId();
if (isValidHiSyncId(hiSyncId)) {
@@ -157,21 +120,17 @@
// To collect all HearingAid devices and call #onHiSyncIdChanged to group device by HiSyncId
void updateHearingAidsDevices() {
- final Set<Long> newSyncIdSet = new HashSet<Long>();
+ final Set<Long> newSyncIdSet = new HashSet<>();
for (CachedBluetoothDevice cachedDevice : mCachedDevices) {
// Do nothing if HiSyncId has been assigned
- if (!isValidHiSyncId(cachedDevice.getHiSyncId())) {
- final long newHiSyncId = getHiSyncId(cachedDevice.getDevice());
- // Do nothing if there is no HiSyncId on Bluetooth device
- if (isValidHiSyncId(newHiSyncId)) {
- // Once hiSyncId is valid, assign hearing aid info
- final HearingAidInfo.Builder infoBuilder = new HearingAidInfo.Builder()
- .setAshaDeviceSide(getDeviceSide(cachedDevice.getDevice()))
- .setAshaDeviceMode(getDeviceMode(cachedDevice.getDevice()))
- .setHiSyncId(newHiSyncId);
- cachedDevice.setHearingAidInfo(infoBuilder.build());
-
- newSyncIdSet.add(newHiSyncId);
+ if (isValidHiSyncId(cachedDevice.getHiSyncId())) {
+ continue;
+ }
+ HearingAidInfo info = generateHearingAidInfo(cachedDevice);
+ if (info != null) {
+ cachedDevice.setHearingAidInfo(info);
+ if (isValidHiSyncId(info.getHiSyncId())) {
+ newSyncIdSet.add(info.getHiSyncId());
}
}
}
@@ -378,6 +337,54 @@
return null;
}
+ private boolean isLeAudioHearingAid(CachedBluetoothDevice cachedDevice) {
+ List<LocalBluetoothProfile> profiles = cachedDevice.getProfiles();
+ boolean supportLeAudio = profiles.stream().anyMatch(p -> p instanceof LeAudioProfile);
+ boolean supportHapClient = profiles.stream().anyMatch(p -> p instanceof HapClientProfile);
+ return supportLeAudio && supportHapClient;
+ }
+
+ private boolean isAshaHearingAid(CachedBluetoothDevice cachedDevice) {
+ return cachedDevice.getProfiles().stream().anyMatch(p -> p instanceof HearingAidProfile);
+ }
+
+ private HearingAidInfo generateHearingAidInfo(CachedBluetoothDevice cachedDevice) {
+ final LocalBluetoothProfileManager profileManager = mBtManager.getProfileManager();
+ if (isAshaHearingAid(cachedDevice)) {
+ final HearingAidProfile asha = profileManager.getHearingAidProfile();
+ if (asha == null) {
+ Log.w(TAG, "HearingAidProfile is not supported on this device");
+ } else {
+ long hiSyncId = asha.getHiSyncId(cachedDevice.getDevice());
+ if (isValidHiSyncId(hiSyncId)) {
+ final HearingAidInfo.Builder infoBuilder = new HearingAidInfo.Builder()
+ .setAshaDeviceSide(asha.getDeviceSide(cachedDevice.getDevice()))
+ .setAshaDeviceMode(asha.getDeviceMode(cachedDevice.getDevice()))
+ .setHiSyncId(hiSyncId);
+ return infoBuilder.build();
+ }
+ }
+ }
+ if (isLeAudioHearingAid(cachedDevice)) {
+ final HapClientProfile hapClientProfile = profileManager.getHapClientProfile();
+ final LeAudioProfile leAudioProfile = profileManager.getLeAudioProfile();
+ if (hapClientProfile == null || leAudioProfile == null) {
+ Log.w(TAG, "HapClientProfile or LeAudioProfile is not supported on this device");
+ } else {
+ int audioLocation = leAudioProfile.getAudioLocation(cachedDevice.getDevice());
+ int hearingAidType = hapClientProfile.getHearingAidType(cachedDevice.getDevice());
+ if (audioLocation != BluetoothLeAudio.AUDIO_LOCATION_INVALID
+ && hearingAidType != HapClientProfile.HearingAidType.TYPE_INVALID) {
+ final HearingAidInfo.Builder infoBuilder = new HearingAidInfo.Builder()
+ .setLeAudioLocation(audioLocation)
+ .setHapDeviceType(hearingAidType);
+ return infoBuilder.build();
+ }
+ }
+ }
+ return null;
+ }
+
private void log(String msg) {
if (DEBUG) {
Log.d(TAG, msg);
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidProfile.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidProfile.java
index 14fab16..f2450de 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidProfile.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidProfile.java
@@ -109,7 +109,7 @@
device.refresh();
}
- // Check current list of CachedDevices to see if any are Hearing Aid devices.
+ // Check current list of CachedDevices to see if any are hearing aid devices.
mDeviceManager.updateHearingAidsDevices();
mIsProfileReady = true;
mProfileManager.callServiceConnectedListeners();
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LeAudioProfile.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LeAudioProfile.java
index 931a6f1..6be4336 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LeAudioProfile.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LeAudioProfile.java
@@ -83,6 +83,8 @@
device.refresh();
}
+ // Check current list of CachedDevices to see if any are hearing aid devices.
+ mDeviceManager.updateHearingAidsDevices();
mProfileManager.callServiceConnectedListeners();
mIsProfileReady = true;
}
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java
index 9348705..1d2f790 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java
@@ -759,6 +759,20 @@
}
/**
+ * Update the LE Broadcast by calling {@link BluetoothLeBroadcast#updateBroadcast(int,
+ * BluetoothLeAudioContentMetadata)}, currently only updates programInfo.
+ */
+ public void updateBroadcast() {
+ if (mServiceBroadcast == null) {
+ Log.d(TAG, "The BluetoothLeBroadcast is null when updating the broadcast.");
+ return;
+ }
+ String programInfo = getProgramInfo();
+ mBluetoothLeAudioContentMetadata = mBuilder.setProgramInfo(programInfo).build();
+ mServiceBroadcast.updateBroadcast(mBroadcastId, mBluetoothLeAudioContentMetadata);
+ }
+
+ /**
* Register Broadcast Callbacks to track its state and receivers
*
* @param executor Executor object for callback
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/DeviceIconUtil.java b/packages/SettingsLib/src/com/android/settingslib/media/DeviceIconUtil.java
index cf224dc..3de4933 100644
--- a/packages/SettingsLib/src/com/android/settingslib/media/DeviceIconUtil.java
+++ b/packages/SettingsLib/src/com/android/settingslib/media/DeviceIconUtil.java
@@ -71,15 +71,15 @@
new Device(
AudioDeviceInfo.TYPE_HDMI,
MediaRoute2Info.TYPE_HDMI,
- mIsTv ? R.drawable.ic_tv : R.drawable.ic_headphone),
+ mIsTv ? R.drawable.ic_tv : R.drawable.ic_external_display),
new Device(
AudioDeviceInfo.TYPE_HDMI_ARC,
MediaRoute2Info.TYPE_HDMI_ARC,
- mIsTv ? R.drawable.ic_hdmi : R.drawable.ic_headphone),
+ mIsTv ? R.drawable.ic_hdmi : R.drawable.ic_external_display),
new Device(
AudioDeviceInfo.TYPE_HDMI_EARC,
MediaRoute2Info.TYPE_HDMI_EARC,
- mIsTv ? R.drawable.ic_hdmi : R.drawable.ic_headphone),
+ mIsTv ? R.drawable.ic_hdmi : R.drawable.ic_external_display),
new Device(
AudioDeviceInfo.TYPE_WIRED_HEADSET,
MediaRoute2Info.TYPE_WIRED_HEADSET,
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/RouterInfoMediaManager.java b/packages/SettingsLib/src/com/android/settingslib/media/RouterInfoMediaManager.java
index 8a122fc..aef09ac 100644
--- a/packages/SettingsLib/src/com/android/settingslib/media/RouterInfoMediaManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/media/RouterInfoMediaManager.java
@@ -26,11 +26,13 @@
import android.media.RouteDiscoveryPreference;
import android.media.RouteListingPreference;
import android.media.RoutingSessionInfo;
+import android.os.Process;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import com.android.media.flags.Flags;
import com.android.settingslib.bluetooth.LocalBluetoothManager;
import java.util.ArrayList;
@@ -62,21 +64,33 @@
refreshDevices();
};
- // TODO: b/192657812 - Create factory method in InfoMediaManager to return
- // RouterInfoMediaManager or ManagerInfoMediaManager based on flag.
+ // TODO (b/321969740): Plumb target UserHandle between UMO and RouterInfoMediaManager.
public RouterInfoMediaManager(
Context context,
String packageName,
Notification notification,
- LocalBluetoothManager localBluetoothManager) throws PackageNotAvailableException {
+ LocalBluetoothManager localBluetoothManager)
+ throws PackageNotAvailableException {
super(context, packageName, notification, localBluetoothManager);
- mRouter = MediaRouter2.getInstance(context, packageName);
+ MediaRouter2 router = null;
- if (mRouter == null) {
+ if (Flags.enableCrossUserRoutingInMediaRouter2()) {
+ try {
+ router = MediaRouter2.getInstance(context, packageName, Process.myUserHandle());
+ } catch (IllegalArgumentException ex) {
+ // Do nothing
+ }
+ } else {
+ router = MediaRouter2.getInstance(context, packageName);
+ }
+ if (router == null) {
throw new PackageNotAvailableException(
"Package name " + packageName + " does not exist.");
}
+ // We have to defer initialization because mRouter is final.
+ mRouter = router;
+
mRouterManager = MediaRouter2Manager.getInstance(context);
}
diff --git a/packages/SettingsLib/src/com/android/settingslib/users/EditUserInfoController.java b/packages/SettingsLib/src/com/android/settingslib/users/EditUserInfoController.java
index cd5f597..b015b2b 100644
--- a/packages/SettingsLib/src/com/android/settingslib/users/EditUserInfoController.java
+++ b/packages/SettingsLib/src/com/android/settingslib/users/EditUserInfoController.java
@@ -234,6 +234,6 @@
EditUserPhotoController createEditUserPhotoController(Activity activity,
ActivityStarter activityStarter, ImageView userPhotoView) {
return new EditUserPhotoController(activity, activityStarter, userPhotoView,
- mSavedPhoto, mSavedDrawable, mFileAuthority);
+ mSavedPhoto, mSavedDrawable, mFileAuthority, false);
}
}
diff --git a/packages/SettingsLib/src/com/android/settingslib/users/EditUserPhotoController.java b/packages/SettingsLib/src/com/android/settingslib/users/EditUserPhotoController.java
index e83b9bc..b2de5a9 100644
--- a/packages/SettingsLib/src/com/android/settingslib/users/EditUserPhotoController.java
+++ b/packages/SettingsLib/src/com/android/settingslib/users/EditUserPhotoController.java
@@ -62,6 +62,7 @@
private static final String AVATAR_PICKER_ACTION = "com.android.avatarpicker"
+ ".FULL_SCREEN_ACTIVITY";
+ static final String EXTRA_IS_USER_NEW = "is_user_new";
private final Activity mActivity;
private final ActivityStarter mActivityStarter;
@@ -72,9 +73,13 @@
private Bitmap mNewUserPhotoBitmap;
private Drawable mNewUserPhotoDrawable;
private String mCachedDrawablePath;
-
public EditUserPhotoController(Activity activity, ActivityStarter activityStarter,
ImageView view, Bitmap savedBitmap, Drawable savedDrawable, String fileAuthority) {
+ this(activity, activityStarter, view, savedBitmap, savedDrawable, fileAuthority, true);
+ }
+ public EditUserPhotoController(Activity activity, ActivityStarter activityStarter,
+ ImageView view, Bitmap savedBitmap, Drawable savedDrawable, String fileAuthority,
+ boolean isUserNew) {
mActivity = activity;
mActivityStarter = activityStarter;
mFileAuthority = fileAuthority;
@@ -82,7 +87,7 @@
mImagesDir = new File(activity.getCacheDir(), IMAGES_DIR);
mImagesDir.mkdir();
mImageView = view;
- mImageView.setOnClickListener(v -> showAvatarPicker());
+ mImageView.setOnClickListener(v -> showAvatarPicker(isUserNew));
mNewUserPhotoBitmap = savedBitmap;
mNewUserPhotoDrawable = savedDrawable;
@@ -117,11 +122,12 @@
return mNewUserPhotoDrawable;
}
- private void showAvatarPicker() {
+ private void showAvatarPicker(boolean isUserNew) {
Intent intent;
if (Flags.avatarSync()) {
intent = new Intent(AVATAR_PICKER_ACTION);
intent.addCategory(Intent.CATEGORY_DEFAULT);
+ intent.putExtra(EXTRA_IS_USER_NEW, isUserNew);
} else {
intent = new Intent(mImageView.getContext(), AvatarPickerActivity.class);
}
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/HearingAidDeviceManagerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/HearingAidDeviceManagerTest.java
index e7487e8..aa5a298 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/HearingAidDeviceManagerTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/HearingAidDeviceManagerTest.java
@@ -15,6 +15,15 @@
*/
package com.android.settingslib.bluetooth;
+import static android.bluetooth.BluetoothHearingAid.HI_SYNC_ID_INVALID;
+import static android.bluetooth.BluetoothLeAudio.AUDIO_LOCATION_FRONT_LEFT;
+import static android.bluetooth.BluetoothLeAudio.AUDIO_LOCATION_INVALID;
+
+import static com.android.settingslib.bluetooth.HapClientProfile.HearingAidType.TYPE_BINAURAL;
+import static com.android.settingslib.bluetooth.HapClientProfile.HearingAidType.TYPE_INVALID;
+import static com.android.settingslib.bluetooth.HearingAidProfile.DeviceMode.MODE_BINAURAL;
+import static com.android.settingslib.bluetooth.HearingAidProfile.DeviceSide.SIDE_RIGHT;
+
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any;
@@ -32,7 +41,6 @@
import android.bluetooth.BluetoothClass;
import android.bluetooth.BluetoothDevice;
-import android.bluetooth.BluetoothHearingAid;
import android.bluetooth.BluetoothProfile;
import android.bluetooth.BluetoothUuid;
import android.bluetooth.le.ScanFilter;
@@ -92,6 +100,10 @@
@Mock
private HearingAidProfile mHearingAidProfile;
@Mock
+ private LeAudioProfile mLeAudioProfile;
+ @Mock
+ private HapClientProfile mHapClientProfile;
+ @Mock
private AudioProductStrategy mAudioStrategy;
@Mock
private BluetoothDevice mDevice1;
@@ -123,6 +135,8 @@
when(mLocalBluetoothManager.getEventManager()).thenReturn(mBluetoothEventManager);
when(mLocalBluetoothManager.getProfileManager()).thenReturn(mLocalProfileManager);
when(mLocalProfileManager.getHearingAidProfile()).thenReturn(mHearingAidProfile);
+ when(mLocalProfileManager.getLeAudioProfile()).thenReturn(mLeAudioProfile);
+ when(mLocalProfileManager.getHapClientProfile()).thenReturn(mHapClientProfile);
when(mAudioStrategy.getAudioAttributesForLegacyStreamType(
AudioManager.STREAM_MUSIC))
.thenReturn((new AudioAttributes.Builder()).build());
@@ -140,34 +154,43 @@
}
/**
- * Test initHearingAidDeviceIfNeeded, set HearingAid's information, including HiSyncId,
- * deviceSide, deviceMode.
+ * Test initHearingAidDeviceIfNeeded
+ *
+ * Conditions:
+ * 1) ASHA hearing aid
+ * 2) Valid HiSyncId
+ * Result:
+ * Set hearing aid info to the device.
*/
@Test
- public void initHearingAidDeviceIfNeeded_validHiSyncId_setHearingAidInfo() {
+ public void initHearingAidDeviceIfNeeded_asha_validHiSyncId_setHearingAidInfo() {
+ when(mCachedDevice1.getProfiles()).thenReturn(List.of(mHearingAidProfile));
when(mHearingAidProfile.getHiSyncId(mDevice1)).thenReturn(HISYNCID1);
- when(mHearingAidProfile.getDeviceMode(mDevice1)).thenReturn(
- HearingAidProfile.DeviceMode.MODE_BINAURAL);
- when(mHearingAidProfile.getDeviceSide(mDevice1)).thenReturn(
- HearingAidProfile.DeviceSide.SIDE_RIGHT);
+ when(mHearingAidProfile.getDeviceMode(mDevice1)).thenReturn(MODE_BINAURAL);
+ when(mHearingAidProfile.getDeviceSide(mDevice1)).thenReturn(SIDE_RIGHT);
assertThat(mCachedDevice1.getHiSyncId()).isNotEqualTo(HISYNCID1);
mHearingAidDeviceManager.initHearingAidDeviceIfNeeded(mCachedDevice1, null);
assertThat(mCachedDevice1.getHiSyncId()).isEqualTo(HISYNCID1);
+ assertThat(mCachedDevice1.getDeviceSide()).isEqualTo(HearingAidInfo.DeviceSide.SIDE_RIGHT);
assertThat(mCachedDevice1.getDeviceMode()).isEqualTo(
HearingAidInfo.DeviceMode.MODE_BINAURAL);
- assertThat(mCachedDevice1.getDeviceSide()).isEqualTo(
- HearingAidInfo.DeviceSide.SIDE_RIGHT);
}
/**
- * Test initHearingAidDeviceIfNeeded, an invalid HiSyncId will not be assigned
+ * Test initHearingAidDeviceIfNeeded
+ *
+ * Conditions:
+ * 1) ASHA hearing aid
+ * 2) Invalid HiSyncId
+ * Result:
+ * Do not set hearing aid info to the device.
*/
@Test
- public void initHearingAidDeviceIfNeeded_invalidHiSyncId_notToSetHearingAidInfo() {
- when(mHearingAidProfile.getHiSyncId(mDevice1)).thenReturn(
- BluetoothHearingAid.HI_SYNC_ID_INVALID);
+ public void initHearingAidDeviceIfNeeded_asha_invalidHiSyncId_notToSetHearingAidInfo() {
+ when(mCachedDevice1.getProfiles()).thenReturn(List.of(mHearingAidProfile));
+ when(mHearingAidProfile.getHiSyncId(mDevice1)).thenReturn(HI_SYNC_ID_INVALID);
mHearingAidDeviceManager.initHearingAidDeviceIfNeeded(mCachedDevice1, null);
@@ -175,34 +198,89 @@
}
/**
- * Test initHearingAidDeviceIfNeeded, an invalid HiSyncId and hearing aid scan filter, set an
- * empty hearing aid info on the device.
+ * Test initHearingAidDeviceIfNeeded
+ *
+ * Conditions:
+ * 1) ASHA hearing aid
+ * 2) Invalid HiSyncId
+ * 3) ASHA uuid scan filter
+ * Result:
+ * Set an empty hearing aid info to the device.
*/
@Test
- public void initHearingAidDeviceIfNeeded_hearingAidScanFilter_setHearingAidInfo() {
- when(mHearingAidProfile.getHiSyncId(mDevice1)).thenReturn(
- BluetoothHearingAid.HI_SYNC_ID_INVALID);
+ public void initHearingAidDeviceIfNeeded_asha_scanFilterNotNull_setEmptyHearingAidInfo() {
+ when(mCachedDevice1.getProfiles()).thenReturn(List.of(mHearingAidProfile));
+ when(mHearingAidProfile.getHiSyncId(mDevice1)).thenReturn(HI_SYNC_ID_INVALID);
final ScanFilter scanFilter = new ScanFilter.Builder()
.setServiceData(BluetoothUuid.HEARING_AID, new byte[]{0}).build();
mHearingAidDeviceManager.initHearingAidDeviceIfNeeded(mCachedDevice1, List.of(scanFilter));
- assertThat(mCachedDevice1.isHearingAidDevice()).isTrue();
+ verify(mCachedDevice1).setHearingAidInfo(new HearingAidInfo.Builder().build());
}
/**
- * Test initHearingAidDeviceIfNeeded, an invalid HiSyncId and random scan filter, not to set
- * hearing aid info on the device.
+ * Test initHearingAidDeviceIfNeeded
+ *
+ * Conditions:
+ * 1) Asha hearing aid
+ * 2) Invalid HiSyncId
+ * 3) Random scan filter
+ * Result:
+ * Do not set hearing aid info to the device.
*/
@Test
- public void initHearingAidDeviceIfNeeded_randomScanFilter_setHearingAidInfo() {
- when(mHearingAidProfile.getHiSyncId(mDevice1)).thenReturn(
- BluetoothHearingAid.HI_SYNC_ID_INVALID);
+ public void initHearingAidDeviceIfNeeded_asha_randomScanFilter_notToSetHearingAidInfo() {
+ when(mCachedDevice1.getProfiles()).thenReturn(List.of(mHearingAidProfile));
+ when(mHearingAidProfile.getHiSyncId(mDevice1)).thenReturn(HI_SYNC_ID_INVALID);
final ScanFilter scanFilter = new ScanFilter.Builder().build();
mHearingAidDeviceManager.initHearingAidDeviceIfNeeded(mCachedDevice1, List.of(scanFilter));
- assertThat(mCachedDevice1.isHearingAidDevice()).isFalse();
+ verify(mCachedDevice1, never()).setHearingAidInfo(any(HearingAidInfo.class));
+ }
+
+ /**
+ * Test initHearingAidDeviceIfNeeded
+ *
+ * Conditions:
+ * 1) LeAudio hearing aid
+ * 2) Valid audio location and device type
+ * Result:
+ * Set hearing aid info to the device.
+ */
+ @Test
+ public void initHearingAidDeviceIfNeeded_leAudio_validInfo_setHearingAidInfo() {
+ when(mCachedDevice1.getProfiles()).thenReturn(List.of(mLeAudioProfile, mHapClientProfile));
+ when(mLeAudioProfile.getAudioLocation(mDevice1)).thenReturn(AUDIO_LOCATION_FRONT_LEFT);
+ when(mHapClientProfile.getHearingAidType(mDevice1)).thenReturn(TYPE_BINAURAL);
+
+ mHearingAidDeviceManager.initHearingAidDeviceIfNeeded(mCachedDevice1, null);
+
+ verify(mCachedDevice1).setHearingAidInfo(any(HearingAidInfo.class));
+ assertThat(mCachedDevice1.getDeviceSide()).isEqualTo(HearingAidInfo.DeviceSide.SIDE_LEFT);
+ assertThat(mCachedDevice1.getDeviceMode()).isEqualTo(
+ HearingAidInfo.DeviceMode.MODE_BINAURAL);
+ }
+
+ /**
+ * Test initHearingAidDeviceIfNeeded
+ *
+ * Conditions:
+ * 1) LeAudio hearing aid
+ * 2) Invalid audio location and device type
+ * Result:
+ * Do not set hearing aid info to the device.
+ */
+ @Test
+ public void initHearingAidDeviceIfNeeded_leAudio_invalidInfo_notToSetHearingAidInfo() {
+ when(mCachedDevice1.getProfiles()).thenReturn(List.of(mLeAudioProfile, mHapClientProfile));
+ when(mLeAudioProfile.getAudioLocation(mDevice1)).thenReturn(AUDIO_LOCATION_INVALID);
+ when(mHapClientProfile.getHearingAidType(mDevice1)).thenReturn(TYPE_INVALID);
+
+ mHearingAidDeviceManager.initHearingAidDeviceIfNeeded(mCachedDevice1, null);
+
+ verify(mCachedDevice1, never()).setHearingAidInfo(any(HearingAidInfo.class));
}
/**
@@ -234,13 +312,20 @@
}
/**
- * Test updateHearingAidsDevices, to link two devices with the same HiSyncId.
- * When first paired devices is connected and second paired device is disconnected, first
- * paired device would be set as main device and second device will be removed from
- * CachedDevices list.
+ * Test updateHearingAidsDevices
+ *
+ * Conditions:
+ * 1) Two ASHA hearing aids with the same HiSyncId
+ * 2) First paired devices is connected
+ * 3) Second paired device is disconnected
+ * Result:
+ * First paired device would be set as main device and second paired device will be set
+ * as sub device and removed from CachedDevices list.
*/
@Test
- public void updateHearingAidsDevices_firstPairedDevicesConnected_verifySubDevice() {
+ public void updateHearingAidsDevices_asha_firstPairedDevicesConnected_verifySubDevice() {
+ when(mCachedDevice1.getProfiles()).thenReturn(List.of(mHearingAidProfile));
+ when(mCachedDevice2.getProfiles()).thenReturn(List.of(mHearingAidProfile));
when(mHearingAidProfile.getHiSyncId(mDevice1)).thenReturn(HISYNCID1);
when(mHearingAidProfile.getHiSyncId(mDevice2)).thenReturn(HISYNCID1);
when(mCachedDevice1.isConnected()).thenReturn(true);
@@ -257,13 +342,20 @@
}
/**
- * Test updateHearingAidsDevices, to link two devices with the same HiSyncId.
- * When second paired devices is connected and first paired device is disconnected, second
- * paired device would be set as main device and first device will be removed from
- * CachedDevices list.
+ * Test updateHearingAidsDevices
+ *
+ * Conditions:
+ * 1) Two ASHA hearing aids with the same HiSyncId
+ * 2) First paired devices is disconnected
+ * 3) Second paired device is connected
+ * Result:
+ * Second paired device would be set as main device and first paired device will be set
+ * as sub device and removed from CachedDevices list.
*/
@Test
- public void updateHearingAidsDevices_secondPairedDeviceConnected_verifySubDevice() {
+ public void updateHearingAidsDevices_asha_secondPairedDeviceConnected_verifySubDevice() {
+ when(mCachedDevice1.getProfiles()).thenReturn(List.of(mHearingAidProfile));
+ when(mCachedDevice2.getProfiles()).thenReturn(List.of(mHearingAidProfile));
when(mHearingAidProfile.getHiSyncId(mDevice1)).thenReturn(HISYNCID1);
when(mHearingAidProfile.getHiSyncId(mDevice2)).thenReturn(HISYNCID1);
when(mCachedDevice1.isConnected()).thenReturn(false);
@@ -280,12 +372,20 @@
}
/**
- * Test updateHearingAidsDevices, to link two devices with the same HiSyncId.
- * When both devices are connected, to build up main and sub relationship and to remove sub
- * device from CachedDevices list.
+ * Test updateHearingAidsDevices
+ *
+ * Conditions:
+ * 1) Two ASHA hearing aids with the same HiSyncId
+ * 2) First paired devices is connected
+ * 3) Second paired device is connected
+ * Result:
+ * First paired device would be set as main device and second paired device will be set
+ * as sub device and removed from CachedDevices list.
*/
@Test
- public void updateHearingAidsDevices_BothConnected_verifySubDevice() {
+ public void updateHearingAidsDevices_asha_bothConnected_verifySubDevice() {
+ when(mCachedDevice1.getProfiles()).thenReturn(List.of(mHearingAidProfile));
+ when(mCachedDevice2.getProfiles()).thenReturn(List.of(mHearingAidProfile));
when(mHearingAidProfile.getHiSyncId(mDevice1)).thenReturn(HISYNCID1);
when(mHearingAidProfile.getHiSyncId(mDevice2)).thenReturn(HISYNCID1);
when(mCachedDevice1.isConnected()).thenReturn(true);
@@ -302,46 +402,64 @@
}
/**
- * Test updateHearingAidsDevices, dispatch callback
+ * Test updateHearingAidsDevices
+ *
+ * Conditions:
+ * 1) Two ASHA hearing aids with the same HiSyncId
+ * Result:
+ * Dispatch device removed callback
*/
@Test
- public void updateHearingAidsDevices_dispatchDeviceRemovedCallback() {
+ public void updateHearingAidsDevices_asha_dispatchDeviceRemovedCallback() {
+ when(mCachedDevice1.getProfiles()).thenReturn(List.of(mHearingAidProfile));
+ when(mCachedDevice2.getProfiles()).thenReturn(List.of(mHearingAidProfile));
when(mHearingAidProfile.getHiSyncId(mDevice1)).thenReturn(HISYNCID1);
when(mHearingAidProfile.getHiSyncId(mDevice2)).thenReturn(HISYNCID1);
mCachedDeviceManager.mCachedDevices.add(mCachedDevice1);
mCachedDeviceManager.mCachedDevices.add(mCachedDevice2);
+
mHearingAidDeviceManager.updateHearingAidsDevices();
verify(mBluetoothEventManager).dispatchDeviceRemoved(mCachedDevice1);
}
/**
- * Test updateHearingAidsDevices, do nothing when HiSyncId is invalid
+ * Test updateHearingAidsDevices
+ *
+ * Conditions:
+ * 1) Two ASHA hearing aids with invalid HiSyncId
+ * Result:
+ * Do nothing
*/
@Test
- public void updateHearingAidsDevices_invalidHiSyncId_doNothing() {
- when(mHearingAidProfile.getHiSyncId(mDevice1)).
- thenReturn(BluetoothHearingAid.HI_SYNC_ID_INVALID);
- when(mHearingAidProfile.getHiSyncId(mDevice2)).
- thenReturn(BluetoothHearingAid.HI_SYNC_ID_INVALID);
+ public void updateHearingAidsDevices_asha_invalidHiSyncId_doNothing() {
+ when(mCachedDevice1.getProfiles()).thenReturn(List.of(mHearingAidProfile));
+ when(mCachedDevice2.getProfiles()).thenReturn(List.of(mHearingAidProfile));
+ when(mHearingAidProfile.getHiSyncId(mDevice1)).thenReturn(HI_SYNC_ID_INVALID);
+ when(mHearingAidProfile.getHiSyncId(mDevice2)).thenReturn(HI_SYNC_ID_INVALID);
mCachedDeviceManager.mCachedDevices.add(mCachedDevice1);
mCachedDeviceManager.mCachedDevices.add(mCachedDevice2);
+
mHearingAidDeviceManager.updateHearingAidsDevices();
verify(mHearingAidDeviceManager, never()).onHiSyncIdChanged(anyLong());
}
/**
- * Test updateHearingAidsDevices, set HearingAid's information, including HiSyncId, deviceSide,
- * deviceMode.
+ * Test updateHearingAidsDevices
+ *
+ * Conditions:
+ * 1) ASHA hearing aids
+ * 2) Valid HiSync Id
+ * Result:
+ * Set hearing aid info to the device.
*/
@Test
- public void updateHearingAidsDevices_validHiSyncId_setHearingAidInfos() {
+ public void updateHearingAidsDevices_asha_validHiSyncId_setHearingAidInfo() {
+ when(mCachedDevice1.getProfiles()).thenReturn(List.of(mHearingAidProfile));
when(mHearingAidProfile.getHiSyncId(mDevice1)).thenReturn(HISYNCID1);
- when(mHearingAidProfile.getDeviceMode(mDevice1)).thenReturn(
- HearingAidProfile.DeviceMode.MODE_BINAURAL);
- when(mHearingAidProfile.getDeviceSide(mDevice1)).thenReturn(
- HearingAidProfile.DeviceSide.SIDE_RIGHT);
+ when(mHearingAidProfile.getDeviceMode(mDevice1)).thenReturn(MODE_BINAURAL);
+ when(mHearingAidProfile.getDeviceSide(mDevice1)).thenReturn(SIDE_RIGHT);
mCachedDeviceManager.mCachedDevices.add(mCachedDevice1);
mHearingAidDeviceManager.updateHearingAidsDevices();
@@ -355,6 +473,51 @@
}
/**
+ * Test updateHearingAidsDevices
+ *
+ * Conditions:
+ * 1) LeAudio hearing aid
+ * 2) Valid audio location and device type
+ * Result:
+ * Set hearing aid info to the device.
+ */
+ @Test
+ public void updateHearingAidsDevices_leAudio_validInfo_setHearingAidInfo() {
+ when(mCachedDevice1.getProfiles()).thenReturn(List.of(mLeAudioProfile, mHapClientProfile));
+ when(mLeAudioProfile.getAudioLocation(mDevice1)).thenReturn(AUDIO_LOCATION_FRONT_LEFT);
+ when(mHapClientProfile.getHearingAidType(mDevice1)).thenReturn(TYPE_BINAURAL);
+ mCachedDeviceManager.mCachedDevices.add(mCachedDevice1);
+
+ mHearingAidDeviceManager.updateHearingAidsDevices();
+
+ verify(mCachedDevice1).setHearingAidInfo(any(HearingAidInfo.class));
+ assertThat(mCachedDevice1.getDeviceSide()).isEqualTo(HearingAidInfo.DeviceSide.SIDE_LEFT);
+ assertThat(mCachedDevice1.getDeviceMode()).isEqualTo(
+ HearingAidInfo.DeviceMode.MODE_BINAURAL);
+ }
+
+ /**
+ * Test updateHearingAidsDevices
+ *
+ * Conditions:
+ * 1) LeAudio hearing aid
+ * 2) Invalid audio location and device type
+ * Result:
+ * Do not set hearing aid info to the device.
+ */
+ @Test
+ public void updateHearingAidsDevices_leAudio_invalidInfo_notToSetHearingAidInfo() {
+ when(mCachedDevice1.getProfiles()).thenReturn(List.of(mLeAudioProfile, mHapClientProfile));
+ when(mLeAudioProfile.getAudioLocation(mDevice1)).thenReturn(AUDIO_LOCATION_INVALID);
+ when(mHapClientProfile.getHearingAidType(mDevice1)).thenReturn(TYPE_INVALID);
+ mCachedDeviceManager.mCachedDevices.add(mCachedDevice1);
+
+ mHearingAidDeviceManager.updateHearingAidsDevices();
+
+ verify(mCachedDevice1, never()).setHearingAidInfo(any(HearingAidInfo.class));
+ }
+
+ /**
* Test onProfileConnectionStateChangedIfProcessed.
* When first hearing aid device is connected, to process it same as other generic devices.
* No need to process it.
diff --git a/packages/SettingsProvider/res/values/defaults.xml b/packages/SettingsProvider/res/values/defaults.xml
index 89a8dd9..17d9f1b 100644
--- a/packages/SettingsProvider/res/values/defaults.xml
+++ b/packages/SettingsProvider/res/values/defaults.xml
@@ -335,4 +335,7 @@
<!-- Default for Settings.BATTERY_CHARGING_STATE_ENFORCE_LEVEL.
-1 means system internal default value is used. -->
<integer name="def_battery_charging_state_enforce_level">-1</integer>
+
+ <!-- Value to use as default scale for fonts -->
+ <item name="def_device_font_scale" format="float" type="dimen">1.0</item>
</resources>
diff --git a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java
index 8ae50eb..8ad5f24 100644
--- a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java
+++ b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java
@@ -74,6 +74,7 @@
Settings.Secure.TTS_DEFAULT_LOCALE,
Settings.Secure.SHOW_IME_WITH_HARD_KEYBOARD,
Settings.Secure.ACCESSIBILITY_BOUNCE_KEYS,
+ Settings.Secure.ACCESSIBILITY_SLOW_KEYS,
Settings.Secure.ACCESSIBILITY_STICKY_KEYS,
Settings.Secure.WIFI_NETWORKS_AVAILABLE_NOTIFICATION_ON, // moved to global
Settings.Secure.WIFI_NETWORKS_AVAILABLE_REPEAT_DELAY, // moved to global
diff --git a/packages/SettingsProvider/src/android/provider/settings/backup/SystemSettings.java b/packages/SettingsProvider/src/android/provider/settings/backup/SystemSettings.java
index e7d7bb0..38ec931 100644
--- a/packages/SettingsProvider/src/android/provider/settings/backup/SystemSettings.java
+++ b/packages/SettingsProvider/src/android/provider/settings/backup/SystemSettings.java
@@ -48,6 +48,7 @@
Settings.System.WIFI_STATIC_DNS2,
Settings.System.BLUETOOTH_DISCOVERABILITY,
Settings.System.BLUETOOTH_DISCOVERABILITY_TIMEOUT,
+ Settings.System.DEFAULT_DEVICE_FONT_SCALE,
Settings.System.FONT_SCALE,
Settings.System.DIM_SCREEN,
Settings.System.SCREEN_OFF_TIMEOUT,
diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java
index 285c8c9..d854df38 100644
--- a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java
+++ b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java
@@ -120,6 +120,7 @@
VALIDATORS.put(Secure.TTS_DEFAULT_LOCALE, TTS_LIST_VALIDATOR);
VALIDATORS.put(Secure.SHOW_IME_WITH_HARD_KEYBOARD, BOOLEAN_VALIDATOR);
VALIDATORS.put(Secure.ACCESSIBILITY_BOUNCE_KEYS, ANY_INTEGER_VALIDATOR);
+ VALIDATORS.put(Secure.ACCESSIBILITY_SLOW_KEYS, ANY_INTEGER_VALIDATOR);
VALIDATORS.put(Secure.ACCESSIBILITY_STICKY_KEYS, BOOLEAN_VALIDATOR);
VALIDATORS.put(Secure.WIFI_NETWORKS_AVAILABLE_NOTIFICATION_ON, BOOLEAN_VALIDATOR);
VALIDATORS.put(Secure.WIFI_NETWORKS_AVAILABLE_REPEAT_DELAY, NON_NEGATIVE_INTEGER_VALIDATOR);
diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/SettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/SettingsValidators.java
index a8a659e..677c81a 100644
--- a/packages/SettingsProvider/src/android/provider/settings/validators/SettingsValidators.java
+++ b/packages/SettingsProvider/src/android/provider/settings/validators/SettingsValidators.java
@@ -45,6 +45,9 @@
}
};
+ public static final Validator FONT_SCALE_VALIDATOR = new InclusiveFloatRangeValidator(0.25f,
+ 5.0f);
+
public static final Validator NON_NEGATIVE_INTEGER_VALIDATOR = new Validator() {
@Override
public boolean validate(@Nullable String value) {
diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/SystemSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/SystemSettingsValidators.java
index 572303a..98941c7 100644
--- a/packages/SettingsProvider/src/android/provider/settings/validators/SystemSettingsValidators.java
+++ b/packages/SettingsProvider/src/android/provider/settings/validators/SystemSettingsValidators.java
@@ -20,6 +20,7 @@
import static android.provider.settings.validators.SettingsValidators.ANY_STRING_VALIDATOR;
import static android.provider.settings.validators.SettingsValidators.BOOLEAN_VALIDATOR;
import static android.provider.settings.validators.SettingsValidators.COMPONENT_NAME_VALIDATOR;
+import static android.provider.settings.validators.SettingsValidators.FONT_SCALE_VALIDATOR;
import static android.provider.settings.validators.SettingsValidators.LENIENT_IP_ADDRESS_VALIDATOR;
import static android.provider.settings.validators.SettingsValidators.NON_NEGATIVE_FLOAT_VALIDATOR;
import static android.provider.settings.validators.SettingsValidators.NON_NEGATIVE_INTEGER_VALIDATOR;
@@ -31,7 +32,6 @@
import android.content.ComponentName;
import android.hardware.display.ColorDisplayManager;
import android.os.BatteryManager;
-import android.provider.Settings.Global;
import android.provider.Settings.System;
import android.util.ArrayMap;
@@ -93,7 +93,8 @@
return value == null || value.length() < MAX_LENGTH;
}
});
- VALIDATORS.put(System.FONT_SCALE, new InclusiveFloatRangeValidator(0.25f, 5.0f));
+ VALIDATORS.put(System.DEFAULT_DEVICE_FONT_SCALE, FONT_SCALE_VALIDATOR);
+ VALIDATORS.put(System.FONT_SCALE, FONT_SCALE_VALIDATOR);
VALIDATORS.put(System.DIM_SCREEN, BOOLEAN_VALIDATOR);
VALIDATORS.put(
System.DISPLAY_COLOR_MODE,
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java
index 2d442f4..febce97 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java
@@ -406,7 +406,7 @@
Process.THREAD_PRIORITY_BACKGROUND);
mHandlerThread.start();
mHandler = new Handler(mHandlerThread.getLooper());
- mSettingsRegistry = new SettingsRegistry();
+ mSettingsRegistry = new SettingsRegistry(mHandlerThread.getLooper());
}
SettingsState.cacheSystemPackageNamesAndSystemSignature(getContext());
synchronized (mLock) {
@@ -2896,8 +2896,8 @@
private String mSettingsCreationBuildId;
- public SettingsRegistry() {
- mHandler = new MyHandler(getContext().getMainLooper());
+ SettingsRegistry(Looper looper) {
+ mHandler = new MyHandler(looper);
mGenerationRegistry = new GenerationRegistry(UserManager.getMaxSupportedUsers());
mBackupManager = new BackupManager(getContext());
}
@@ -3812,7 +3812,7 @@
}
private final class UpgradeController {
- private static final int SETTINGS_VERSION = 224;
+ private static final int SETTINGS_VERSION = 225;
private final int mUserId;
@@ -6004,6 +6004,13 @@
currentVersion = 224;
}
+ // Version 224: Update the default font scale depending on the
+ // R.dimen.def_device_font_scale configuration property.
+ if (currentVersion == 224) {
+ handleDefaultFontScale(getSystemSettingsLocked(userId));
+ currentVersion = 225;
+ }
+
// vXXX: Add new settings above this point.
if (currentVersion != newVersion) {
@@ -6021,6 +6028,32 @@
return currentVersion;
}
+ @SuppressWarnings("GuardedBy")
+ @GuardedBy("mLock")
+ private void handleDefaultFontScale(@NonNull SettingsState systemSettings) {
+ final float defaultFontScale = getContext().getResources()
+ .getFloat(R.dimen.def_device_font_scale);
+ // Persist the value for future use (e.g. Reset Settings option)
+ systemSettings.insertSettingLocked(
+ Settings.System.DEFAULT_DEVICE_FONT_SCALE,
+ String.valueOf(defaultFontScale),
+ /* tag= */ null,
+ /* makeDefault= */ false,
+ SettingsState.SYSTEM_PACKAGE_NAME);
+ // We verify if there is a pre existing value for font_scale.
+ final Setting existingFontScale = systemSettings.getSettingLocked(
+ Settings.System.FONT_SCALE);
+ if (existingFontScale == null || existingFontScale.isNull()) {
+ // Set the default value only if it didn't exist before
+ systemSettings.insertSettingLocked(
+ Settings.System.FONT_SCALE,
+ String.valueOf(defaultFontScale),
+ /* tag= */ null,
+ /* makeDefault= */ false,
+ SettingsState.SYSTEM_PACKAGE_NAME);
+ }
+ }
+
@GuardedBy("mLock")
private void initGlobalSettingsDefaultValLocked(String key, boolean val) {
initGlobalSettingsDefaultValLocked(key, val ? "1" : "0");
diff --git a/packages/Shell/AndroidManifest.xml b/packages/Shell/AndroidManifest.xml
index cc63996..d384542 100644
--- a/packages/Shell/AndroidManifest.xml
+++ b/packages/Shell/AndroidManifest.xml
@@ -343,6 +343,7 @@
<uses-permission android:name="android.permission.REQUEST_COMPANION_PROFILE_GLASSES" />
<uses-permission android:name="android.permission.REQUEST_COMPANION_SELF_MANAGED" />
<uses-permission android:name="android.permission.USE_COMPANION_TRANSPORTS" />
+ <uses-permission android:name="android.permission.REQUEST_OBSERVE_DEVICE_UUID_PRESENCE" />
<uses-permission android:name="android.permission.MANAGE_APPOPS" />
<uses-permission android:name="android.permission.WATCH_APPOPS" />
diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp
index d61ae7e..80656e9 100644
--- a/packages/SystemUI/Android.bp
+++ b/packages/SystemUI/Android.bp
@@ -361,6 +361,7 @@
"androidx.test.ext.junit",
"androidx.test.ext.truth",
"kotlin-test",
+ "SystemUICustomizationTestUtils",
],
libs: [
"android.test.runner",
@@ -439,6 +440,7 @@
"androidx.test.ext.junit",
"inline-mockito-robolectric-prebuilt",
"platform-parametric-runner-lib",
+ "SystemUICustomizationTestUtils",
],
libs: [
"android.test.runner",
diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml
index 168e6e0..54ab5d1 100644
--- a/packages/SystemUI/AndroidManifest.xml
+++ b/packages/SystemUI/AndroidManifest.xml
@@ -899,6 +899,14 @@
android:exported="true"
/>
+ <activity
+ android:name=".volume.panel.ui.activity.VolumePanelActivity"
+ android:label="@string/sound_settings"
+ android:excludeFromRecents="true"
+ android:exported="false"
+ android:launchMode="singleInstance"
+ android:theme="@style/Theme.VolumePanelActivity" />
+
<activity android:name=".wallet.ui.WalletActivity"
android:label="@string/wallet_title"
android:theme="@style/Wallet.Theme"
diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig
index 3236130..2c35c77 100644
--- a/packages/SystemUI/aconfig/systemui.aconfig
+++ b/packages/SystemUI/aconfig/systemui.aconfig
@@ -89,6 +89,13 @@
}
flag {
+ name: "notification_avalanche_suppression"
+ namespace: "systemui"
+ description: "After notification avalanche floodgate event, suppress HUNs completely."
+ bug: "321089634"
+}
+
+flag {
name: "notification_background_tint_optimization"
namespace: "systemui"
description: "Re-enable the codepath that removed tinting of notifications when the"
diff --git a/packages/SystemUI/compose/facade/disabled/src/com/android/systemui/compose/ComposeFacade.kt b/packages/SystemUI/compose/facade/disabled/src/com/android/systemui/compose/ComposeFacade.kt
index 2052e2c..3c32594 100644
--- a/packages/SystemUI/compose/facade/disabled/src/com/android/systemui/compose/ComposeFacade.kt
+++ b/packages/SystemUI/compose/facade/disabled/src/com/android/systemui/compose/ComposeFacade.kt
@@ -17,6 +17,7 @@
package com.android.systemui.compose
+import android.app.Dialog
import android.content.Context
import android.view.View
import android.view.WindowInsets
@@ -26,11 +27,14 @@
import com.android.systemui.bouncer.ui.viewmodel.BouncerViewModel
import com.android.systemui.communal.ui.viewmodel.BaseCommunalViewModel
import com.android.systemui.communal.widgets.WidgetConfigurator
+import com.android.systemui.keyboard.stickykeys.ui.viewmodel.StickyKeysIndicatorViewModel
import com.android.systemui.people.ui.viewmodel.PeopleViewModel
import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel
import com.android.systemui.scene.shared.model.Scene
import com.android.systemui.scene.shared.model.SceneKey
import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModel
+import com.android.systemui.statusbar.phone.SystemUIDialogFactory
+import com.android.systemui.volume.panel.ui.viewmodel.VolumePanelViewModel
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.StateFlow
@@ -60,6 +64,14 @@
throwComposeUnavailableError()
}
+ override fun setVolumePanelActivityContent(
+ activity: ComponentActivity,
+ viewModel: VolumePanelViewModel,
+ onDismissAnimationFinished: () -> Unit,
+ ) {
+ throwComposeUnavailableError()
+ }
+
override fun createFooterActionsView(
context: Context,
viewModel: FooterActionsViewModel,
@@ -78,6 +90,13 @@
throwComposeUnavailableError()
}
+ override fun createStickyKeysDialog(
+ dialogFactory: SystemUIDialogFactory,
+ viewModel: StickyKeysIndicatorViewModel
+ ): Dialog {
+ throwComposeUnavailableError()
+ }
+
override fun createCommunalView(
context: Context,
viewModel: BaseCommunalViewModel,
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/VolumePanelComponentKey.kt b/packages/SystemUI/compose/facade/disabled/src/com/android/systemui/volume/panel/component/bottombar/BottomBarModule.kt
similarity index 77%
copy from packages/SystemUI/src/com/android/systemui/volume/panel/VolumePanelComponentKey.kt
copy to packages/SystemUI/compose/facade/disabled/src/com/android/systemui/volume/panel/component/bottombar/BottomBarModule.kt
index 22a74d2..c8dae76 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/VolumePanelComponentKey.kt
+++ b/packages/SystemUI/compose/facade/disabled/src/com/android/systemui/volume/panel/component/bottombar/BottomBarModule.kt
@@ -14,7 +14,8 @@
* limitations under the License.
*/
-package com.android.systemui.volume.panel
+package com.android.systemui.volume.panel.component.bottombar
-/** Uniquely identifies the [com.android.systemui.volume.panel.ui.VolumePanelComponent]. */
-typealias VolumePanelComponentKey = String
+import dagger.Module
+
+@Module interface BottomBarModule
diff --git a/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/compose/ComposeFacade.kt b/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/compose/ComposeFacade.kt
index b607d59..afb860e 100644
--- a/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/compose/ComposeFacade.kt
+++ b/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/compose/ComposeFacade.kt
@@ -16,6 +16,7 @@
package com.android.systemui.compose
+import android.app.Dialog
import android.content.Context
import android.graphics.Point
import android.view.View
@@ -38,6 +39,8 @@
import com.android.systemui.communal.ui.compose.CommunalHub
import com.android.systemui.communal.ui.viewmodel.BaseCommunalViewModel
import com.android.systemui.communal.widgets.WidgetConfigurator
+import com.android.systemui.keyboard.stickykeys.ui.view.StickyKeysIndicator
+import com.android.systemui.keyboard.stickykeys.ui.viewmodel.StickyKeysIndicatorViewModel
import com.android.systemui.people.ui.compose.PeopleScreen
import com.android.systemui.people.ui.viewmodel.PeopleViewModel
import com.android.systemui.qs.footer.ui.compose.FooterActions
@@ -47,6 +50,10 @@
import com.android.systemui.scene.ui.composable.ComposableScene
import com.android.systemui.scene.ui.composable.SceneContainer
import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModel
+import com.android.systemui.statusbar.phone.SystemUIDialogFactory
+import com.android.systemui.statusbar.phone.create
+import com.android.systemui.volume.panel.ui.composable.VolumePanelRoot
+import com.android.systemui.volume.panel.ui.viewmodel.VolumePanelViewModel
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
@@ -86,6 +93,19 @@
}
}
+ override fun setVolumePanelActivityContent(
+ activity: ComponentActivity,
+ viewModel: VolumePanelViewModel,
+ onDismissAnimationFinished: () -> Unit,
+ ) {
+ activity.setContent {
+ VolumePanelRoot(
+ viewModel = viewModel,
+ onDismissAnimationFinished = onDismissAnimationFinished,
+ )
+ }
+ }
+
override fun createFooterActionsView(
context: Context,
viewModel: FooterActionsViewModel,
@@ -120,6 +140,13 @@
}
}
+ override fun createStickyKeysDialog(
+ dialogFactory: SystemUIDialogFactory,
+ viewModel: StickyKeysIndicatorViewModel
+ ): Dialog {
+ return dialogFactory.create { StickyKeysIndicator(viewModel) }
+ }
+
override fun createCommunalView(
context: Context,
viewModel: BaseCommunalViewModel,
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt
index c073b79b..4fdcf75 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt
@@ -64,12 +64,11 @@
transitions = sceneTransitions,
)
- // Don't show hub mode UI if keyguard is not present. This is important since we're in the
- // shade, which can be opened from many locations.
- val isKeyguardShowing by viewModel.isKeyguardVisible.collectAsState(initial = false)
+ // Don't show hub mode UI if communal is not available. Communal is only available if it has
+ // been enabled via settings and either keyguard is showing, or, the device is currently
+ // dreaming.
val isCommunalAvailable by viewModel.isCommunalAvailable.collectAsState()
-
- if (!isKeyguardShowing || !isCommunalAvailable) {
+ if (!isCommunalAvailable) {
return
}
@@ -85,13 +84,14 @@
SceneTransitionLayout(
state = sceneTransitionLayoutState,
modifier = modifier.fillMaxSize(),
- edgeDetector = FixedSizeEdgeDetector(ContainerDimensions.EdgeSwipeSize),
+ swipeSourceDetector = FixedSizeEdgeDetector(ContainerDimensions.EdgeSwipeSize),
) {
scene(
TransitionSceneKey.Blank,
userActions =
mapOf(
- Swipe(SwipeDirection.Left, fromEdge = Edge.Right) to TransitionSceneKey.Communal
+ Swipe(SwipeDirection.Left, fromSource = Edge.Right) to
+ TransitionSceneKey.Communal
)
) {
// This scene shows nothing only allowing for transitions to the communal scene.
@@ -102,7 +102,7 @@
TransitionSceneKey.Communal,
userActions =
mapOf(
- Swipe(SwipeDirection.Right, fromEdge = Edge.Left) to TransitionSceneKey.Blank
+ Swipe(SwipeDirection.Right, fromSource = Edge.Left) to TransitionSceneKey.Blank
),
) {
CommunalScene(viewModel, modifier = modifier)
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt
index 409f15b..761e74e 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt
@@ -97,11 +97,13 @@
import androidx.compose.ui.viewinterop.AndroidView
import androidx.compose.ui.window.Popup
import androidx.core.view.setPadding
+import com.android.compose.modifiers.thenIf
import com.android.compose.theme.LocalAndroidColorScheme
import com.android.systemui.communal.domain.model.CommunalContentModel
import com.android.systemui.communal.shared.model.CommunalContentSize
import com.android.systemui.communal.ui.compose.Dimensions.CardOutlineWidth
import com.android.systemui.communal.ui.compose.extensions.allowGestures
+import com.android.systemui.communal.ui.compose.extensions.detectLongPressGesture
import com.android.systemui.communal.ui.compose.extensions.firstItemAtOffset
import com.android.systemui.communal.ui.compose.extensions.observeTapsWithoutConsuming
import com.android.systemui.communal.ui.viewmodel.BaseCommunalViewModel
@@ -132,6 +134,8 @@
val removeButtonEnabled by remember {
derivedStateOf { selectedIndex.value != null || reorderingWidgets }
}
+ val (isButtonToEditWidgetsShowing, setIsButtonToEditWidgetsShowing) =
+ remember { mutableStateOf(false) }
val contentPadding = gridContentPadding(viewModel.isEditMode, toolbarSize)
val contentOffset = beforeContentPadding(contentPadding).toOffset()
@@ -158,6 +162,11 @@
}
viewModel.setSelectedIndex(newIndex)
}
+ }
+ .thenIf(!viewModel.isEditMode) {
+ Modifier.pointerInput(Unit) {
+ detectLongPressGesture { offset -> setIsButtonToEditWidgetsShowing(true) }
+ }
},
) {
CommunalHubLazyGrid(
@@ -207,6 +216,16 @@
PopupOnDismissCtaTile(viewModel::onHidePopupAfterDismissCta)
}
+ if (isButtonToEditWidgetsShowing) {
+ ButtonToEditWidgets(
+ onClick = {
+ setIsButtonToEditWidgetsShowing(false)
+ viewModel.onOpenWidgetEditor()
+ },
+ onHide = { setIsButtonToEditWidgetsShowing(false) },
+ )
+ }
+
// This spacer covers the edge of the LazyHorizontalGrid and prevents it from receiving
// touches, so that the SceneTransitionLayout can intercept the touches and allow an edge
// swipe back to the blank scene.
@@ -414,6 +433,34 @@
}
@Composable
+private fun ButtonToEditWidgets(
+ onClick: () -> Unit,
+ onHide: () -> Unit,
+) {
+ Popup(alignment = Alignment.TopCenter, offset = IntOffset(0, 40), onDismissRequest = onHide) {
+ val colors = LocalAndroidColorScheme.current
+ Button(
+ modifier =
+ Modifier.height(56.dp).background(colors.secondary, RoundedCornerShape(50.dp)),
+ onClick = onClick,
+ ) {
+ Icon(
+ imageVector = Icons.Outlined.Widgets,
+ contentDescription = stringResource(R.string.button_to_configure_widgets_text),
+ tint = colors.onSecondary,
+ modifier = Modifier.size(20.dp)
+ )
+ Spacer(modifier = Modifier.size(8.dp))
+ Text(
+ text = stringResource(R.string.button_to_configure_widgets_text),
+ style = MaterialTheme.typography.titleSmall,
+ color = colors.onSecondary,
+ )
+ }
+ }
+}
+
+@Composable
private fun PopupOnDismissCtaTile(onHidePopupAfterDismissCta: () -> Unit) {
Popup(
alignment = Alignment.TopCenter,
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/extensions/PointerInputScopeExt.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/extensions/PointerInputScopeExt.kt
index 1407494..bc1e429 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/extensions/PointerInputScopeExt.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/extensions/PointerInputScopeExt.kt
@@ -20,9 +20,13 @@
import androidx.compose.foundation.gestures.awaitFirstDown
import androidx.compose.foundation.gestures.waitForUpOrCancellation
import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.input.pointer.AwaitPointerEventScope
import androidx.compose.ui.input.pointer.PointerEventPass
+import androidx.compose.ui.input.pointer.PointerEventTimeoutCancellationException
import androidx.compose.ui.input.pointer.PointerInputChange
import androidx.compose.ui.input.pointer.PointerInputScope
+import androidx.compose.ui.util.fastAny
+import androidx.compose.ui.util.fastForEach
import kotlinx.coroutines.coroutineScope
/**
@@ -44,6 +48,41 @@
}
}
+/**
+ * Detect long press gesture and calls onLongPress when detected. The callback parameter receives an
+ * Offset representing the position relative to the containing element.
+ */
+suspend fun PointerInputScope.detectLongPressGesture(
+ pass: PointerEventPass = PointerEventPass.Initial,
+ onLongPress: ((Offset) -> Unit),
+) = coroutineScope {
+ awaitEachGesture {
+ val down = awaitFirstDown(pass = pass)
+ val longPressTimeout = viewConfiguration.longPressTimeoutMillis
+ // wait for first tap up or long press
+ try {
+ withTimeout(longPressTimeout) { waitForUpOrCancellation(pass = pass) }
+ } catch (_: PointerEventTimeoutCancellationException) {
+ // withTimeout throws exception if timeout has passed before block completes
+ onLongPress.invoke(down.position)
+ consumeUntilUp(pass)
+ }
+ }
+}
+
+/**
+ * Consumes all pointer events until nothing is pressed and then returns. This method assumes that
+ * something is currently pressed.
+ */
+private suspend fun AwaitPointerEventScope.consumeUntilUp(
+ pass: PointerEventPass = PointerEventPass.Initial
+) {
+ do {
+ val event = awaitPointerEvent(pass = pass)
+ event.changes.fastForEach { it.consume() }
+ } while (event.changes.fastAny { it.pressed })
+}
+
/** Consume all gestures on the initial pass so that child elements do not receive them. */
suspend fun PointerInputScope.consumeAllGestures() = coroutineScope {
awaitEachGesture {
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyboard/stickykeys/ui/view/StickyKeysIndicator.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyboard/stickykeys/ui/view/StickyKeysIndicator.kt
new file mode 100644
index 0000000..68e57b5
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyboard/stickykeys/ui/view/StickyKeysIndicator.kt
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyboard.stickykeys.ui.view
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.key
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.dp
+import com.android.systemui.keyboard.stickykeys.shared.model.Locked
+import com.android.systemui.keyboard.stickykeys.shared.model.ModifierKey
+import com.android.systemui.keyboard.stickykeys.ui.viewmodel.StickyKeysIndicatorViewModel
+
+@Composable
+fun StickyKeysIndicator(viewModel: StickyKeysIndicatorViewModel) {
+ val stickyKeys by viewModel.indicatorContent.collectAsState(emptyMap())
+ StickyKeysIndicator(stickyKeys)
+}
+
+@Composable
+fun StickyKeysIndicator(stickyKeys: Map<ModifierKey, Locked>, modifier: Modifier = Modifier) {
+ Surface(
+ color = MaterialTheme.colorScheme.surface,
+ shape = MaterialTheme.shapes.medium,
+ modifier = modifier
+ ) {
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally,
+ modifier = Modifier.padding(16.dp)
+ ) {
+ stickyKeys.forEach { (key, isLocked) ->
+ key(key) {
+ Text(
+ text = key.text,
+ fontWeight = if (isLocked.locked) FontWeight.Bold else FontWeight.Normal
+ )
+ }
+ }
+ }
+ }
+}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/DefaultBlueprint.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/DefaultBlueprint.kt
index 56d6879..bf02d8a 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/DefaultBlueprint.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/DefaultBlueprint.kt
@@ -173,7 +173,8 @@
val belowLockIconPlaceable =
belowLockIconMeasurable.measure(
noMinConstraints.copy(
- maxHeight = constraints.maxHeight - lockIconBounds.bottom
+ maxHeight =
+ (constraints.maxHeight - lockIconBounds.bottom).coerceAtLeast(0)
)
)
val startShortcutPleaceable = startShortcutMeasurable.measure(noMinConstraints)
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/SplitShadeBlueprint.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/SplitShadeBlueprint.kt
index fdf1166..616a7b4 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/SplitShadeBlueprint.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/SplitShadeBlueprint.kt
@@ -16,19 +16,42 @@
package com.android.systemui.keyguard.ui.composable.blueprint
-import androidx.compose.foundation.background
-import androidx.compose.foundation.layout.Box
-import androidx.compose.material3.Text
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.layout.Layout
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.dimensionResource
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.IntRect
+import androidx.compose.ui.unit.dp
import com.android.compose.animation.scene.SceneScope
+import com.android.compose.modifiers.padding
+import com.android.systemui.Flags
+import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor
import com.android.systemui.keyguard.ui.composable.LockscreenLongPress
+import com.android.systemui.keyguard.ui.composable.section.AmbientIndicationSection
+import com.android.systemui.keyguard.ui.composable.section.BottomAreaSection
+import com.android.systemui.keyguard.ui.composable.section.ClockSection
+import com.android.systemui.keyguard.ui.composable.section.LockSection
+import com.android.systemui.keyguard.ui.composable.section.NotificationSection
+import com.android.systemui.keyguard.ui.composable.section.SettingsMenuSection
+import com.android.systemui.keyguard.ui.composable.section.SmartSpaceSection
+import com.android.systemui.keyguard.ui.composable.section.StatusBarSection
import com.android.systemui.keyguard.ui.viewmodel.LockscreenContentViewModel
+import com.android.systemui.res.R
+import com.android.systemui.shade.LargeScreenHeaderHelper
import dagger.Binds
import dagger.Module
import dagger.multibindings.IntoSet
+import java.util.Optional
import javax.inject.Inject
/**
@@ -39,22 +62,174 @@
@Inject
constructor(
private val viewModel: LockscreenContentViewModel,
+ private val statusBarSection: StatusBarSection,
+ private val clockSection: ClockSection,
+ private val smartSpaceSection: SmartSpaceSection,
+ private val notificationSection: NotificationSection,
+ private val lockSection: LockSection,
+ private val ambientIndicationSectionOptional: Optional<AmbientIndicationSection>,
+ private val bottomAreaSection: BottomAreaSection,
+ private val settingsMenuSection: SettingsMenuSection,
+ private val clockInteractor: KeyguardClockInteractor,
+ private val largeScreenHeaderHelper: LargeScreenHeaderHelper,
) : LockscreenSceneBlueprint {
override val id: String = "split-shade"
@Composable
override fun SceneScope.Content(modifier: Modifier) {
+ val isUdfpsVisible = viewModel.isUdfpsVisible
+ val burnIn = rememberBurnIn(clockInteractor)
+ val resources = LocalContext.current.resources
+
LockscreenLongPress(
viewModel = viewModel.longPress,
modifier = modifier,
- ) { _ ->
- Box(modifier.background(Color.Black)) {
- Text(
- text = "TODO(b/316211368): split shade blueprint",
- color = Color.White,
- modifier = Modifier.align(Alignment.Center),
- )
+ ) { onSettingsMenuPlaced ->
+ Layout(
+ content = {
+ // Constrained to above the lock icon.
+ Column(
+ modifier = Modifier.fillMaxSize(),
+ ) {
+ with(statusBarSection) { StatusBar(modifier = Modifier.fillMaxWidth()) }
+ Row(
+ modifier = Modifier.fillMaxSize(),
+ ) {
+ Column(
+ modifier = Modifier.fillMaxHeight().weight(weight = 1f),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ with(smartSpaceSection) {
+ SmartSpace(
+ burnInParams = burnIn.parameters,
+ onTopChanged = burnIn.onSmartspaceTopChanged,
+ modifier =
+ Modifier.fillMaxWidth()
+ .padding(
+ top = {
+ viewModel.getSmartSpacePaddingTop(resources)
+ }
+ ),
+ )
+ }
+
+ Spacer(modifier = Modifier.weight(weight = 1f))
+ with(clockSection) { LargeClock() }
+ Spacer(modifier = Modifier.weight(weight = 1f))
+ }
+ with(notificationSection) {
+ val splitShadeTopMargin: Dp =
+ if (Flags.centralizedStatusBarDimensRefactor()) {
+ largeScreenHeaderHelper.getLargeScreenHeaderHeight().dp
+ } else {
+ dimensionResource(
+ id = R.dimen.large_screen_shade_header_height
+ )
+ }
+ Notifications(
+ modifier =
+ Modifier.fillMaxHeight()
+ .weight(weight = 1f)
+ .padding(top = splitShadeTopMargin)
+ )
+ }
+ }
+
+ if (!isUdfpsVisible && ambientIndicationSectionOptional.isPresent) {
+ with(ambientIndicationSectionOptional.get()) {
+ AmbientIndication(modifier = Modifier.fillMaxWidth())
+ }
+ }
+ }
+
+ with(lockSection) { LockIcon() }
+
+ // Aligned to bottom and constrained to below the lock icon.
+ Column(modifier = Modifier.fillMaxWidth()) {
+ if (isUdfpsVisible && ambientIndicationSectionOptional.isPresent) {
+ with(ambientIndicationSectionOptional.get()) {
+ AmbientIndication(modifier = Modifier.fillMaxWidth())
+ }
+ }
+
+ with(bottomAreaSection) {
+ IndicationArea(modifier = Modifier.fillMaxWidth())
+ }
+ }
+
+ // Aligned to bottom and NOT constrained by the lock icon.
+ with(bottomAreaSection) {
+ Shortcut(isStart = true, applyPadding = true)
+ Shortcut(isStart = false, applyPadding = true)
+ }
+ with(settingsMenuSection) { SettingsMenu(onSettingsMenuPlaced) }
+ },
+ modifier = Modifier.fillMaxSize(),
+ ) { measurables, constraints ->
+ check(measurables.size == 6)
+ val aboveLockIconMeasurable = measurables[0]
+ val lockIconMeasurable = measurables[1]
+ val belowLockIconMeasurable = measurables[2]
+ val startShortcutMeasurable = measurables[3]
+ val endShortcutMeasurable = measurables[4]
+ val settingsMenuMeasurable = measurables[5]
+
+ val noMinConstraints =
+ constraints.copy(
+ minWidth = 0,
+ minHeight = 0,
+ )
+ val lockIconPlaceable = lockIconMeasurable.measure(noMinConstraints)
+ val lockIconBounds =
+ IntRect(
+ left = lockIconPlaceable[BlueprintAlignmentLines.LockIcon.Left],
+ top = lockIconPlaceable[BlueprintAlignmentLines.LockIcon.Top],
+ right = lockIconPlaceable[BlueprintAlignmentLines.LockIcon.Right],
+ bottom = lockIconPlaceable[BlueprintAlignmentLines.LockIcon.Bottom],
+ )
+
+ val aboveLockIconPlaceable =
+ aboveLockIconMeasurable.measure(
+ noMinConstraints.copy(maxHeight = lockIconBounds.top)
+ )
+ val belowLockIconPlaceable =
+ belowLockIconMeasurable.measure(
+ noMinConstraints.copy(
+ maxHeight =
+ (constraints.maxHeight - lockIconBounds.bottom).coerceAtLeast(0)
+ )
+ )
+ val startShortcutPleaceable = startShortcutMeasurable.measure(noMinConstraints)
+ val endShortcutPleaceable = endShortcutMeasurable.measure(noMinConstraints)
+ val settingsMenuPlaceable = settingsMenuMeasurable.measure(noMinConstraints)
+
+ layout(constraints.maxWidth, constraints.maxHeight) {
+ aboveLockIconPlaceable.place(
+ x = 0,
+ y = 0,
+ )
+ lockIconPlaceable.place(
+ x = lockIconBounds.left,
+ y = lockIconBounds.top,
+ )
+ belowLockIconPlaceable.place(
+ x = 0,
+ y = constraints.maxHeight - belowLockIconPlaceable.height,
+ )
+ startShortcutPleaceable.place(
+ x = 0,
+ y = constraints.maxHeight - startShortcutPleaceable.height,
+ )
+ endShortcutPleaceable.place(
+ x = constraints.maxWidth - endShortcutPleaceable.width,
+ y = constraints.maxHeight - endShortcutPleaceable.height,
+ )
+ settingsMenuPlaceable.place(
+ x = (constraints.maxWidth - settingsMenuPlaceable.width) / 2,
+ y = constraints.maxHeight - settingsMenuPlaceable.height,
+ )
+ }
}
}
}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/ClockSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/ClockSection.kt
index f40b871..8f21879 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/ClockSection.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/ClockSection.kt
@@ -16,7 +16,8 @@
package com.android.systemui.keyguard.ui.composable.section
-import androidx.compose.foundation.layout.fillMaxWidth
+import android.view.ViewGroup
+import android.widget.FrameLayout
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
@@ -75,7 +76,13 @@
) {
content {
AndroidView(
- factory = { checkNotNull(currentClock).smallClock.view },
+ factory = { context ->
+ FrameLayout(context).apply {
+ val newClockView = checkNotNull(currentClock).smallClock.view
+ (newClockView.parent as? ViewGroup)?.removeView(newClockView)
+ addView(newClockView)
+ }
+ },
modifier =
Modifier.padding(
horizontal =
@@ -83,6 +90,12 @@
)
.padding(top = { viewModel.getSmallClockTopMargin(view.context) })
.onTopPlacementChanged(onTopChanged),
+ update = {
+ val newClockView = checkNotNull(currentClock).smallClock.view
+ it.removeAllViews()
+ (newClockView.parent as? ViewGroup)?.removeView(newClockView)
+ it.addView(newClockView)
+ },
)
}
}
@@ -116,8 +129,19 @@
) {
content {
AndroidView(
- factory = { checkNotNull(currentClock).largeClock.view },
- modifier = Modifier.fillMaxWidth()
+ factory = { context ->
+ FrameLayout(context).apply {
+ val newClockView = checkNotNull(currentClock).largeClock.view
+ (newClockView.parent as? ViewGroup)?.removeView(newClockView)
+ addView(newClockView)
+ }
+ },
+ update = {
+ val newClockView = checkNotNull(currentClock).largeClock.view
+ it.removeAllViews()
+ (newClockView.parent as? ViewGroup)?.removeView(newClockView)
+ it.addView(newClockView)
+ },
)
}
}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettings.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettings.kt
index 9778e53..c027c49 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettings.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettings.kt
@@ -16,17 +16,16 @@
package com.android.systemui.qs.ui.composable
-import android.view.ContextThemeWrapper
import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.defaultMinSize
-import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
-import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
@@ -53,14 +52,6 @@
}
}
-@Composable
-private fun QuickSettingsTheme(content: @Composable () -> Unit) {
- val context = LocalContext.current
- val themedContext =
- remember(context) { ContextThemeWrapper(context, R.style.Theme_SystemUI_QuickSettings) }
- CompositionLocalProvider(LocalContext provides themedContext) { content() }
-}
-
private fun SceneScope.stateForQuickSettingsContent(): QSSceneAdapter.State {
return when (val transitionState = layoutState.transitionState) {
is TransitionState.Idle -> {
@@ -115,6 +106,7 @@
modifier: Modifier = Modifier,
) {
val qsView by qsSceneAdapter.qsView.collectAsState(null)
+ val isCustomizing by qsSceneAdapter.isCustomizing.collectAsState()
QuickSettingsTheme {
val context = LocalContext.current
@@ -124,14 +116,27 @@
}
}
qsView?.let { view ->
- AndroidView(
- modifier = modifier.fillMaxSize().background(colorAttr(R.attr.underSurface)),
- factory = { _ ->
- qsSceneAdapter.setState(state)
- view
- },
- update = { qsSceneAdapter.setState(state) }
- )
+ Box(
+ modifier =
+ modifier
+ .fillMaxWidth()
+ .then(
+ if (isCustomizing) {
+ Modifier.fillMaxHeight()
+ } else {
+ Modifier.wrapContentHeight()
+ }
+ )
+ ) {
+ AndroidView(
+ modifier = Modifier.fillMaxWidth().background(colorAttr(R.attr.underSurface)),
+ factory = { _ ->
+ qsSceneAdapter.setState(state)
+ view
+ },
+ update = { qsSceneAdapter.setState(state) }
+ )
+ }
}
}
}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt
index d8c7290..bbfe0fd 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt
@@ -24,31 +24,44 @@
import androidx.compose.animation.fadeOut
import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.background
+import androidx.compose.foundation.clipScrollableContainer
+import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.wrapContentHeight
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.unit.dp
import com.android.compose.animation.scene.SceneScope
+import com.android.compose.animation.scene.TransitionState
import com.android.compose.windowsizeclass.LocalWindowSizeClass
import com.android.systemui.battery.BatteryMeterViewController
+import com.android.systemui.compose.modifiers.sysuiResTag
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.notifications.ui.composable.HeadsUpNotificationSpace
+import com.android.systemui.qs.footer.ui.compose.FooterActions
import com.android.systemui.qs.ui.viewmodel.QuickSettingsSceneViewModel
import com.android.systemui.scene.shared.model.SceneKey
import com.android.systemui.scene.ui.composable.ComposableScene
+import com.android.systemui.scene.ui.composable.toTransitionSceneKey
import com.android.systemui.shade.ui.composable.CollapsedShadeHeader
import com.android.systemui.shade.ui.composable.ExpandedShadeHeader
import com.android.systemui.shade.ui.composable.Shade
@@ -105,57 +118,120 @@
) {
// TODO(b/280887232): implement the real UI.
Box(modifier = modifier.fillMaxSize()) {
- Box(modifier = Modifier.fillMaxSize()) {
- val isCustomizing by viewModel.qsSceneAdapter.isCustomizing.collectAsState()
- val collapsedHeaderHeight =
- with(LocalDensity.current) { ShadeHeader.Dimensions.CollapsedHeight.roundToPx() }
- Spacer(
- modifier =
- Modifier.element(Shade.Elements.ScrimBackground)
- .fillMaxSize()
- .background(MaterialTheme.colorScheme.scrim, shape = Shade.Shapes.Scrim)
- )
- Column(
- horizontalAlignment = Alignment.CenterHorizontally,
- modifier =
- Modifier.fillMaxSize().padding(start = 16.dp, end = 16.dp, bottom = 48.dp)
- ) {
- when (LocalWindowSizeClass.current.widthSizeClass) {
- WindowWidthSizeClass.Compact ->
- AnimatedVisibility(
- visible = !isCustomizing,
- enter =
- expandVertically(
- animationSpec = tween(1000),
- initialHeight = { collapsedHeaderHeight },
- ) + fadeIn(tween(1000)),
- exit =
- shrinkVertically(
- animationSpec = tween(1000),
- targetHeight = { collapsedHeaderHeight },
- shrinkTowards = Alignment.Top,
- ) + fadeOut(tween(1000)),
- ) {
- ExpandedShadeHeader(
+ val isCustomizing by viewModel.qsSceneAdapter.isCustomizing.collectAsState()
+ val collapsedHeaderHeight =
+ with(LocalDensity.current) { ShadeHeader.Dimensions.CollapsedHeight.roundToPx() }
+ val lifecycleOwner = LocalLifecycleOwner.current
+ val footerActionsViewModel =
+ remember(lifecycleOwner, viewModel) {
+ viewModel.getFooterActionsViewModel(lifecycleOwner)
+ }
+ val scrollState = rememberScrollState()
+ // When animating into the scene, we don't want it to be able to scroll, as it could mess
+ // up with the expansion animation.
+ val isScrollable =
+ when (val state = layoutState.transitionState) {
+ is TransitionState.Idle -> true
+ is TransitionState.Transition -> {
+ state.fromScene == SceneKey.QuickSettings.toTransitionSceneKey()
+ }
+ }
+
+ LaunchedEffect(isCustomizing, scrollState) {
+ if (isCustomizing) {
+ scrollState.scrollTo(0)
+ }
+ }
+
+ // This is the background for the whole scene, as the elements don't necessarily provide
+ // a background that extends to the edges.
+ Spacer(
+ modifier =
+ Modifier.element(Shade.Elements.ScrimBackground)
+ .fillMaxSize()
+ .background(MaterialTheme.colorScheme.scrim, shape = Shade.Shapes.Scrim)
+ )
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally,
+ modifier =
+ Modifier.fillMaxSize()
+ // bottom should be tied to insets
+ .padding(bottom = 16.dp)
+ ) {
+ Box(modifier = Modifier.fillMaxSize().weight(1f)) {
+ val shadeHeaderAndQuickSettingsModifier =
+ if (isCustomizing) {
+ Modifier.fillMaxHeight().align(Alignment.TopCenter)
+ } else {
+ Modifier.verticalNestedScrollToScene()
+ .verticalScroll(
+ scrollState,
+ enabled = isScrollable,
+ )
+ .clipScrollableContainer(Orientation.Horizontal)
+ .fillMaxWidth()
+ .wrapContentHeight(unbounded = true)
+ .align(Alignment.TopCenter)
+ }
+
+ Column(
+ modifier = shadeHeaderAndQuickSettingsModifier,
+ ) {
+ when (LocalWindowSizeClass.current.widthSizeClass) {
+ WindowWidthSizeClass.Compact ->
+ AnimatedVisibility(
+ visible = !isCustomizing,
+ enter =
+ expandVertically(
+ animationSpec = tween(100),
+ initialHeight = { collapsedHeaderHeight },
+ ) + fadeIn(tween(100)),
+ exit =
+ shrinkVertically(
+ animationSpec = tween(100),
+ targetHeight = { collapsedHeaderHeight },
+ shrinkTowards = Alignment.Top,
+ ) + fadeOut(tween(100)),
+ ) {
+ ExpandedShadeHeader(
+ viewModel = viewModel.shadeHeaderViewModel,
+ createTintedIconManager = createTintedIconManager,
+ createBatteryMeterViewController =
+ createBatteryMeterViewController,
+ statusBarIconController = statusBarIconController,
+ modifier = Modifier.padding(horizontal = 16.dp),
+ )
+ }
+ else ->
+ CollapsedShadeHeader(
viewModel = viewModel.shadeHeaderViewModel,
createTintedIconManager = createTintedIconManager,
createBatteryMeterViewController = createBatteryMeterViewController,
statusBarIconController = statusBarIconController,
+ modifier = Modifier.padding(horizontal = 16.dp),
)
- }
- else ->
- CollapsedShadeHeader(
- viewModel = viewModel.shadeHeaderViewModel,
- createTintedIconManager = createTintedIconManager,
- createBatteryMeterViewController = createBatteryMeterViewController,
- statusBarIconController = statusBarIconController,
- )
+ }
+ Spacer(modifier = Modifier.height(16.dp))
+ // This view has its own horizontal padding
+ QuickSettings(
+ modifier = Modifier.sysuiResTag("expanded_qs_scroll_view"),
+ viewModel.qsSceneAdapter,
+ )
}
- Spacer(modifier = Modifier.height(16.dp))
- QuickSettings(
- modifier = Modifier.fillMaxHeight(),
- viewModel.qsSceneAdapter,
- )
+ }
+ AnimatedVisibility(
+ visible = !isCustomizing,
+ modifier = Modifier.align(Alignment.CenterHorizontally).fillMaxWidth()
+ ) {
+ QuickSettingsTheme {
+ // This view has its own horizontal padding
+ // TODO(b/321716470) This should use a lifecycle tied to the scene.
+ FooterActions(
+ viewModel = footerActionsViewModel,
+ qsVisibilityLifecycleOwner = lifecycleOwner,
+ modifier = Modifier.element(QuickSettings.Elements.FooterActions)
+ )
+ }
}
}
HeadsUpNotificationSpace(
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsTheme.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsTheme.kt
new file mode 100644
index 0000000..87b6f95b
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsTheme.kt
@@ -0,0 +1,32 @@
+/*
+ * 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.qs.ui.composable
+
+import android.view.ContextThemeWrapper
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.remember
+import androidx.compose.ui.platform.LocalContext
+import com.android.systemui.res.R
+
+@Composable
+fun QuickSettingsTheme(content: @Composable () -> Unit) {
+ val context = LocalContext.current
+ val themedContext =
+ remember(context) { ContextThemeWrapper(context, R.style.Theme_SystemUI_QuickSettings) }
+ CompositionLocalProvider(LocalContext provides themedContext) { content() }
+}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt
index c35202c..9f9e1f5 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt
@@ -183,7 +183,7 @@
is UserAction.Swipe ->
Swipe(
pointerCount = pointerCount,
- fromEdge =
+ fromSource =
when (this.fromEdge) {
null -> null
Edge.LEFT -> SceneTransitionEdge.Left
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/bottombar/BottomBarModule.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/bottombar/BottomBarModule.kt
new file mode 100644
index 0000000..43d5453
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/bottombar/BottomBarModule.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.volume.panel.component.bottombar
+
+import com.android.systemui.volume.panel.component.bottombar.ui.BottomBarComponent
+import com.android.systemui.volume.panel.component.shared.model.VolumePanelComponents
+import com.android.systemui.volume.panel.domain.AlwaysAvailableCriteria
+import com.android.systemui.volume.panel.domain.ComponentAvailabilityCriteria
+import com.android.systemui.volume.panel.shared.model.VolumePanelUiComponent
+import dagger.Binds
+import dagger.Module
+import dagger.multibindings.IntoMap
+import dagger.multibindings.StringKey
+
+@Module
+interface BottomBarModule {
+
+ @Binds
+ @IntoMap
+ @StringKey(VolumePanelComponents.BOTTOM_BAR)
+ fun bindMediaVolumeSliderComponent(component: BottomBarComponent): VolumePanelUiComponent
+
+ @Binds
+ @IntoMap
+ @StringKey(VolumePanelComponents.BOTTOM_BAR)
+ fun bindComponentAvailabilityCriteria(
+ defaultCriteria: AlwaysAvailableCriteria
+ ): ComponentAvailabilityCriteria
+}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/bottombar/ui/BottomBarComponent.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/bottombar/ui/BottomBarComponent.kt
new file mode 100644
index 0000000..03c07f7
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/bottombar/ui/BottomBarComponent.kt
@@ -0,0 +1,60 @@
+/*
+ * 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.volume.panel.component.bottombar.ui
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import com.android.compose.PlatformButton
+import com.android.compose.PlatformOutlinedButton
+import com.android.systemui.res.R
+import com.android.systemui.volume.panel.component.bottombar.ui.viewmodel.BottomBarViewModel
+import com.android.systemui.volume.panel.dagger.scope.VolumePanelScope
+import com.android.systemui.volume.panel.ui.composable.ComposeVolumePanelUiComponent
+import com.android.systemui.volume.panel.ui.composable.VolumePanelComposeScope
+import javax.inject.Inject
+
+@VolumePanelScope
+class BottomBarComponent
+@Inject
+constructor(
+ private val viewModel: BottomBarViewModel,
+) : ComposeVolumePanelUiComponent {
+
+ @Composable
+ override fun VolumePanelComposeScope.Content(modifier: Modifier) {
+ Row(
+ modifier = modifier.height(48.dp).fillMaxWidth(),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ PlatformOutlinedButton(onClick = viewModel::onSettingsClicked) {
+ Text(text = stringResource(R.string.volume_panel_dialog_settings_button))
+ }
+ PlatformButton(onClick = viewModel::onDoneClicked) {
+ Text(text = stringResource(R.string.inline_done_button))
+ }
+ }
+ }
+}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/ui/composable/ComposeVolumePanelUiComponent.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/ui/composable/ComposeVolumePanelUiComponent.kt
new file mode 100644
index 0000000..e1834ee
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/ui/composable/ComposeVolumePanelUiComponent.kt
@@ -0,0 +1,30 @@
+/*
+ * 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.volume.panel.ui.composable
+
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import com.android.systemui.volume.panel.shared.model.VolumePanelUiComponent
+
+/**
+ * Compose implementation of [VolumePanelUiComponent]. Each new UI component should implement this
+ * interface.
+ */
+interface ComposeVolumePanelUiComponent : VolumePanelUiComponent {
+
+ @Composable fun VolumePanelComposeScope.Content(modifier: Modifier)
+}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/ui/composable/VerticalVolumePanelContent.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/ui/composable/VerticalVolumePanelContent.kt
new file mode 100644
index 0000000..dcd22fe
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/ui/composable/VerticalVolumePanelContent.kt
@@ -0,0 +1,42 @@
+/*
+ * 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.volume.panel.ui.composable
+
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import com.android.systemui.volume.panel.ui.viewmodel.ComponentState
+
+@Composable
+fun VolumePanelComposeScope.VerticalVolumePanelContent(
+ components: List<ComponentState>,
+ modifier: Modifier = Modifier,
+) {
+ Column(
+ modifier = modifier,
+ verticalArrangement = Arrangement.spacedBy(20.dp),
+ ) {
+ for (component in components) {
+ AnimatedVisibility(component.isVisible) {
+ with(component.component as ComposeVolumePanelUiComponent) { Content(Modifier) }
+ }
+ }
+ }
+}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/ui/composable/VolumePanelComposeScope.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/ui/composable/VolumePanelComposeScope.kt
new file mode 100644
index 0000000..c70c6b1
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/ui/composable/VolumePanelComposeScope.kt
@@ -0,0 +1,32 @@
+/*
+ * 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.volume.panel.ui.composable
+
+import android.content.res.Configuration.Orientation
+import com.android.systemui.volume.panel.ui.viewmodel.VolumePanelState
+
+class VolumePanelComposeScope(private val state: VolumePanelState) {
+
+ /**
+ * Layout orientation of the panel. It doesn't necessarily aligns with the device orientation,
+ * because in some cases we want to show bigger version of a portrait orientation when the
+ * device is in landscape.
+ */
+ @Orientation
+ val orientation: Int
+ get() = state.orientation
+}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/ui/composable/VolumePanelRoot.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/ui/composable/VolumePanelRoot.kt
new file mode 100644
index 0000000..3487184
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/ui/composable/VolumePanelRoot.kt
@@ -0,0 +1,120 @@
+/*
+ * 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.volume.panel.ui.composable
+
+import android.content.res.Configuration
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.core.MutableTransitionState
+import androidx.compose.animation.slideInVertically
+import androidx.compose.animation.slideOutVertically
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.isSystemInDarkTheme
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.navigationBarsPadding
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.statusBarsPadding
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.dimensionResource
+import com.android.compose.theme.PlatformTheme
+import com.android.systemui.res.R
+import com.android.systemui.volume.panel.ui.layout.ComponentsLayout
+import com.android.systemui.volume.panel.ui.viewmodel.VolumePanelState
+import com.android.systemui.volume.panel.ui.viewmodel.VolumePanelViewModel
+
+@Composable
+fun VolumePanelRoot(
+ viewModel: VolumePanelViewModel,
+ onDismissAnimationFinished: () -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ PlatformTheme(isSystemInDarkTheme()) {
+ val state: VolumePanelState by viewModel.volumePanelState.collectAsState()
+ val components by viewModel.componentsLayout.collectAsState(null)
+
+ val transitionState =
+ remember { MutableTransitionState(false) }.apply { targetState = state.isVisible }
+
+ LaunchedEffect(transitionState.targetState, transitionState.isIdle) {
+ if (!transitionState.targetState && transitionState.isIdle) {
+ onDismissAnimationFinished()
+ }
+ }
+
+ Column(
+ modifier =
+ modifier
+ .fillMaxSize()
+ .statusBarsPadding()
+ .clickable(onClick = { viewModel.dismissPanel() }),
+ verticalArrangement = Arrangement.Bottom,
+ ) {
+ AnimatedVisibility(
+ visibleState = transitionState,
+ enter = slideInVertically { it },
+ exit = slideOutVertically { it },
+ ) {
+ val radius = dimensionResource(R.dimen.volume_panel_corner_radius)
+ Surface(
+ shape = RoundedCornerShape(topStart = radius, topEnd = radius),
+ color = MaterialTheme.colorScheme.surfaceBright,
+ ) {
+ Column {
+ components?.let { componentsState ->
+ with(VolumePanelComposeScope(state)) { Components(componentsState) }
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+@Composable
+private fun VolumePanelComposeScope.Components(state: ComponentsLayout) {
+ if (orientation == Configuration.ORIENTATION_PORTRAIT) {
+ VerticalVolumePanelContent(
+ components = state.contentComponents,
+ modifier = Modifier.padding(dimensionResource(R.dimen.volume_panel_content_padding)),
+ )
+ } else {
+ TODO("Add landscape layout")
+ }
+
+ val horizontalPadding = dimensionResource(R.dimen.volume_panel_bottom_bar_horizontal_padding)
+ if (state.bottomBarComponent.isVisible) {
+ with(state.bottomBarComponent.component as ComposeVolumePanelUiComponent) {
+ Content(
+ Modifier.navigationBarsPadding()
+ .padding(
+ start = horizontalPadding,
+ end = horizontalPadding,
+ bottom = dimensionResource(R.dimen.volume_panel_bottom_bar_bottom_padding),
+ )
+ )
+ }
+ }
+}
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
index 82d4239..b0dc3a1 100644
--- 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
@@ -23,24 +23,19 @@
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?
+/** The edge of a [SceneTransitionLayout]. */
+enum class Edge : SwipeSource {
+ Left,
+ Right,
+ Top,
+ Bottom,
}
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(
+/** An [SwipeSourceDetector] that detects edges assuming a fixed edge size of [size]. */
+class FixedSizeEdgeDetector(val size: Dp) : SwipeSourceDetector {
+ override fun source(
layoutSize: IntSize,
position: IntOffset,
density: Density,
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Key.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Key.kt
index 90f46bd..9d4b69c 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Key.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Key.kt
@@ -44,7 +44,7 @@
class SceneKey(
name: String,
identity: Any = Object(),
-) : Key(name, identity) {
+) : Key(name, identity), UserActionResult {
@VisibleForTesting
// TODO(b/240432457): Make internal once PlatformComposeSceneTransitionLayoutTestsUtils can
// access internal members.
@@ -53,6 +53,10 @@
/** The unique [ElementKey] identifying this scene's root element. */
val rootElementKey = ElementKey(name, identity)
+ // Implementation of [UserActionResult].
+ override val toScene: SceneKey = this
+ override val distance: UserActionDistance? = null
+
override fun toString(): String {
return "SceneKey(debugName=$debugName)"
}
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt
index 3873878..8552aaf 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt
@@ -64,8 +64,8 @@
@Stable
internal fun Modifier.multiPointerDraggable(
orientation: Orientation,
- enabled: Boolean,
- startDragImmediately: Boolean,
+ enabled: () -> Boolean,
+ startDragImmediately: () -> Boolean,
onDragStarted: (startedPosition: Offset, overSlop: Float, pointersDown: Int) -> Unit,
onDragDelta: (delta: Float) -> Unit,
onDragStopped: (velocity: Float) -> Unit,
@@ -83,8 +83,8 @@
private data class MultiPointerDraggableElement(
private val orientation: Orientation,
- private val enabled: Boolean,
- private val startDragImmediately: Boolean,
+ private val enabled: () -> Boolean,
+ private val startDragImmediately: () -> Boolean,
private val onDragStarted:
(startedPosition: Offset, overSlop: Float, pointersDown: Int) -> Unit,
private val onDragDelta: (Float) -> Unit,
@@ -110,10 +110,10 @@
}
}
-private class MultiPointerDraggableNode(
+internal class MultiPointerDraggableNode(
orientation: Orientation,
- enabled: Boolean,
- var startDragImmediately: Boolean,
+ enabled: () -> Boolean,
+ var startDragImmediately: () -> Boolean,
var onDragStarted: (startedPosition: Offset, overSlop: Float, pointersDown: Int) -> Unit,
var onDragDelta: (Float) -> Unit,
var onDragStopped: (velocity: Float) -> Unit,
@@ -122,7 +122,7 @@
private val delegate = delegate(SuspendingPointerInputModifierNode(pointerInputHandler))
private val velocityTracker = VelocityTracker()
- var enabled: Boolean = enabled
+ var enabled: () -> Boolean = enabled
set(value) {
// Reset the pointer input whenever enabled changed.
if (value != field) {
@@ -133,7 +133,7 @@
var orientation: Orientation = orientation
set(value) {
- // Reset the pointer input whenever enabled orientation.
+ // Reset the pointer input whenever orientation changed.
if (value != field) {
field = value
delegate.resetPointerInputHandler()
@@ -149,7 +149,7 @@
) = delegate.onPointerEvent(pointerEvent, pass, bounds)
private suspend fun PointerInputScope.pointerInput() {
- if (!enabled) {
+ if (!enabled()) {
return
}
@@ -163,8 +163,7 @@
val onDragEnd: () -> Unit = {
val maxFlingVelocity =
currentValueOf(LocalViewConfiguration).maximumFlingVelocity.let { max ->
- val maxF = max.toFloat()
- Velocity(maxF, maxF)
+ Velocity(max, max)
}
val velocity = velocityTracker.calculateVelocity(maxFlingVelocity)
@@ -183,7 +182,7 @@
detectDragGestures(
orientation = orientation,
- startDragImmediately = { startDragImmediately },
+ startDragImmediately = startDragImmediately,
onDragStart = onDragStart,
onDragEnd = onDragEnd,
onDragCancel = 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 f67df54..af51cee 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
@@ -38,7 +38,7 @@
val key: SceneKey,
layoutImpl: SceneTransitionLayoutImpl,
content: @Composable SceneScope.() -> Unit,
- actions: Map<UserAction, SceneKey>,
+ actions: Map<UserAction, UserActionResult>,
zIndex: Float,
) {
internal val scope = SceneScopeImpl(layoutImpl, this)
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneGestureHandler.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneGestureHandler.kt
index ff05478..aed04f6 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneGestureHandler.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneGestureHandler.kt
@@ -28,6 +28,8 @@
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.round
import com.android.compose.nestedscroll.PriorityNestedScrollConnection
@@ -75,23 +77,25 @@
internal var currentSource: Any? = null
- /** The [UserAction]s associated to the current swipe. */
- private var actionUpOrLeft: UserAction? = null
- private var actionDownOrRight: UserAction? = null
- private var actionUpOrLeftNoEdge: UserAction? = null
- private var actionDownOrRightNoEdge: UserAction? = null
- private var upOrLeftScene: SceneKey? = null
- private var downOrRightScene: SceneKey? = null
+ /** The [Swipes] associated to the current gesture. */
+ private var swipes: Swipes? = null
+
+ /** The [UserActionResult] associated to up and down swipes. */
+ private var upOrLeftResult: UserActionResult? = null
+ private var downOrRightResult: UserActionResult? = null
internal fun onDragStarted(pointersDown: Int, startedPosition: Offset?, overSlop: Float) {
if (isDrivingTransition) {
// This [transition] was already driving the animation: simply take over it.
// Stop animating and start from where the current offset.
swipeTransition.cancelOffsetAnimation()
- updateTargetScenes(swipeTransition._fromScene)
+ updateSwipesResults(swipeTransition._fromScene)
return
}
+ check(overSlop != 0f) {
+ "onDragStarted() called while isDrivingTransition=false overSlop=0f"
+ }
val transitionState = layoutState.transitionState
if (transitionState is TransitionState.Transition) {
// TODO(b/290184746): Better handle interruptions here if state != idle.
@@ -104,18 +108,25 @@
}
val fromScene = layoutImpl.scene(transitionState.currentScene)
- setCurrentActions(fromScene, startedPosition, pointersDown)
+ updateSwipes(fromScene, startedPosition, pointersDown)
val (targetScene, distance) =
- findTargetSceneAndDistance(fromScene, overSlop, updateScenes = true) ?: return
-
+ findTargetSceneAndDistance(fromScene, overSlop, updateSwipesResults = true) ?: return
updateTransition(SwipeTransition(fromScene, targetScene, distance), force = true)
}
- private fun setCurrentActions(fromScene: Scene, startedPosition: Offset?, pointersDown: Int) {
- val fromEdge =
+ private fun updateSwipes(fromScene: Scene, startedPosition: Offset?, pointersDown: Int) {
+ this.swipes = computeSwipes(fromScene, startedPosition, pointersDown)
+ }
+
+ private fun computeSwipes(
+ fromScene: Scene,
+ startedPosition: Offset?,
+ pointersDown: Int
+ ): Swipes {
+ val fromSource =
startedPosition?.let { position ->
- layoutImpl.edgeDetector.edge(
+ layoutImpl.swipeSourceDetector.source(
fromScene.targetSize,
position.round(),
layoutImpl.density,
@@ -131,7 +142,7 @@
Orientation.Vertical -> SwipeDirection.Up
},
pointerCount = pointersDown,
- fromEdge = fromEdge,
+ fromSource = fromSource,
)
val downOrRight =
@@ -142,33 +153,31 @@
Orientation.Vertical -> SwipeDirection.Down
},
pointerCount = pointersDown,
- fromEdge = fromEdge,
+ fromSource = fromSource,
)
- if (fromEdge == null) {
- actionUpOrLeft = null
- actionDownOrRight = null
- actionUpOrLeftNoEdge = upOrLeft
- actionDownOrRightNoEdge = downOrRight
+ return if (fromSource == null) {
+ Swipes(
+ upOrLeft = null,
+ downOrRight = null,
+ upOrLeftNoSource = upOrLeft,
+ downOrRightNoSource = downOrRight,
+ )
} else {
- actionUpOrLeft = upOrLeft
- actionDownOrRight = downOrRight
- actionUpOrLeftNoEdge = upOrLeft.copy(fromEdge = null)
- actionDownOrRightNoEdge = downOrRight.copy(fromEdge = null)
+ Swipes(
+ upOrLeft = upOrLeft,
+ downOrRight = downOrRight,
+ upOrLeftNoSource = upOrLeft.copy(fromSource = null),
+ downOrRightNoSource = downOrRight.copy(fromSource = null),
+ )
}
}
- /**
- * Use the layout size in the swipe orientation for swipe distance.
- *
- * TODO(b/290184746): Also handle custom distances for transitions. With smaller distances, we
- * will also have to make sure that we correctly handle overscroll.
- */
- private fun Scene.getAbsoluteDistance(): Float {
- return when (orientation) {
- Orientation.Horizontal -> targetSize.width
- Orientation.Vertical -> targetSize.height
- }.toFloat()
+ private fun Scene.getAbsoluteDistance(distance: UserActionDistance?): Float {
+ val targetSize = this.targetSize
+ return with(distance ?: DefaultSwipeDistance) {
+ layoutImpl.density.absoluteDistance(targetSize, orientation)
+ }
}
internal fun onDrag(delta: Float) {
@@ -183,7 +192,7 @@
findTargetSceneAndDistance(
fromScene,
swipeTransition.dragOffset,
- updateScenes = isNewFromScene,
+ updateSwipesResults = isNewFromScene,
)
?: run {
onDragStopped(delta, true)
@@ -200,9 +209,31 @@
}
}
- private fun updateTargetScenes(fromScene: Scene) {
- upOrLeftScene = fromScene.upOrLeft()
- downOrRightScene = fromScene.downOrRight()
+ private fun updateSwipesResults(fromScene: Scene) {
+ val (upOrLeftResult, downOrRightResult) =
+ swipesResults(
+ fromScene,
+ this.swipes ?: error("updateSwipes() should be called before updateSwipesResults()")
+ )
+
+ this.upOrLeftResult = upOrLeftResult
+ this.downOrRightResult = downOrRightResult
+ }
+
+ private fun swipesResults(
+ fromScene: Scene,
+ swipes: Swipes
+ ): Pair<UserActionResult?, UserActionResult?> {
+ val userActions = fromScene.userActions
+ fun sceneToSwipePair(swipe: Swipe?): UserActionResult? {
+ return userActions[swipe ?: return null]
+ }
+
+ val upOrLeftResult =
+ sceneToSwipePair(swipes.upOrLeft) ?: sceneToSwipePair(swipes.upOrLeftNoSource)
+ val downOrRightResult =
+ sceneToSwipePair(swipes.downOrRight) ?: sceneToSwipePair(swipes.downOrRightNoSource)
+ return Pair(upOrLeftResult, downOrRightResult)
}
/**
@@ -229,9 +260,9 @@
// If the offset is past the distance then let's change fromScene so that the user can swipe
// to the next screen or go back to the previous one.
val offset = swipeTransition.dragOffset
- return if (offset <= -absoluteDistance && upOrLeftScene == toScene.key) {
+ return if (offset <= -absoluteDistance && upOrLeftResult?.toScene == toScene.key) {
Pair(toScene, absoluteDistance)
- } else if (offset >= absoluteDistance && downOrRightScene == toScene.key) {
+ } else if (offset >= absoluteDistance && downOrRightResult?.toScene == toScene.key) {
Pair(toScene, -absoluteDistance)
} else {
Pair(fromScene, 0f)
@@ -244,31 +275,41 @@
* @param fromScene the scene from which we look for the target
* @param directionOffset signed float that indicates the direction. Positive is down or right
* negative is up or left.
- * @param updateScenes whether the target scenes should be updated to the current values held in
- * the Scenes map. Usually we don't want to update them while doing a drag, because this could
- * change the target scene (jump cutting) to a different scene, when some system state changed
- * the targets the background. However, an update is needed any time we calculate the targets
- * for a new fromScene.
+ * @param updateSwipesResults whether the target scenes should be updated to the current values
+ * held in the Scenes map. Usually we don't want to update them while doing a drag, because
+ * this could change the target scene (jump cutting) to a different scene, when some system
+ * state changed the targets the background. However, an update is needed any time we
+ * calculate the targets for a new fromScene.
* @return null when there are no targets in either direction. If one direction is null and you
* drag into the null direction this function will return the opposite direction, assuming
* that the users intention is to start the drag into the other direction eventually. If
* [directionOffset] is 0f and both direction are available, it will default to
- * [upOrLeftScene].
+ * [upOrLeftResult].
*/
private inline fun findTargetSceneAndDistance(
fromScene: Scene,
directionOffset: Float,
- updateScenes: Boolean,
+ updateSwipesResults: Boolean,
): Pair<Scene, Float>? {
- if (updateScenes) updateTargetScenes(fromScene)
- val absoluteDistance = fromScene.getAbsoluteDistance()
+ if (updateSwipesResults) updateSwipesResults(fromScene)
// Compute the target scene depending on the current offset.
return when {
- upOrLeftScene == null && downOrRightScene == null -> null
- (directionOffset < 0f && upOrLeftScene != null) || downOrRightScene == null ->
- Pair(layoutImpl.scene(upOrLeftScene!!), -absoluteDistance)
- else -> Pair(layoutImpl.scene(downOrRightScene!!), absoluteDistance)
+ upOrLeftResult == null && downOrRightResult == null -> null
+ (directionOffset < 0f && upOrLeftResult != null) || downOrRightResult == null ->
+ upOrLeftResult?.let { result ->
+ Pair(
+ layoutImpl.scene(result.toScene),
+ -fromScene.getAbsoluteDistance(result.distance)
+ )
+ }
+ else ->
+ downOrRightResult?.let { result ->
+ Pair(
+ layoutImpl.scene(result.toScene),
+ fromScene.getAbsoluteDistance(result.distance)
+ )
+ }
}
}
@@ -280,24 +321,25 @@
fromScene: Scene,
directionOffset: Float,
): Pair<Scene, Float>? {
- val absoluteDistance = fromScene.getAbsoluteDistance()
return when {
directionOffset > 0f ->
- upOrLeftScene?.let { Pair(layoutImpl.scene(it), -absoluteDistance) }
+ upOrLeftResult?.let { result ->
+ Pair(
+ layoutImpl.scene(result.toScene),
+ -fromScene.getAbsoluteDistance(result.distance),
+ )
+ }
directionOffset < 0f ->
- downOrRightScene?.let { Pair(layoutImpl.scene(it), absoluteDistance) }
+ downOrRightResult?.let { result ->
+ Pair(
+ layoutImpl.scene(result.toScene),
+ fromScene.getAbsoluteDistance(result.distance),
+ )
+ }
else -> null
}
}
- private fun Scene.upOrLeft(): SceneKey? {
- return userActions[actionUpOrLeft] ?: userActions[actionUpOrLeftNoEdge]
- }
-
- private fun Scene.downOrRight(): SceneKey? {
- return userActions[actionDownOrRight] ?: userActions[actionDownOrRightNoEdge]
- }
-
internal fun onDragStopped(velocity: Float, canChangeScene: Boolean) {
// The state was changed since the drag started; don't do anything.
if (!isDrivingTransition) {
@@ -515,6 +557,26 @@
companion object {
private const val TAG = "SceneGestureHandler"
}
+
+ private object DefaultSwipeDistance : UserActionDistance {
+ override fun Density.absoluteDistance(
+ fromSceneSize: IntSize,
+ orientation: Orientation,
+ ): Float {
+ return when (orientation) {
+ Orientation.Horizontal -> fromSceneSize.width
+ Orientation.Vertical -> fromSceneSize.height
+ }.toFloat()
+ }
+ }
+
+ /** The [Swipe] associated to a given fromScene, startedPosition and pointersDown. */
+ private class Swipes(
+ val upOrLeft: Swipe?,
+ val downOrRight: Swipe?,
+ val upOrLeftNoSource: Swipe?,
+ val downOrRightNoSource: Swipe?,
+ )
}
private class SceneDraggableHandler(
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 80f8c1c..7e0aa9c3 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
@@ -27,6 +27,10 @@
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.IntSize
/**
* [SceneTransitionLayout] is a container that automatically animates its content whenever its state
@@ -38,7 +42,8 @@
* UI code.
*
* @param state the state of this layout.
- * @param edgeDetector the edge detector used to detect which edge a swipe is started from, if any.
+ * @param swipeSourceDetector the edge detector used to detect which edge a swipe is started from,
+ * if any.
* @param transitionInterceptionThreshold used during a scene transition. For the scene to be
* intercepted, the progress value must be above the threshold, and below (1 - threshold).
* @param scenes the configuration of the different scenes of this layout.
@@ -48,14 +53,14 @@
fun SceneTransitionLayout(
state: SceneTransitionLayoutState,
modifier: Modifier = Modifier,
- edgeDetector: EdgeDetector = DefaultEdgeDetector,
+ swipeSourceDetector: SwipeSourceDetector = DefaultEdgeDetector,
@FloatRange(from = 0.0, to = 0.5) transitionInterceptionThreshold: Float = 0f,
scenes: SceneTransitionLayoutScope.() -> Unit,
) {
SceneTransitionLayoutForTesting(
state,
modifier,
- edgeDetector,
+ swipeSourceDetector,
transitionInterceptionThreshold,
onLayoutImpl = null,
scenes,
@@ -76,7 +81,8 @@
* This is called when the user commits a transition to a new scene because of a [UserAction], for
* 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 edgeDetector the edge detector used to detect which edge a swipe is started from, if any.
+ * @param swipeSourceDetector the source detector used to detect which source a swipe is started
+ * from, if any.
* @param transitionInterceptionThreshold used during a scene transition. For the scene to be
* intercepted, the progress value must be above the threshold, and below (1 - threshold).
* @param scenes the configuration of the different scenes of this layout.
@@ -87,7 +93,7 @@
onChangeScene: (SceneKey) -> Unit,
transitions: SceneTransitions,
modifier: Modifier = Modifier,
- edgeDetector: EdgeDetector = DefaultEdgeDetector,
+ swipeSourceDetector: SwipeSourceDetector = DefaultEdgeDetector,
@FloatRange(from = 0.0, to = 0.5) transitionInterceptionThreshold: Float = 0f,
scenes: SceneTransitionLayoutScope.() -> Unit,
) {
@@ -95,7 +101,7 @@
SceneTransitionLayout(
state,
modifier,
- edgeDetector,
+ swipeSourceDetector,
transitionInterceptionThreshold,
scenes,
)
@@ -113,7 +119,7 @@
*/
fun scene(
key: SceneKey,
- userActions: Map<UserAction, SceneKey> = emptyMap(),
+ userActions: Map<UserAction, UserActionResult> = emptyMap(),
content: @Composable SceneScope.() -> Unit,
)
}
@@ -335,7 +341,7 @@
data class Swipe(
val direction: SwipeDirection,
val pointerCount: Int = 1,
- val fromEdge: Edge? = null,
+ val fromSource: SwipeSource? = null,
) : UserAction {
companion object {
val Left = Swipe(SwipeDirection.Left)
@@ -353,6 +359,95 @@
}
/**
+ * The source of a Swipe.
+ *
+ * Important: This can be anything that can be returned by any [SwipeSourceDetector], but this must
+ * implement [equals] and [hashCode]. Note that those can be trivially implemented using data
+ * classes.
+ */
+interface SwipeSource {
+ // Require equals() and hashCode() to be implemented.
+ override fun equals(other: Any?): Boolean
+
+ override fun hashCode(): Int
+}
+
+interface SwipeSourceDetector {
+ /**
+ * Return the [SwipeSource] associated to [position] inside a layout of size [layoutSize], given
+ * [density] and [orientation].
+ */
+ fun source(
+ layoutSize: IntSize,
+ position: IntOffset,
+ density: Density,
+ orientation: Orientation,
+ ): SwipeSource?
+}
+
+/**
+ * The result of performing a [UserAction].
+ *
+ * Note: [UserActionResult] is implemented by [SceneKey], and you can also use [withDistance] to
+ * easily create a [UserActionResult] with a fixed distance:
+ * ```
+ * SceneTransitionLayout(...) {
+ * scene(
+ * Scenes.Foo,
+ * userActions =
+ * mapOf(
+ * Swipe.Right to Scene.Bar,
+ * Swipe.Down to Scene.Doe withDistance 100.dp,
+ * )
+ * )
+ * ) { ... }
+ * }
+ * ```
+ */
+interface UserActionResult {
+ /** The scene we should be transitioning to during the [UserAction]. */
+ val toScene: SceneKey
+
+ /**
+ * The distance the action takes to animate from 0% to 100%.
+ *
+ * If `null`, a default distance will be used that depends on the [UserAction] performed.
+ */
+ val distance: UserActionDistance?
+}
+
+interface UserActionDistance {
+ /**
+ * Return the **absolute** distance of the user action given the size of the scene we are
+ * animating from and the [orientation].
+ */
+ fun Density.absoluteDistance(fromSceneSize: IntSize, orientation: Orientation): Float
+}
+
+/**
+ * A utility function to make it possible to define user actions with a distance using the syntax
+ * `Swipe.Up to Scene.foo withDistance 100.dp`
+ */
+infix fun Pair<UserAction, SceneKey>.withDistance(
+ distance: Dp
+): Pair<UserAction, UserActionResult> {
+ val scene = second
+ val distance = FixedDistance(distance)
+ return first to
+ object : UserActionResult {
+ override val toScene: SceneKey = scene
+ override val distance: UserActionDistance = distance
+ }
+}
+
+/** The user action has a fixed [absoluteDistance]. */
+private class FixedDistance(private val distance: Dp) : UserActionDistance {
+ override fun Density.absoluteDistance(fromSceneSize: IntSize, orientation: Orientation): Float {
+ return distance.toPx()
+ }
+}
+
+/**
* An internal version of [SceneTransitionLayout] to be used for tests.
*
* Important: You should use this only in tests and if you need to access the underlying
@@ -362,7 +457,7 @@
internal fun SceneTransitionLayoutForTesting(
state: SceneTransitionLayoutState,
modifier: Modifier = Modifier,
- edgeDetector: EdgeDetector = DefaultEdgeDetector,
+ swipeSourceDetector: SwipeSourceDetector = DefaultEdgeDetector,
transitionInterceptionThreshold: Float = 0f,
onLayoutImpl: ((SceneTransitionLayoutImpl) -> Unit)? = null,
scenes: SceneTransitionLayoutScope.() -> Unit,
@@ -373,7 +468,7 @@
SceneTransitionLayoutImpl(
state = state as BaseSceneTransitionLayoutState,
density = density,
- edgeDetector = edgeDetector,
+ swipeSourceDetector = swipeSourceDetector,
transitionInterceptionThreshold = transitionInterceptionThreshold,
builder = scenes,
coroutineScope = coroutineScope,
@@ -394,7 +489,7 @@
}
layoutImpl.density = density
- layoutImpl.edgeDetector = edgeDetector
+ layoutImpl.swipeSourceDetector = swipeSourceDetector
}
layoutImpl.Content(modifier)
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 7cc9d26..8c5a472 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,7 +47,7 @@
internal class SceneTransitionLayoutImpl(
internal val state: BaseSceneTransitionLayoutState,
internal var density: Density,
- internal var edgeDetector: EdgeDetector,
+ internal var swipeSourceDetector: SwipeSourceDetector,
internal var transitionInterceptionThreshold: Float,
builder: SceneTransitionLayoutScope.() -> Unit,
private val coroutineScope: CoroutineScope,
@@ -140,7 +140,7 @@
object : SceneTransitionLayoutScope {
override fun scene(
key: SceneKey,
- userActions: Map<UserAction, SceneKey>,
+ userActions: Map<UserAction, UserActionResult>,
content: @Composable SceneScope.() -> Unit,
) {
scenesToRemove.remove(key)
@@ -229,8 +229,10 @@
// Handle back events.
// TODO(b/290184746): Make sure that this works with SystemUI once we use
// SceneTransitionLayout in Flexiglass.
- scene(state.transitionState.currentScene).userActions[Back]?.let { backScene ->
- BackHandler { with(state) { coroutineScope.onChangeScene(backScene) } }
+ scene(state.transitionState.currentScene).userActions[Back]?.let { result ->
+ // TODO(b/290184746): Handle predictive back and use result.distance if
+ // specified.
+ BackHandler { with(state) { coroutineScope.onChangeScene(result.toScene) } }
}
Box {
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 0d3bc7d..b9c4ac0 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
@@ -17,40 +17,98 @@
package com.android.compose.animation.scene
import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.runtime.Stable
import androidx.compose.ui.Modifier
+import androidx.compose.ui.input.pointer.PointerEvent
+import androidx.compose.ui.input.pointer.PointerEventPass
+import androidx.compose.ui.node.DelegatingNode
+import androidx.compose.ui.node.ModifierNodeElement
+import androidx.compose.ui.node.PointerInputModifierNode
+import androidx.compose.ui.unit.IntSize
/**
* Configures the swipeable behavior of a [SceneTransitionLayout] depending on the current state.
*/
+@Stable
internal fun Modifier.swipeToScene(gestureHandler: SceneGestureHandler): Modifier {
- /** Whether swipe should be enabled in the given [orientation]. */
- fun Scene.shouldEnableSwipes(orientation: Orientation): Boolean =
- userActions.keys.any { it is Swipe && it.direction.orientation == orientation }
+ return this.then(SwipeToSceneElement(gestureHandler))
+}
- val layoutImpl = gestureHandler.layoutImpl
- val currentScene = layoutImpl.scene(layoutImpl.state.transitionState.currentScene)
- val orientation = gestureHandler.orientation
- val canSwipe = currentScene.shouldEnableSwipes(orientation)
- val canOppositeSwipe =
- currentScene.shouldEnableSwipes(
- when (orientation) {
+private data class SwipeToSceneElement(
+ val gestureHandler: SceneGestureHandler,
+) : ModifierNodeElement<SwipeToSceneNode>() {
+ override fun create(): SwipeToSceneNode = SwipeToSceneNode(gestureHandler)
+
+ override fun update(node: SwipeToSceneNode) {
+ node.gestureHandler = gestureHandler
+ }
+}
+
+private class SwipeToSceneNode(
+ gestureHandler: SceneGestureHandler,
+) : DelegatingNode(), PointerInputModifierNode {
+ private val delegate =
+ delegate(
+ MultiPointerDraggableNode(
+ orientation = gestureHandler.orientation,
+ enabled = ::enabled,
+ startDragImmediately = ::startDragImmediately,
+ onDragStarted = gestureHandler.draggable::onDragStarted,
+ onDragDelta = gestureHandler.draggable::onDelta,
+ onDragStopped = gestureHandler.draggable::onDragStopped,
+ )
+ )
+
+ var gestureHandler: SceneGestureHandler = gestureHandler
+ set(value) {
+ if (value != field) {
+ field = value
+
+ // Make sure to update the delegate orientation. Note that this will automatically
+ // reset the underlying pointer input handler, so previous gestures will be
+ // cancelled.
+ delegate.orientation = value.orientation
+ }
+ }
+
+ override fun onPointerEvent(
+ pointerEvent: PointerEvent,
+ pass: PointerEventPass,
+ bounds: IntSize,
+ ) = delegate.onPointerEvent(pointerEvent, pass, bounds)
+
+ override fun onCancelPointerInput() = delegate.onCancelPointerInput()
+
+ private fun enabled(): Boolean {
+ return gestureHandler.isDrivingTransition ||
+ currentScene().shouldEnableSwipes(gestureHandler.orientation)
+ }
+
+ private fun currentScene(): Scene {
+ val layoutImpl = gestureHandler.layoutImpl
+ return layoutImpl.scene(layoutImpl.state.transitionState.currentScene)
+ }
+
+ /** Whether swipe should be enabled in the given [orientation]. */
+ private fun Scene.shouldEnableSwipes(orientation: Orientation): Boolean {
+ return userActions.keys.any { it is Swipe && it.direction.orientation == orientation }
+ }
+
+ private fun startDragImmediately(): Boolean {
+ // Immediately start the drag if this our transition is currently animating to a scene
+ // (i.e. the user released their input pointer after swiping in this orientation) and the
+ // user can't swipe in the other direction.
+ return gestureHandler.isDrivingTransition &&
+ gestureHandler.swipeTransition.isAnimatingOffset &&
+ !canOppositeSwipe()
+ }
+
+ private fun canOppositeSwipe(): Boolean {
+ val oppositeOrientation =
+ when (gestureHandler.orientation) {
Orientation.Vertical -> Orientation.Horizontal
Orientation.Horizontal -> Orientation.Vertical
}
- )
-
- return multiPointerDraggable(
- orientation = orientation,
- enabled = gestureHandler.isDrivingTransition || canSwipe,
- // Immediately start the drag if this our [transition] is currently animating to a scene
- // (i.e. the user released their input pointer after swiping in this orientation) and the
- // user can't swipe in the other direction.
- startDragImmediately =
- gestureHandler.isDrivingTransition &&
- gestureHandler.swipeTransition.isAnimatingOffset &&
- !canOppositeSwipe,
- onDragStarted = gestureHandler.draggable::onDragStarted,
- onDragDelta = gestureHandler.draggable::onDelta,
- onDragStopped = gestureHandler.draggable::onDragStopped,
- )
+ return currentScene().shouldEnableSwipes(oppositeOrientation)
+ }
}
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt
index dc8505c..a764a527 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt
@@ -320,11 +320,3 @@
anchorHeight: Boolean = true,
)
}
-
-/** The edge of a [SceneTransitionLayout]. */
-enum class Edge {
- Left,
- Right,
- Top,
- Bottom,
-}
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/nestedscroll/PriorityNestedScrollConnection.kt b/packages/SystemUI/compose/scene/src/com/android/compose/nestedscroll/PriorityNestedScrollConnection.kt
index 2841bcf..ac11d30 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/nestedscroll/PriorityNestedScrollConnection.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/nestedscroll/PriorityNestedScrollConnection.kt
@@ -22,6 +22,7 @@
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
import androidx.compose.ui.unit.Velocity
import com.android.compose.ui.util.SpaceVectorConverter
+import kotlin.math.sign
/**
* This [NestedScrollConnection] waits for a child to scroll ([onPreScroll] or [onPostScroll]), and
@@ -117,7 +118,12 @@
return Velocity.Zero
}
- onPriorityStart(available = Offset.Zero)
+ // The offset passed to onPriorityStart() must be != 0f, so we create a small offset of 1px
+ // given the available velocity.
+ // TODO(b/291053278): Remove canStartPostFling() and instead make it possible to define the
+ // overscroll behavior on the Scene level.
+ val smallOffset = Offset(available.x.sign, available.y.sign)
+ onPriorityStart(available = smallOffset)
// This is the last event of a scroll gesture.
return onPriorityStop(available)
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
index a68282a..cceaf57 100644
--- 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
@@ -35,7 +35,7 @@
@Test
fun horizontalEdges() {
fun horizontalEdge(position: Int): Edge? =
- detector.edge(
+ detector.source(
layoutSize,
position = IntOffset(position, 0),
density,
@@ -53,7 +53,7 @@
@Test
fun verticalEdges() {
fun verticalEdge(position: Int): Edge? =
- detector.edge(
+ detector.source(
layoutSize,
position = IntOffset(0, position),
density,
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 066a3e4..88363ad 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
@@ -77,7 +77,7 @@
userActions =
mapOf(
Swipe.Up to SceneB,
- Swipe(SwipeDirection.Up, fromEdge = Edge.Bottom) to SceneA
+ Swipe(SwipeDirection.Up, fromSource = Edge.Bottom) to SceneA
),
) {
Text("SceneC")
@@ -90,7 +90,7 @@
SceneTransitionLayoutImpl(
state = layoutState,
density = Density(1f),
- edgeDetector = DefaultEdgeDetector,
+ swipeSourceDetector = DefaultEdgeDetector,
transitionInterceptionThreshold = transitionInterceptionThreshold,
builder = scenesBuilder,
coroutineScope = coroutineScope,
@@ -192,16 +192,14 @@
@Test
fun onDragStarted_shouldStartATransition() = runGestureTest {
- draggable.onDragStarted()
+ draggable.onDragStarted(overSlop = down(0.1f))
assertTransition(currentScene = SceneA)
}
@Test
fun afterSceneTransitionIsStarted_interceptDragEvents() = runGestureTest {
- draggable.onDragStarted()
+ draggable.onDragStarted(overSlop = down(0.1f))
assertTransition(currentScene = SceneA)
-
- draggable.onDelta(pixels = down(0.1f))
assertThat(progress).isEqualTo(0.1f)
draggable.onDelta(pixels = down(0.1f))
@@ -210,10 +208,7 @@
@Test
fun onDragStoppedAfterDrag_velocityLowerThanThreshold_remainSameScene() = runGestureTest {
- draggable.onDragStarted()
- assertTransition(currentScene = SceneA)
-
- draggable.onDelta(pixels = down(0.1f))
+ draggable.onDragStarted(overSlop = down(0.1f))
assertTransition(currentScene = SceneA)
draggable.onDragStopped(
@@ -228,10 +223,7 @@
@Test
fun onDragStoppedAfterDrag_velocityAtLeastThreshold_goToNextScene() = runGestureTest {
- draggable.onDragStarted()
- assertTransition(currentScene = SceneA)
-
- draggable.onDelta(pixels = down(0.1f))
+ draggable.onDragStarted(overSlop = down(0.1f))
assertTransition(currentScene = SceneA)
draggable.onDragStopped(velocity = velocityThreshold)
@@ -245,7 +237,7 @@
@Test
fun onDragStoppedAfterStarted_returnToIdle() = runGestureTest {
- draggable.onDragStarted()
+ draggable.onDragStarted(overSlop = down(0.1f))
assertTransition(currentScene = SceneA)
draggable.onDragStopped(velocity = 0f)
@@ -256,8 +248,7 @@
@Test
fun onDragReversedDirection_changeToScene() = runGestureTest {
// Drag A -> B with progress 0.6
- draggable.onDragStarted()
- draggable.onDelta(up(0.6f))
+ draggable.onDragStarted(overSlop = up(0.6f))
assertTransition(
currentScene = SceneA,
fromScene = SceneA,
@@ -366,8 +357,7 @@
@Test
fun onAccelaratedScroll_scrollToThirdScene() = runGestureTest {
// Drag A -> B with progress 0.2
- draggable.onDragStarted()
- draggable.onDelta(up(0.2f))
+ draggable.onDragStarted(overSlop = up(0.2f))
assertTransition(
currentScene = SceneA,
fromScene = SceneA,
@@ -401,9 +391,7 @@
@Test
fun onAccelaratedScrollBothTargetsBecomeNull_settlesToIdle() = runGestureTest {
- draggable.onDragStarted()
- draggable.onDelta(up(0.2f))
-
+ draggable.onDragStarted(overSlop = up(0.2f))
draggable.onDelta(up(0.2f))
draggable.onDragStopped(velocity = -velocityThreshold)
assertTransition(currentScene = SceneB, fromScene = SceneA, toScene = SceneB)
@@ -459,16 +447,14 @@
draggable.onDragStopped(down(0.1f))
// now target changed to C for new drag that started before previous drag settled to Idle
- draggable.onDragStarted(up(0.1f))
+ draggable.onDragStarted(overSlop = 0f)
+ draggable.onDelta(up(0.1f))
assertTransition(fromScene = SceneA, toScene = SceneC, progress = 0.3f)
}
@Test
fun startGestureDuringAnimatingOffset_shouldImmediatelyStopTheAnimation() = runGestureTest {
- draggable.onDragStarted()
- assertTransition(currentScene = SceneA)
-
- draggable.onDelta(pixels = down(0.1f))
+ draggable.onDragStarted(overSlop = down(0.1f))
assertTransition(currentScene = SceneA)
draggable.onDragStopped(
@@ -759,10 +745,8 @@
@Test
fun startNestedScrollWhileDragging() = runGestureTest {
val nestedScroll = nestedScrollConnection(nestedScrollBehavior = EdgeAlways)
- draggable.onDragStarted()
+ draggable.onDragStarted(overSlop = down(0.1f))
assertTransition(currentScene = SceneA)
-
- draggable.onDelta(down(0.1f))
assertThat(progress).isEqualTo(0.1f)
// now we can intercept the scroll events
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 1ec3c8b..9403358 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
@@ -17,6 +17,7 @@
package com.android.compose.animation.scene
import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
@@ -89,8 +90,8 @@
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,
+ Swipe(SwipeDirection.Right, fromSource = Edge.Left) to TestScenes.SceneB,
+ Swipe(SwipeDirection.Down, fromSource = Edge.Top) to TestScenes.SceneB,
),
) {
Box(Modifier.fillMaxSize())
@@ -349,4 +350,46 @@
assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Idle::class.java)
assertThat(layoutState.transitionState.currentScene).isEqualTo(TestScenes.SceneC)
}
+
+ @Test
+ fun swipeDistance() {
+ // 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
+
+ val layoutState = MutableSceneTransitionLayoutState(TestScenes.SceneA)
+ val verticalSwipeDistance = 50.dp
+ assertThat(verticalSwipeDistance).isNotEqualTo(LayoutHeight)
+
+ rule.setContent {
+ touchSlop = LocalViewConfiguration.current.touchSlop
+
+ SceneTransitionLayout(
+ state = layoutState,
+ modifier = Modifier.size(LayoutWidth, LayoutHeight)
+ ) {
+ scene(
+ TestScenes.SceneA,
+ userActions =
+ mapOf(Swipe.Down to TestScenes.SceneB withDistance verticalSwipeDistance),
+ ) {
+ Spacer(Modifier.fillMaxSize())
+ }
+ scene(TestScenes.SceneB) { Spacer(Modifier.fillMaxSize()) }
+ }
+ }
+
+ assertThat(layoutState.currentTransition).isNull()
+
+ // Swipe by half of verticalSwipeDistance.
+ rule.onRoot().performTouchInput {
+ down(middleTop)
+ moveBy(Offset(0f, touchSlop + (verticalSwipeDistance / 2).toPx()), delayMillis = 1_000)
+ }
+
+ // We should be at 50%
+ val transition = layoutState.currentTransition
+ assertThat(transition).isNotNull()
+ assertThat(transition!!.progress).isEqualTo(0.5f)
+ }
}
diff --git a/packages/SystemUI/customization/Android.bp b/packages/SystemUI/customization/Android.bp
index 1d18496..81b5bd4 100644
--- a/packages/SystemUI/customization/Android.bp
+++ b/packages/SystemUI/customization/Android.bp
@@ -34,15 +34,19 @@
"PluginCoreLib",
"SystemUIPluginLib",
"SystemUIUnfoldLib",
- "androidx.dynamicanimation_dynamicanimation",
+ "kotlinx_coroutines",
+ "dagger2",
+ "jsr330",
+ ],
+ libs: [
+ // Keep android-specific libraries as libs instead of static_libs, so that they don't break
+ // things when included as transitive dependencies in robolectric targets.
"androidx.concurrent_concurrent-futures",
+ "androidx.dynamicanimation_dynamicanimation",
"androidx.lifecycle_lifecycle-runtime-ktx",
"androidx.lifecycle_lifecycle-viewmodel-ktx",
"androidx.recyclerview_recyclerview",
"kotlinx_coroutines_android",
- "kotlinx_coroutines",
- "dagger2",
- "jsr330",
],
resource_dirs: [
"res",
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/AccessibilityQsShortcutsRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/AccessibilityQsShortcutsRepositoryImplTest.kt
new file mode 100644
index 0000000..9287edf
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/AccessibilityQsShortcutsRepositoryImplTest.kt
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.accessibility.data.repository
+
+import android.provider.Settings
+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.util.settings.FakeSettings
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class AccessibilityQsShortcutsRepositoryImplTest : SysuiTestCase() {
+ private val testDispatcher = StandardTestDispatcher()
+ private val testScope = TestScope(testDispatcher)
+ private val secureSettings = FakeSettings()
+
+ private val userA11yQsShortcutsRepositoryFactory =
+ object : UserA11yQsShortcutsRepository.Factory {
+ override fun create(userId: Int): UserA11yQsShortcutsRepository {
+ return UserA11yQsShortcutsRepository(
+ userId,
+ secureSettings,
+ testScope.backgroundScope,
+ testDispatcher,
+ )
+ }
+ }
+
+ private val underTest =
+ AccessibilityQsShortcutsRepositoryImpl(userA11yQsShortcutsRepositoryFactory)
+
+ @Test
+ fun a11yQsShortcutTargetsForCorrectUsers() =
+ testScope.runTest {
+ val user0 = 0
+ val targetsForUser0 = setOf("a", "b", "c")
+ val user1 = 1
+ val targetsForUser1 = setOf("A")
+ val targetsFromUser0 by collectLastValue(underTest.a11yQsShortcutTargets(user0))
+ val targetsFromUser1 by collectLastValue(underTest.a11yQsShortcutTargets(user1))
+
+ storeA11yQsShortcutTargetsForUser(targetsForUser0, user0)
+ storeA11yQsShortcutTargetsForUser(targetsForUser1, user1)
+
+ assertThat(targetsFromUser0).isEqualTo(targetsForUser0)
+ assertThat(targetsFromUser1).isEqualTo(targetsForUser1)
+ }
+
+ private fun storeA11yQsShortcutTargetsForUser(a11yQsTargets: Set<String>, forUser: Int) {
+ secureSettings.putStringForUser(
+ SETTING_NAME,
+ a11yQsTargets.joinToString(separator = ":"),
+ forUser
+ )
+ }
+
+ companion object {
+ private const val SETTING_NAME = Settings.Secure.ACCESSIBILITY_QS_TARGETS
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/UserA11yQsShortcutsRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/UserA11yQsShortcutsRepositoryTest.kt
new file mode 100644
index 0000000..ce22e28
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/UserA11yQsShortcutsRepositoryTest.kt
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.accessibility.data.repository
+
+import android.provider.Settings
+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.util.settings.FakeSettings
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class UserA11yQsShortcutsRepositoryTest : SysuiTestCase() {
+ private val secureSettings = FakeSettings()
+ private val testDispatcher = StandardTestDispatcher()
+ private val testScope = TestScope(testDispatcher)
+
+ private val underTest =
+ UserA11yQsShortcutsRepository(
+ USER_ID,
+ secureSettings,
+ testScope.backgroundScope,
+ testDispatcher
+ )
+
+ @Test
+ fun targetsMatchesSetting() =
+ testScope.runTest {
+ val observedTargets by collectLastValue(underTest.targets)
+ val a11yQsTargets = setOf("a", "b", "c")
+ secureSettings.putStringForUser(
+ SETTING_NAME,
+ a11yQsTargets.joinToString(SEPARATOR),
+ USER_ID
+ )
+
+ assertThat(observedTargets).isEqualTo(a11yQsTargets)
+ }
+
+ companion object {
+ private const val USER_ID = 0
+ private const val SEPARATOR = ":"
+ private const val SETTING_NAME = Settings.Secure.ACCESSIBILITY_QS_TARGETS
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/domain/interactor/SideFpsSensorInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/domain/interactor/SideFpsSensorInteractorTest.kt
index 8d6d052..a862112 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/domain/interactor/SideFpsSensorInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/domain/interactor/SideFpsSensorInteractorTest.kt
@@ -37,6 +37,8 @@
import com.android.systemui.biometrics.shared.model.FingerprintSensorType
import com.android.systemui.biometrics.shared.model.SensorStrength
import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.keyguard.data.repository.biometricSettingsRepository
+import com.android.systemui.keyguard.data.repository.fakeBiometricSettingsRepository
import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
import com.android.systemui.keyguard.shared.model.KeyguardState
@@ -112,6 +114,7 @@
windowManager,
displayStateInteractor,
Optional.of(fingerprintInteractiveToAuthProvider),
+ kosmos.biometricSettingsRepository,
kosmos.keyguardTransitionInteractor,
SideFpsLogger(logcatLogBuffer("SfpsLogger"))
)
@@ -420,6 +423,7 @@
@Test
fun isProlongedTouchRequiredForAuthentication_dependsOnSettingsToggle() =
testScope.runTest {
+ kosmos.fakeBiometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(true)
val isEnabled by collectLastValue(underTest.isProlongedTouchRequiredForAuthentication)
setupFingerprint(FingerprintSensorType.POWER_BUTTON)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalRepositoryImplTest.kt
index 81d5344..bd9ca30 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalRepositoryImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalRepositoryImplTest.kt
@@ -16,24 +16,28 @@
package com.android.systemui.communal.data.repository
+import android.content.pm.UserInfo
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
+import com.android.systemui.Flags.FLAG_COMMUNAL_HUB
import com.android.systemui.SysuiTestCase
import com.android.systemui.communal.shared.model.CommunalSceneKey
import com.android.systemui.communal.shared.model.ObservableCommunalTransitionState
import com.android.systemui.coroutines.collectLastValue
-import com.android.systemui.flags.FakeFeatureFlagsClassic
import com.android.systemui.flags.Flags
-import com.android.systemui.scene.data.repository.SceneContainerRepository
+import com.android.systemui.flags.fakeFeatureFlagsClassic
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.kosmos.testScope
import com.android.systemui.scene.data.repository.sceneContainerRepository
-import com.android.systemui.scene.shared.flag.FakeSceneContainerFlags
+import com.android.systemui.scene.shared.flag.fakeSceneContainerFlags
import com.android.systemui.scene.shared.model.SceneKey
import com.android.systemui.scene.shared.model.SceneModel
import com.android.systemui.testKosmos
+import com.android.systemui.user.data.repository.FakeUserRepository
+import com.android.systemui.user.data.repository.fakeUserRepository
+import com.android.systemui.util.settings.FakeSettings
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.flow.flowOf
-import kotlinx.coroutines.test.StandardTestDispatcher
-import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
@@ -44,19 +48,23 @@
class CommunalRepositoryImplTest : SysuiTestCase() {
private lateinit var underTest: CommunalRepositoryImpl
- private val testDispatcher = StandardTestDispatcher()
- private val testScope = TestScope(testDispatcher)
+ private lateinit var secureSettings: FakeSettings
+ private lateinit var userRepository: FakeUserRepository
- private lateinit var featureFlagsClassic: FakeFeatureFlagsClassic
- private lateinit var sceneContainerRepository: SceneContainerRepository
+ private val kosmos = testKosmos()
+ private val testScope = kosmos.testScope
+ private val sceneContainerRepository = kosmos.sceneContainerRepository
@Before
fun setUp() {
- val kosmos = testKosmos()
- sceneContainerRepository = kosmos.sceneContainerRepository
- featureFlagsClassic = FakeFeatureFlagsClassic()
+ secureSettings = FakeSettings()
+ userRepository = kosmos.fakeUserRepository
- featureFlagsClassic.set(Flags.COMMUNAL_SERVICE_ENABLED, true)
+ val listOfUserInfo = listOf(MAIN_USER_INFO)
+ userRepository.setUserInfos(listOfUserInfo)
+
+ kosmos.fakeFeatureFlagsClassic.apply { set(Flags.COMMUNAL_SERVICE_ENABLED, true) }
+ mSetFlagsRule.enableFlags(FLAG_COMMUNAL_HUB)
underTest = createRepositoryImpl(false)
}
@@ -64,9 +72,13 @@
private fun createRepositoryImpl(sceneContainerEnabled: Boolean): CommunalRepositoryImpl {
return CommunalRepositoryImpl(
testScope.backgroundScope,
- featureFlagsClassic,
- FakeSceneContainerFlags(enabled = sceneContainerEnabled),
+ testScope.backgroundScope,
+ kosmos.testDispatcher,
+ kosmos.fakeFeatureFlagsClassic,
+ kosmos.fakeSceneContainerFlags.apply { enabled = sceneContainerEnabled },
sceneContainerRepository,
+ kosmos.fakeUserRepository,
+ secureSettings,
)
}
@@ -147,4 +159,29 @@
assertThat(transitionState)
.isEqualTo(ObservableCommunalTransitionState.Idle(CommunalSceneKey.DEFAULT))
}
+
+ @Test
+ fun communalEnabledState_false_whenGlanceableHubSettingFalse() =
+ testScope.runTest {
+ userRepository.setSelectedUserInfo(MAIN_USER_INFO)
+ secureSettings.putIntForUser(GLANCEABLE_HUB_ENABLED, 0, MAIN_USER_INFO.id)
+
+ val communalEnabled by collectLastValue(underTest.communalEnabledState)
+ assertThat(communalEnabled).isFalse()
+ }
+
+ @Test
+ fun communalEnabledState_true_whenGlanceableHubSettingTrue() =
+ testScope.runTest {
+ userRepository.setSelectedUserInfo(MAIN_USER_INFO)
+ secureSettings.putIntForUser(GLANCEABLE_HUB_ENABLED, 1, MAIN_USER_INFO.id)
+
+ val communalEnabled by collectLastValue(underTest.communalEnabledState)
+ assertThat(communalEnabled).isTrue()
+ }
+
+ companion object {
+ private const val GLANCEABLE_HUB_ENABLED = "glanceable_hub_enabled"
+ private val MAIN_USER_INFO = UserInfo(0, "primary", UserInfo.FLAG_MAIN)
+ }
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryImplTest.kt
index bb3429e..c979ca6 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryImplTest.kt
@@ -30,7 +30,6 @@
import com.android.systemui.communal.shared.CommunalWidgetHost
import com.android.systemui.communal.shared.model.CommunalWidgetContentModel
import com.android.systemui.communal.widgets.CommunalAppWidgetHost
-import com.android.systemui.communal.widgets.WidgetConfigurator
import com.android.systemui.communal.widgets.widgetConfiguratorFail
import com.android.systemui.communal.widgets.widgetConfiguratorSuccess
import com.android.systemui.coroutines.collectLastValue
@@ -45,8 +44,7 @@
import com.google.common.truth.Truth.assertThat
import java.util.Optional
import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.flow.flowOf
-import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Before
@@ -62,24 +60,17 @@
@SmallTest
@RunWith(AndroidJUnit4::class)
class CommunalWidgetRepositoryImplTest : SysuiTestCase() {
- @Mock private lateinit var appWidgetManagerOptional: Optional<AppWidgetManager>
-
@Mock private lateinit var appWidgetManager: AppWidgetManager
-
@Mock private lateinit var appWidgetHost: CommunalAppWidgetHost
-
@Mock private lateinit var stopwatchProviderInfo: AppWidgetProviderInfo
-
@Mock private lateinit var providerInfoA: AppWidgetProviderInfo
-
@Mock private lateinit var communalWidgetHost: CommunalWidgetHost
-
@Mock private lateinit var communalWidgetDao: CommunalWidgetDao
private lateinit var logBuffer: LogBuffer
+ private lateinit var fakeWidgets: MutableStateFlow<Map<CommunalItemRank, CommunalWidgetItem>>
private val kosmos = testKosmos()
- private val testDispatcher = kosmos.testDispatcher
private val testScope = kosmos.testScope
private val fakeAllowlist =
@@ -94,7 +85,7 @@
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
-
+ fakeWidgets = MutableStateFlow(emptyMap())
logBuffer = logcatLogBuffer(name = "CommunalWidgetRepoImplTest")
setAppWidgetIds(emptyList())
@@ -102,13 +93,11 @@
overrideResource(R.array.config_communalWidgetAllowlist, fakeAllowlist.toTypedArray())
whenever(stopwatchProviderInfo.loadLabel(any())).thenReturn("Stopwatch")
- whenever(communalWidgetDao.getWidgets()).thenReturn(flowOf(emptyMap()))
- whenever(appWidgetManagerOptional.isPresent).thenReturn(true)
- whenever(appWidgetManagerOptional.get()).thenReturn(appWidgetManager)
+ whenever(communalWidgetDao.getWidgets()).thenReturn(fakeWidgets)
underTest =
CommunalWidgetRepositoryImpl(
- appWidgetManagerOptional,
+ Optional.of(appWidgetManager),
appWidgetHost,
testScope.backgroundScope,
kosmos.testDispatcher,
@@ -119,30 +108,16 @@
}
@Test
- fun neverQueryDbForWidgets_whenHostIsInactive() =
+ fun communalWidgets_queryWidgetsFromDb() =
testScope.runTest {
- underTest.updateAppWidgetHostActive(false)
- underTest.communalWidgets.launchIn(testScope.backgroundScope)
- runCurrent()
-
- verify(communalWidgetDao, never()).getWidgets()
- }
-
- @Test
- fun communalWidgets_whenHostIsActive_queryWidgetsFromDb() =
- testScope.runTest {
- underTest.updateAppWidgetHostActive(true)
-
val communalItemRankEntry = CommunalItemRank(uid = 1L, rank = 1)
val communalWidgetItemEntry = CommunalWidgetItem(uid = 1L, 1, "pk_name/cls_name", 1L)
- whenever(communalWidgetDao.getWidgets())
- .thenReturn(flowOf(mapOf(communalItemRankEntry to communalWidgetItemEntry)))
+ fakeWidgets.value = mapOf(communalItemRankEntry to communalWidgetItemEntry)
whenever(appWidgetManager.getAppWidgetInfo(anyInt())).thenReturn(providerInfoA)
installedProviders(listOf(stopwatchProviderInfo))
val communalWidgets by collectLastValue(underTest.communalWidgets)
- runCurrent()
verify(communalWidgetDao).getWidgets()
assertThat(communalWidgets)
.containsExactly(
@@ -157,8 +132,6 @@
@Test
fun addWidget_allocateId_bindWidget_andAddToDb() =
testScope.runTest {
- underTest.updateAppWidgetHostActive(true)
-
val provider = ComponentName("pkg_name", "cls_name")
val id = 1
val priority = 1
@@ -176,8 +149,6 @@
@Test
fun addWidget_configurationFails_doNotAddWidgetToDb() =
testScope.runTest {
- underTest.updateAppWidgetHostActive(true)
-
val provider = ComponentName("pkg_name", "cls_name")
val id = 1
val priority = 1
@@ -195,23 +166,13 @@
@Test
fun addWidget_configurationThrowsError_doNotAddWidgetToDb() =
testScope.runTest {
- underTest.updateAppWidgetHostActive(true)
-
val provider = ComponentName("pkg_name", "cls_name")
val id = 1
val priority = 1
whenever(communalWidgetHost.getAppWidgetInfo(id))
.thenReturn(PROVIDER_INFO_REQUIRES_CONFIGURATION)
whenever(communalWidgetHost.allocateIdAndBindWidget(provider)).thenReturn(id)
- underTest.addWidget(
- provider,
- priority,
- object : WidgetConfigurator {
- override suspend fun configureWidget(appWidgetId: Int): Boolean {
- throw IllegalStateException("some error")
- }
- }
- )
+ underTest.addWidget(provider, priority) { throw IllegalStateException("some error") }
runCurrent()
verify(communalWidgetHost).allocateIdAndBindWidget(provider)
@@ -222,8 +183,6 @@
@Test
fun addWidget_configurationNotRequired_doesNotConfigure_addWidgetToDb() =
testScope.runTest {
- underTest.updateAppWidgetHostActive(true)
-
val provider = ComponentName("pkg_name", "cls_name")
val id = 1
val priority = 1
@@ -241,8 +200,6 @@
@Test
fun deleteWidget_removeWidgetId_andDeleteFromDb() =
testScope.runTest {
- underTest.updateAppWidgetHostActive(true)
-
val id = 1
underTest.deleteWidget(id)
runCurrent()
@@ -254,8 +211,6 @@
@Test
fun reorderWidgets_queryDb() =
testScope.runTest {
- underTest.updateAppWidgetHostActive(true)
-
val widgetIdToPriorityMap = mapOf(104 to 1, 103 to 2, 101 to 3)
underTest.updateWidgetOrder(widgetIdToPriorityMap)
runCurrent()
@@ -263,28 +218,6 @@
verify(communalWidgetDao).updateWidgetOrder(widgetIdToPriorityMap)
}
- @Test
- fun appWidgetHost_startListening() =
- testScope.runTest {
- verify(appWidgetHost, never()).startListening()
-
- underTest.updateAppWidgetHostActive(true)
-
- verify(appWidgetHost).startListening()
- }
-
- @Test
- fun appWidgetHost_stopListening() =
- testScope.runTest {
- underTest.updateAppWidgetHostActive(true)
-
- verify(appWidgetHost).startListening()
-
- underTest.updateAppWidgetHostActive(false)
-
- verify(appWidgetHost).stopListening()
- }
-
private fun installedProviders(providers: List<AppWidgetProviderInfo>) {
whenever(appWidgetManager.installedProviders).thenReturn(providers)
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorCommunalDisabledTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorCommunalDisabledTest.kt
index e821673..6a3fc2a 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorCommunalDisabledTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorCommunalDisabledTest.kt
@@ -80,15 +80,4 @@
assertThat(isCommunalAvailable).isFalse()
}
-
- @Test
- fun updateAppWidgetHostActive_whenStorageUnlock_false() =
- testScope.runTest {
- assertThat(widgetRepository.isHostActive()).isFalse()
-
- keyguardRepository.setIsEncryptedOrLockdown(false)
- runCurrent()
-
- assertThat(widgetRepository.isHostActive()).isFalse()
- }
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt
index f0b941a..a083e7c 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt
@@ -18,6 +18,7 @@
package com.android.systemui.communal.domain.interactor
import android.app.smartspace.SmartspaceTarget
+import android.content.pm.UserInfo
import android.provider.Settings.Secure.HUB_MODE_TUTORIAL_COMPLETED
import android.widget.RemoteViews
import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -46,6 +47,8 @@
import com.android.systemui.smartspace.data.repository.FakeSmartspaceRepository
import com.android.systemui.smartspace.data.repository.fakeSmartspaceRepository
import com.android.systemui.testKosmos
+import com.android.systemui.user.data.repository.FakeUserRepository
+import com.android.systemui.user.data.repository.fakeUserRepository
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
@@ -57,8 +60,10 @@
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
+import org.mockito.Mock
import org.mockito.Mockito.mock
import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
/**
* This class of test cases assume that communal is enabled. For disabled cases, see
@@ -68,6 +73,9 @@
@OptIn(ExperimentalCoroutinesApi::class)
@RunWith(AndroidJUnit4::class)
class CommunalInteractorTest : SysuiTestCase() {
+ @Mock private lateinit var mainUser: UserInfo
+ @Mock private lateinit var secondaryUser: UserInfo
+
private val kosmos = testKosmos()
private val testScope = kosmos.testScope
@@ -76,6 +84,7 @@
private lateinit var mediaRepository: FakeCommunalMediaRepository
private lateinit var widgetRepository: FakeCommunalWidgetRepository
private lateinit var smartspaceRepository: FakeSmartspaceRepository
+ private lateinit var userRepository: FakeUserRepository
private lateinit var keyguardRepository: FakeKeyguardRepository
private lateinit var communalPrefsRepository: FakeCommunalPrefsRepository
private lateinit var editWidgetsActivityStarter: EditWidgetsActivityStarter
@@ -84,15 +93,22 @@
@Before
fun setUp() {
+ MockitoAnnotations.initMocks(this)
+
tutorialRepository = kosmos.fakeCommunalTutorialRepository
communalRepository = kosmos.fakeCommunalRepository
mediaRepository = kosmos.fakeCommunalMediaRepository
widgetRepository = kosmos.fakeCommunalWidgetRepository
smartspaceRepository = kosmos.fakeSmartspaceRepository
+ userRepository = kosmos.fakeUserRepository
keyguardRepository = kosmos.fakeKeyguardRepository
editWidgetsActivityStarter = kosmos.editWidgetsActivityStarter
communalPrefsRepository = kosmos.fakeCommunalPrefsRepository
+ whenever(mainUser.isMain).thenReturn(true)
+ whenever(secondaryUser.isMain).thenReturn(false)
+ userRepository.setUserInfos(listOf(mainUser, secondaryUser))
+
underTest = kosmos.communalInteractor
}
@@ -101,39 +117,58 @@
testScope.runTest { assertThat(underTest.isCommunalEnabled).isTrue() }
@Test
- fun isCommunalAvailable_trueWhenStorageUnlock() =
+ fun isCommunalAvailable_storageUnlockedAndMainUser_true() =
testScope.runTest {
val isAvailable by collectLastValue(underTest.isCommunalAvailable)
assertThat(isAvailable).isFalse()
keyguardRepository.setIsEncryptedOrLockdown(false)
+ userRepository.setSelectedUserInfo(mainUser)
+ keyguardRepository.setKeyguardShowing(true)
runCurrent()
assertThat(isAvailable).isTrue()
}
@Test
- fun isCommunalAvailable_whenStorageUnlock_true() =
+ fun isCommunalAvailable_storageLockedAndMainUser_false() =
+ testScope.runTest {
+ val isAvailable by collectLastValue(underTest.isCommunalAvailable)
+ assertThat(isAvailable).isFalse()
+
+ keyguardRepository.setIsEncryptedOrLockdown(true)
+ userRepository.setSelectedUserInfo(mainUser)
+ runCurrent()
+
+ assertThat(isAvailable).isFalse()
+ }
+
+ @Test
+ fun isCommunalAvailable_storageUnlockedAndSecondaryUser_false() =
testScope.runTest {
val isAvailable by collectLastValue(underTest.isCommunalAvailable)
assertThat(isAvailable).isFalse()
keyguardRepository.setIsEncryptedOrLockdown(false)
+ userRepository.setSelectedUserInfo(secondaryUser)
+ keyguardRepository.setKeyguardShowing(true)
runCurrent()
- assertThat(isAvailable).isTrue()
+ assertThat(isAvailable).isFalse()
}
@Test
- fun updateAppWidgetHostActive_uponStorageUnlock_true() =
+ fun isCommunalAvailable_whenDreaming_true() =
testScope.runTest {
- collectLastValue(underTest.isCommunalAvailable)
- assertThat(widgetRepository.isHostActive()).isFalse()
+ val isAvailable by collectLastValue(underTest.isCommunalAvailable)
+ assertThat(isAvailable).isFalse()
keyguardRepository.setIsEncryptedOrLockdown(false)
+ userRepository.setSelectedUserInfo(mainUser)
+ keyguardRepository.setDreaming(true)
runCurrent()
- assertThat(widgetRepository.isHostActive()).isTrue()
+ assertThat(isAvailable).isTrue()
}
@Test
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalTutorialInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalTutorialInteractorTest.kt
index 161356d..352463f 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalTutorialInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalTutorialInteractorTest.kt
@@ -16,6 +16,7 @@
package com.android.systemui.communal.domain.interactor
+import android.content.pm.UserInfo
import android.provider.Settings.Secure.HUB_MODE_TUTORIAL_COMPLETED
import android.provider.Settings.Secure.HUB_MODE_TUTORIAL_NOT_STARTED
import android.provider.Settings.Secure.HUB_MODE_TUTORIAL_STARTED
@@ -30,9 +31,9 @@
import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
import com.android.systemui.kosmos.testScope
-import com.android.systemui.settings.UserTracker
import com.android.systemui.testKosmos
-import com.android.systemui.util.mockito.mock
+import com.android.systemui.user.data.repository.FakeUserRepository
+import com.android.systemui.user.data.repository.fakeUserRepository
import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.test.runTest
@@ -45,15 +46,17 @@
@SmallTest
@RunWith(AndroidJUnit4::class)
class CommunalTutorialInteractorTest : SysuiTestCase() {
+ @Mock lateinit var user: UserInfo
+
private val kosmos = testKosmos()
private val testScope = kosmos.testScope
- @Mock private lateinit var userTracker: UserTracker
-
private lateinit var underTest: CommunalTutorialInteractor
private lateinit var keyguardRepository: FakeKeyguardRepository
private lateinit var communalTutorialRepository: FakeCommunalTutorialRepository
private lateinit var communalRepository: FakeCommunalRepository
+ private lateinit var communalInteractor: CommunalInteractor
+ private lateinit var userRepository: FakeUserRepository
@Before
fun setUp() {
@@ -62,16 +65,17 @@
keyguardRepository = kosmos.fakeKeyguardRepository
communalTutorialRepository = kosmos.fakeCommunalTutorialRepository
communalRepository = kosmos.fakeCommunalRepository
+ communalInteractor = kosmos.communalInteractor
+ userRepository = kosmos.fakeUserRepository
underTest = kosmos.communalTutorialInteractor
-
- whenever(userTracker.userHandle).thenReturn(mock())
}
@Test
fun tutorialUnavailable_whenKeyguardNotVisible() =
testScope.runTest {
val isTutorialAvailable by collectLastValue(underTest.isTutorialAvailable)
+ setCommunalAvailable(true)
communalTutorialRepository.setTutorialSettingState(HUB_MODE_TUTORIAL_NOT_STARTED)
keyguardRepository.setKeyguardShowing(false)
assertThat(isTutorialAvailable).isFalse()
@@ -81,6 +85,7 @@
fun tutorialUnavailable_whenTutorialIsCompleted() =
testScope.runTest {
val isTutorialAvailable by collectLastValue(underTest.isTutorialAvailable)
+ setCommunalAvailable(true)
keyguardRepository.setKeyguardShowing(true)
keyguardRepository.setKeyguardOccluded(false)
communalRepository.setIsCommunalHubShowing(false)
@@ -89,9 +94,20 @@
}
@Test
+ fun tutorialUnavailable_whenCommunalNotAvailable() =
+ testScope.runTest {
+ val isTutorialAvailable by collectLastValue(underTest.isTutorialAvailable)
+ setCommunalAvailable(false)
+ communalTutorialRepository.setTutorialSettingState(HUB_MODE_TUTORIAL_NOT_STARTED)
+ keyguardRepository.setKeyguardShowing(true)
+ assertThat(isTutorialAvailable).isFalse()
+ }
+
+ @Test
fun tutorialAvailable_whenTutorialNotStarted() =
testScope.runTest {
val isTutorialAvailable by collectLastValue(underTest.isTutorialAvailable)
+ setCommunalAvailable(true)
keyguardRepository.setKeyguardShowing(true)
keyguardRepository.setKeyguardOccluded(false)
communalRepository.setIsCommunalHubShowing(false)
@@ -103,6 +119,7 @@
fun tutorialAvailable_whenTutorialIsStarted() =
testScope.runTest {
val isTutorialAvailable by collectLastValue(underTest.isTutorialAvailable)
+ setCommunalAvailable(true)
keyguardRepository.setKeyguardShowing(true)
keyguardRepository.setKeyguardOccluded(false)
communalRepository.setIsCommunalHubShowing(true)
@@ -183,4 +200,16 @@
assertThat(tutorialSettingState).isEqualTo(HUB_MODE_TUTORIAL_COMPLETED)
}
+
+ private suspend fun setCommunalAvailable(available: Boolean) {
+ if (available) {
+ communalRepository.setIsCommunalEnabled(true)
+ keyguardRepository.setIsEncryptedOrLockdown(false)
+ whenever(user.isMain).thenReturn(true)
+ userRepository.setUserInfos(listOf(user))
+ userRepository.setSelectedUserInfo(user)
+ } else {
+ keyguardRepository.setIsEncryptedOrLockdown(true)
+ }
+ }
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt
index 033dc6d..c814f3f 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt
@@ -17,6 +17,7 @@
package com.android.systemui.communal.view.viewmodel
import android.app.smartspace.SmartspaceTarget
+import android.content.pm.UserInfo
import android.provider.Settings
import android.widget.RemoteViews
import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -43,6 +44,8 @@
import com.android.systemui.smartspace.data.repository.FakeSmartspaceRepository
import com.android.systemui.smartspace.data.repository.fakeSmartspaceRepository
import com.android.systemui.testKosmos
+import com.android.systemui.user.data.repository.FakeUserRepository
+import com.android.systemui.user.data.repository.fakeUserRepository
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
@@ -62,6 +65,7 @@
@RunWith(AndroidJUnit4::class)
class CommunalViewModelTest : SysuiTestCase() {
@Mock private lateinit var mediaHost: MediaHost
+ @Mock private lateinit var user: UserInfo
private val kosmos = testKosmos()
private val testScope = kosmos.testScope
@@ -71,6 +75,7 @@
private lateinit var widgetRepository: FakeCommunalWidgetRepository
private lateinit var smartspaceRepository: FakeSmartspaceRepository
private lateinit var mediaRepository: FakeCommunalMediaRepository
+ private lateinit var userRepository: FakeUserRepository
private lateinit var underTest: CommunalViewModel
@@ -83,6 +88,7 @@
widgetRepository = kosmos.fakeCommunalWidgetRepository
smartspaceRepository = kosmos.fakeSmartspaceRepository
mediaRepository = kosmos.fakeCommunalMediaRepository
+ userRepository = kosmos.fakeUserRepository
underTest =
CommunalViewModel(
@@ -103,9 +109,11 @@
@Test
fun tutorial_tutorialNotCompletedAndKeyguardVisible_showTutorialContent() =
testScope.runTest {
- // Keyguard showing, and tutorial not started.
+ // Keyguard showing, storage unlocked, main user, and tutorial not started.
keyguardRepository.setKeyguardShowing(true)
keyguardRepository.setKeyguardOccluded(false)
+ keyguardRepository.setIsEncryptedOrLockdown(false)
+ setIsMainUser(true)
tutorialRepository.setTutorialSettingState(
Settings.Secure.HUB_MODE_TUTORIAL_NOT_STARTED
)
@@ -201,4 +209,10 @@
underTest.onHidePopupAfterDismissCta()
assertThat(isPopupOnDismissCtaShowing).isEqualTo(false)
}
+
+ private suspend fun setIsMainUser(isMainUser: Boolean) {
+ whenever(user.isMain).thenReturn(isMainUser)
+ userRepository.setUserInfos(listOf(user))
+ userRepository.setSelectedUserInfo(user)
+ }
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartableTest.kt
new file mode 100644
index 0000000..112b0c7
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartableTest.kt
@@ -0,0 +1,132 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.communal.widgets
+
+import android.content.pm.UserInfo
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.communal.domain.interactor.communalInteractor
+import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.testKosmos
+import com.android.systemui.user.data.repository.fakeUserRepository
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class CommunalAppWidgetHostStartableTest : SysuiTestCase() {
+ private val kosmos = testKosmos()
+
+ @Mock private lateinit var appWidgetHost: CommunalAppWidgetHost
+
+ private lateinit var underTest: CommunalAppWidgetHostStartable
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+ kosmos.fakeUserRepository.setUserInfos(listOf(MAIN_USER_INFO))
+
+ underTest =
+ CommunalAppWidgetHostStartable(
+ appWidgetHost,
+ kosmos.communalInteractor,
+ kosmos.applicationCoroutineScope,
+ kosmos.testDispatcher,
+ )
+ }
+
+ @Test
+ fun editModeShowingStartsAppWidgetHost() =
+ with(kosmos) {
+ testScope.runTest {
+ setCommunalAvailable(false)
+ communalInteractor.setEditModeOpen(true)
+ verify(appWidgetHost, never()).startListening()
+
+ underTest.start()
+ runCurrent()
+
+ verify(appWidgetHost).startListening()
+ verify(appWidgetHost, never()).stopListening()
+
+ communalInteractor.setEditModeOpen(false)
+ runCurrent()
+
+ verify(appWidgetHost).stopListening()
+ }
+ }
+
+ @Test
+ fun communalShowingStartsAppWidgetHost() =
+ with(kosmos) {
+ testScope.runTest {
+ setCommunalAvailable(true)
+ communalInteractor.setEditModeOpen(false)
+ verify(appWidgetHost, never()).startListening()
+
+ underTest.start()
+ runCurrent()
+
+ verify(appWidgetHost).startListening()
+ verify(appWidgetHost, never()).stopListening()
+
+ setCommunalAvailable(false)
+ runCurrent()
+
+ verify(appWidgetHost).stopListening()
+ }
+ }
+
+ @Test
+ fun communalAndEditModeNotShowingNeverStartListening() =
+ with(kosmos) {
+ testScope.runTest {
+ setCommunalAvailable(false)
+ communalInteractor.setEditModeOpen(false)
+
+ underTest.start()
+ runCurrent()
+
+ verify(appWidgetHost, never()).startListening()
+ verify(appWidgetHost, never()).stopListening()
+ }
+ }
+
+ private suspend fun setCommunalAvailable(available: Boolean) =
+ with(kosmos) {
+ fakeKeyguardRepository.setIsEncryptedOrLockdown(!available)
+ fakeUserRepository.setSelectedUserInfo(MAIN_USER_INFO)
+ fakeKeyguardRepository.setKeyguardShowing(true)
+ }
+
+ private companion object {
+ val MAIN_USER_INFO = UserInfo(0, "primary", UserInfo.FLAG_MAIN)
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/touch/CommunalTouchHandlerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/touch/CommunalTouchHandlerTest.java
new file mode 100644
index 0000000..74c1970
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/touch/CommunalTouchHandlerTest.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.dreams.touch;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.verify;
+
+import android.view.GestureDetector;
+import android.view.MotionEvent;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.shared.system.InputChannelCompat;
+import com.android.systemui.statusbar.NotificationShadeWindowController;
+import com.android.systemui.statusbar.phone.CentralSurfaces;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+
+import java.util.Optional;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class CommunalTouchHandlerTest extends SysuiTestCase {
+ @Mock
+ CentralSurfaces mCentralSurfaces;
+ @Mock
+ NotificationShadeWindowController mNotificationShadeWindowController;
+ @Mock
+ DreamTouchHandler.TouchSession mTouchSession;
+ CommunalTouchHandler mTouchHandler;
+
+ private static final int INITIATION_WIDTH = 20;
+
+ @Before
+ public void setup() {
+ MockitoAnnotations.initMocks(this);
+ mTouchHandler = new CommunalTouchHandler(
+ Optional.of(mCentralSurfaces),
+ mNotificationShadeWindowController,
+ INITIATION_WIDTH);
+ }
+
+ @Test
+ public void testSessionStartForcesShadeOpen() {
+ mTouchHandler.onSessionStart(mTouchSession);
+ verify(mNotificationShadeWindowController).setForcePluginOpen(true, mTouchHandler);
+ }
+
+ @Test
+ public void testEventPropagation() {
+ final MotionEvent motionEvent = Mockito.mock(MotionEvent.class);
+
+ final ArgumentCaptor<InputChannelCompat.InputEventListener>
+ inputEventListenerArgumentCaptor =
+ ArgumentCaptor.forClass(InputChannelCompat.InputEventListener.class);
+
+ mTouchHandler.onSessionStart(mTouchSession);
+ verify(mTouchSession).registerInputListener(inputEventListenerArgumentCaptor.capture());
+ inputEventListenerArgumentCaptor.getValue().onInputEvent(motionEvent);
+ verify(mCentralSurfaces).handleDreamTouch(motionEvent);
+ }
+
+ @Test
+ public void testTouchPilferingOnScroll() {
+ final MotionEvent motionEvent1 = Mockito.mock(MotionEvent.class);
+ final MotionEvent motionEvent2 = Mockito.mock(MotionEvent.class);
+
+ final ArgumentCaptor<GestureDetector.OnGestureListener> gestureListenerArgumentCaptor =
+ ArgumentCaptor.forClass(GestureDetector.OnGestureListener.class);
+
+ mTouchHandler.onSessionStart(mTouchSession);
+ verify(mTouchSession).registerGestureListener(gestureListenerArgumentCaptor.capture());
+
+ assertThat(gestureListenerArgumentCaptor.getValue()
+ .onScroll(motionEvent1, motionEvent2, 1, 1))
+ .isTrue();
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/haptics/slider/SeekableSliderHapticPluginTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/haptics/slider/SeekableSliderHapticPluginTest.kt
new file mode 100644
index 0000000..ea766f8
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/haptics/slider/SeekableSliderHapticPluginTest.kt
@@ -0,0 +1,156 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.haptics.slider
+
+import android.widget.SeekBar
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.statusbar.VibratorHelper
+import com.android.systemui.util.mockito.whenever
+import com.android.systemui.util.time.fakeSystemClock
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.advanceTimeBy
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.Mock
+import org.mockito.junit.MockitoJUnit
+import org.mockito.junit.MockitoRule
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+@OptIn(ExperimentalCoroutinesApi::class)
+class SeekableSliderHapticPluginTest : SysuiTestCase() {
+
+ private val kosmos = Kosmos()
+
+ @Rule @JvmField val mMockitoRule: MockitoRule = MockitoJUnit.rule()
+ @Mock private lateinit var vibratorHelper: VibratorHelper
+ private val seekBar = SeekBar(mContext)
+ private lateinit var plugin: SeekableSliderHapticPlugin
+
+ @Before
+ fun setup() {
+ whenever(vibratorHelper.getPrimitiveDurations(anyInt())).thenReturn(intArrayOf(0))
+ }
+
+ @Test
+ fun start_beginsTrackingSlider() = runOnStartedPlugin { assertThat(plugin.isTracking).isTrue() }
+
+ @Test
+ fun stop_stopsTrackingSlider() = runOnStartedPlugin {
+ // WHEN called to stop
+ plugin.stop()
+
+ // THEN stops tracking
+ assertThat(plugin.isTracking).isFalse()
+ }
+
+ @Test
+ fun start_afterStop_startsTheTrackingAgain() = runOnStartedPlugin {
+ // WHEN the plugin is restarted
+ plugin.stop()
+ plugin.start()
+
+ // THEN the tracking begins again
+ assertThat(plugin.isTracking).isTrue()
+ }
+
+ @Test
+ fun onKeyDown_startsWaiting() = runOnStartedPlugin {
+ // WHEN a keyDown event is recorded
+ plugin.onKeyDown()
+
+ // THEN the timer starts waiting
+ assertThat(plugin.isKeyUpTimerWaiting).isTrue()
+ }
+
+ @Test
+ fun keyUpWaitComplete_triggersOnArrowUp() = runOnStartedPlugin {
+ // GIVEN an onKeyDown that starts the wait and a program progress change that advances the
+ // slider state to ARROW_HANDLE_MOVED_ONCE
+ plugin.onKeyDown()
+ plugin.onProgressChanged(seekBar, 50, false)
+ testScheduler.runCurrent()
+ assertThat(plugin.trackerState).isEqualTo(SliderState.ARROW_HANDLE_MOVED_ONCE)
+
+ // WHEN the key-up wait completes after the timeout plus a small buffer
+ advanceTimeBy(KEY_UP_TIMEOUT + 10L)
+
+ // THEN the onArrowUp event is delivered causing the slider tracker to move to IDLE
+ assertThat(plugin.trackerState).isEqualTo(SliderState.IDLE)
+ assertThat(plugin.isKeyUpTimerWaiting).isFalse()
+ }
+
+ @Test
+ fun onKeyDown_whileWaiting_restartsWait() = runOnStartedPlugin {
+ // GIVEN an onKeyDown that starts the wait and a program progress change that advances the
+ // slider state to ARROW_HANDLE_MOVED_ONCE
+ plugin.onKeyDown()
+ plugin.onProgressChanged(seekBar, 50, false)
+ testScheduler.runCurrent()
+ assertThat(plugin.trackerState).isEqualTo(SliderState.ARROW_HANDLE_MOVED_ONCE)
+
+ // WHEN half the timeout period has elapsed and a new keyDown event occurs
+ advanceTimeBy(KEY_UP_TIMEOUT / 2)
+ plugin.onKeyDown()
+
+ // AFTER advancing by a period of time that should have complete the original wait
+ advanceTimeBy(KEY_UP_TIMEOUT / 2 + 10L)
+
+ // THEN the timer is still waiting and the slider tracker remains on ARROW_HANDLE_MOVED_ONCE
+ assertThat(plugin.isKeyUpTimerWaiting).isTrue()
+ assertThat(plugin.trackerState).isEqualTo(SliderState.ARROW_HANDLE_MOVED_ONCE)
+ }
+
+ private fun runOnStartedPlugin(test: suspend TestScope.() -> Unit) =
+ with(kosmos) {
+ testScope.runTest {
+ createPlugin(this, UnconfinedTestDispatcher(testScheduler))
+ // GIVEN that the plugin is started
+ plugin.start()
+
+ // THEN run the test
+ test()
+ }
+ }
+
+ private fun createPlugin(scope: CoroutineScope, dispatcher: CoroutineDispatcher) {
+ plugin =
+ SeekableSliderHapticPlugin(
+ vibratorHelper,
+ kosmos.fakeSystemClock,
+ dispatcher,
+ scope,
+ )
+ }
+
+ companion object {
+ private const val KEY_UP_TIMEOUT = 100L
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt
index 6f62afc..dc8b97a 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt
@@ -139,6 +139,18 @@
}
@Test
+ fun topClippingBounds() =
+ testScope.runTest {
+ assertThat(underTest.topClippingBounds.value).isNull()
+
+ underTest.topClippingBounds.value = 50
+ assertThat(underTest.topClippingBounds.value).isEqualTo(50)
+
+ underTest.topClippingBounds.value = 500
+ assertThat(underTest.topClippingBounds.value).isEqualTo(500)
+ }
+
+ @Test
fun clockPosition() =
testScope.runTest {
assertThat(underTest.clockPosition.value).isEqualTo(Position(0, 0))
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt
index 1eaa060..6cc680b 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt
@@ -25,9 +25,14 @@
import com.android.systemui.Flags as AConfigFlags
import com.android.systemui.Flags.FLAG_NEW_AOD_TRANSITION
import com.android.systemui.SysuiTestCase
+import com.android.systemui.communal.data.repository.communalRepository
+import com.android.systemui.communal.shared.model.CommunalSceneKey
+import com.android.systemui.communal.shared.model.ObservableCommunalTransitionState
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.deviceentry.data.repository.fakeDeviceEntryRepository
+import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
+import com.android.systemui.keyguard.domain.interactor.keyguardInteractor
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.keyguard.shared.model.TransitionState
import com.android.systemui.keyguard.shared.model.TransitionStep
@@ -42,6 +47,7 @@
import com.android.systemui.util.ui.value
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Before
@@ -54,13 +60,14 @@
private val kosmos = testKosmos()
private val testScope = kosmos.testScope
private val keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository
+ private val keyguardInteractor = kosmos.keyguardInteractor
+ private val keyguardRepository = kosmos.fakeKeyguardRepository
+ private val communalRepository = kosmos.communalRepository
private val screenOffAnimationController = kosmos.screenOffAnimationController
private val deviceEntryRepository = kosmos.fakeDeviceEntryRepository
private val notificationsKeyguardInteractor = kosmos.notificationsKeyguardInteractor
private val dozeParameters = kosmos.dozeParameters
- private val underTest by lazy {
- kosmos.keyguardRootViewModel
- }
+ private val underTest by lazy { kosmos.keyguardRootViewModel }
@Before
fun setUp() {
@@ -207,7 +214,38 @@
}
@Test
- fun alpha_glanceableHubOpen_isZero() =
+ fun topClippingBounds() =
+ testScope.runTest {
+ val topClippingBounds by collectLastValue(underTest.topClippingBounds)
+ assertThat(topClippingBounds).isNull()
+
+ keyguardRepository.topClippingBounds.value = 50
+ assertThat(topClippingBounds).isEqualTo(50)
+
+ keyguardRepository.topClippingBounds.value = 1000
+ assertThat(topClippingBounds).isEqualTo(1000)
+ }
+
+ @Test
+ fun alpha_idleOnHub_isZero() =
+ testScope.runTest {
+ val alpha by collectLastValue(underTest.alpha)
+
+ // Hub transition state is idle with hub open.
+ communalRepository.setTransitionState(
+ flowOf(ObservableCommunalTransitionState.Idle(CommunalSceneKey.Communal))
+ )
+ runCurrent()
+
+ // Set keyguard alpha to 1.0f.
+ keyguardInteractor.setAlpha(1.0f)
+
+ // Alpha property remains 0 regardless.
+ assertThat(alpha).isEqualTo(0f)
+ }
+
+ @Test
+ fun alpha_transitionToHub_isZero() =
testScope.runTest {
val alpha by collectLastValue(underTest.alpha)
@@ -221,7 +259,7 @@
}
@Test
- fun alpha_glanceableHubClosed_isOne() =
+ fun alpha_transitionFromHubToLockscreen_isOne() =
testScope.runTest {
val alpha by collectLastValue(underTest.alpha)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/SideFpsProgressBarViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/SideFpsProgressBarViewModelTest.kt
new file mode 100644
index 0000000..2fe4ef78
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/SideFpsProgressBarViewModelTest.kt
@@ -0,0 +1,189 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.ui.viewmodel
+
+import android.content.applicationContext
+import android.hardware.biometrics.BiometricFingerprintConstants
+import android.os.PowerManager
+import android.testing.TestableLooper
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.Flags
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.biometrics.data.repository.fakeFingerprintPropertyRepository
+import com.android.systemui.biometrics.domain.interactor.displayStateInteractor
+import com.android.systemui.biometrics.domain.interactor.sideFpsSensorInteractor
+import com.android.systemui.biometrics.fakeFingerprintInteractiveToAuthProvider
+import com.android.systemui.biometrics.shared.model.FingerprintSensorType
+import com.android.systemui.biometrics.shared.model.SensorStrength
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.deviceentry.domain.interactor.deviceEntryFingerprintAuthInteractor
+import com.android.systemui.keyguard.data.repository.fakeBiometricSettingsRepository
+import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFingerprintAuthRepository
+import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
+import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
+import com.android.systemui.keyguard.domain.interactor.keyguardInteractor
+import com.android.systemui.keyguard.shared.model.AcquiredFingerprintAuthenticationStatus
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.shared.model.TransitionState
+import com.android.systemui.keyguard.shared.model.TransitionStep
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.power.data.repository.fakePowerRepository
+import com.android.systemui.power.domain.interactor.powerInteractor
+import com.android.systemui.res.R
+import com.android.systemui.statusbar.phone.dozeServiceHost
+import com.android.systemui.testKosmos
+import com.android.systemui.util.mockito.mock
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.verify
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@TestableLooper.RunWithLooper
+@RunWith(AndroidJUnit4::class)
+class SideFpsProgressBarViewModelTest : SysuiTestCase() {
+ private val kosmos = testKosmos()
+ private lateinit var underTest: SideFpsProgressBarViewModel
+ private val testScope = kosmos.testScope
+ private lateinit var mTestableLooper: TestableLooper
+
+ @Before
+ fun setup() {
+ mTestableLooper = TestableLooper.get(this)
+ allowTestableLooperAsMainThread()
+ }
+
+ private suspend fun setupRestToUnlockEnabled() {
+ mSetFlagsRule.enableFlags(Flags.FLAG_REST_TO_UNLOCK)
+ overrideResource(R.bool.config_restToUnlockSupported, true)
+ kosmos.fakeFingerprintPropertyRepository.setProperties(
+ 1,
+ SensorStrength.STRONG,
+ FingerprintSensorType.POWER_BUTTON,
+ mutableMapOf(Pair("sensor", mock()))
+ )
+ kosmos.fakeFingerprintInteractiveToAuthProvider.enabledForCurrentUser.value = true
+ kosmos.fakeKeyguardTransitionRepository.sendTransitionStep(
+ TransitionStep(
+ from = KeyguardState.LOCKSCREEN,
+ to = KeyguardState.AOD,
+ value = 0.0f,
+ transitionState = TransitionState.STARTED
+ )
+ )
+ kosmos.fakeKeyguardTransitionRepository.sendTransitionStep(
+ TransitionStep(
+ from = KeyguardState.LOCKSCREEN,
+ to = KeyguardState.AOD,
+ value = 1.0f,
+ transitionState = TransitionState.FINISHED
+ )
+ )
+ kosmos.fakeBiometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(true)
+ }
+
+ @Test
+ fun whenConfigDisabled_featureIsDisabled() =
+ testScope.runTest {
+ overrideResource(R.bool.config_restToUnlockSupported, false)
+ underTest = createViewModel()
+ val enabled by collectLastValue(underTest.isProlongedTouchRequiredForAuthentication)
+
+ assertThat(enabled).isFalse()
+ }
+
+ @Test
+ fun whenConfigEnabledSensorIsPowerButtonAndSettingsToggleIsEnabled_featureIsEnabled() =
+ testScope.runTest {
+ overrideResource(R.bool.config_restToUnlockSupported, true)
+ underTest = createViewModel()
+ val enabled by collectLastValue(underTest.isProlongedTouchRequiredForAuthentication)
+
+ assertThat(enabled).isFalse()
+ kosmos.fakeFingerprintPropertyRepository.setProperties(
+ 1,
+ SensorStrength.STRONG,
+ FingerprintSensorType.POWER_BUTTON,
+ mutableMapOf(Pair("sensor", mock()))
+ )
+ assertThat(enabled).isFalse()
+
+ kosmos.fakeFingerprintInteractiveToAuthProvider.enabledForCurrentUser.value = true
+ kosmos.fakeBiometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(true)
+
+ runCurrent()
+ assertThat(enabled).isTrue()
+ }
+
+ @Test
+ fun whenFingerprintAcquiredStartsWhenNotDozing_wakesUpDevice() =
+ testScope.runTest {
+ setupRestToUnlockEnabled()
+ underTest = createViewModel()
+
+ kosmos.fakeKeyguardRepository.setIsDozing(false)
+ kosmos.fakeDeviceEntryFingerprintAuthRepository.setAuthenticationStatus(
+ AcquiredFingerprintAuthenticationStatus(
+ BiometricFingerprintConstants.FINGERPRINT_ACQUIRED_START
+ )
+ )
+
+ runCurrent()
+
+ assertThat(kosmos.fakePowerRepository.lastWakeReason)
+ .isEqualTo(PowerManager.WAKE_REASON_BIOMETRIC)
+ }
+
+ @Test
+ fun whenFingerprintAcquiredStartsWhenDozing_pulsesAod() =
+ testScope.runTest {
+ setupRestToUnlockEnabled()
+ underTest = createViewModel()
+
+ kosmos.fakeKeyguardRepository.setIsDozing(true)
+ kosmos.fakeDeviceEntryFingerprintAuthRepository.setAuthenticationStatus(
+ AcquiredFingerprintAuthenticationStatus(
+ BiometricFingerprintConstants.FINGERPRINT_ACQUIRED_START
+ )
+ )
+
+ runCurrent()
+
+ verify(kosmos.dozeServiceHost).fireSideFpsAcquisitionStarted()
+ }
+
+ private fun createViewModel() =
+ SideFpsProgressBarViewModel(
+ kosmos.applicationContext,
+ kosmos.deviceEntryFingerprintAuthInteractor,
+ kosmos.sideFpsSensorInteractor,
+ kosmos.dozeServiceHost,
+ kosmos.keyguardInteractor,
+ kosmos.displayStateInteractor,
+ kosmos.testDispatcher,
+ kosmos.applicationCoroutineScope,
+ kosmos.powerInteractor,
+ )
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/data/repository/InstalledTilesComponentRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/data/repository/InstalledTilesComponentRepositoryImplTest.kt
index eb845b2..d9f24b3 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/data/repository/InstalledTilesComponentRepositoryImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/data/repository/InstalledTilesComponentRepositoryImplTest.kt
@@ -35,7 +35,6 @@
import com.android.systemui.common.data.repository.packageChangeRepository
import com.android.systemui.common.data.shared.model.PackageChangeModel
import com.android.systemui.coroutines.collectLastValue
-import com.android.systemui.kosmos.testDispatcher
import com.android.systemui.kosmos.testScope
import com.android.systemui.testKosmos
import com.android.systemui.util.mockito.any
@@ -44,6 +43,8 @@
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.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
@@ -52,6 +53,7 @@
import org.mockito.Mock
import org.mockito.MockitoAnnotations
+@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@RunWith(AndroidJUnit4::class)
@TestableLooper.RunWithLooper
@@ -82,7 +84,7 @@
underTest =
InstalledTilesComponentRepositoryImpl(
context,
- kosmos.testDispatcher,
+ testScope.backgroundScope,
kosmos.packageChangeRepository
)
}
@@ -103,6 +105,7 @@
.thenReturn(listOf(resolveInfo))
val componentNames by collectLastValue(underTest.getInstalledTilesComponents(userId))
+ runCurrent()
assertThat(componentNames).containsExactly(TEST_COMPONENT)
}
@@ -115,6 +118,8 @@
ResolveInfo(TEST_COMPONENT, hasPermission = true, defaultEnabled = true)
val componentNames by collectLastValue(underTest.getInstalledTilesComponents(userId))
+ runCurrent()
+
assertThat(componentNames).isEmpty()
whenever(
@@ -126,6 +131,7 @@
)
.thenReturn(listOf(resolveInfo))
kosmos.fakePackageChangeRepository.notifyChange(PackageChangeModel.Empty)
+ runCurrent()
assertThat(componentNames).containsExactly(TEST_COMPONENT)
}
@@ -146,6 +152,8 @@
.thenReturn(listOf(resolveInfo))
val componentNames by collectLastValue(underTest.getInstalledTilesComponents(userId))
+ runCurrent()
+
assertThat(componentNames).isEmpty()
}
@@ -165,6 +173,8 @@
.thenReturn(listOf(resolveInfo))
val componentNames by collectLastValue(underTest.getInstalledTilesComponents(userId))
+ runCurrent()
+
assertThat(componentNames).isEmpty()
}
@@ -210,10 +220,31 @@
.thenReturn(listOf(resolveInfo))
val componentNames by collectLastValue(underTest.getInstalledTilesComponents(userId))
+ runCurrent()
assertThat(componentNames).containsExactly(TEST_COMPONENT)
}
+ @Test
+ fun loadComponentsForSameUserTwice_returnsSameFlow() =
+ testScope.runTest {
+ val flowForUser1 = underTest.getInstalledTilesComponents(1)
+ val flowForUser1TheSecondTime = underTest.getInstalledTilesComponents(1)
+ runCurrent()
+
+ assertThat(flowForUser1TheSecondTime).isEqualTo(flowForUser1)
+ }
+
+ @Test
+ fun loadComponentsForDifferentUsers_returnsDifferentFlow() =
+ testScope.runTest {
+ val flowForUser1 = underTest.getInstalledTilesComponents(1)
+ val flowForUser2 = underTest.getInstalledTilesComponents(2)
+ runCurrent()
+
+ assertThat(flowForUser2).isNotEqualTo(flowForUser1)
+ }
+
companion object {
private val INTENT = Intent(TileService.ACTION_QS_TILE)
private val FLAGS =
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/autoaddable/A11yShortcutAutoAddableListTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/autoaddable/A11yShortcutAutoAddableListTest.kt
new file mode 100644
index 0000000..311122d
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/autoaddable/A11yShortcutAutoAddableListTest.kt
@@ -0,0 +1,87 @@
+/*
+ * 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.qs.pipeline.domain.autoaddable
+
+import android.content.ComponentName
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
+import android.view.accessibility.Flags
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.internal.accessibility.AccessibilityShortcutController
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.qs.tiles.ColorCorrectionTile
+import com.android.systemui.qs.tiles.ColorInversionTile
+import com.android.systemui.qs.tiles.OneHandedModeTile
+import com.android.systemui.qs.tiles.ReduceBrightColorsTile
+import com.android.systemui.util.mockito.mock
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class A11yShortcutAutoAddableListTest : SysuiTestCase() {
+
+ private val factory =
+ object : A11yShortcutAutoAddable.Factory {
+ override fun create(
+ spec: TileSpec,
+ componentName: ComponentName
+ ): A11yShortcutAutoAddable {
+ return A11yShortcutAutoAddable(mock(), mock(), spec, componentName)
+ }
+ }
+
+ @Test
+ @DisableFlags(Flags.FLAG_A11Y_QS_SHORTCUT)
+ fun getA11yShortcutAutoAddables_withA11yQsShortcutFlagOff_emptyResult() {
+ val autoAddables = A11yShortcutAutoAddableList.getA11yShortcutAutoAddables(factory)
+
+ assertThat(autoAddables).isEmpty()
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_A11Y_QS_SHORTCUT)
+ fun getA11yShortcutAutoAddables_withA11yQsShortcutFlagOn_correctAutoAddables() {
+ val expected =
+ setOf(
+ factory.create(
+ TileSpec.create(ColorCorrectionTile.TILE_SPEC),
+ AccessibilityShortcutController.DALTONIZER_COMPONENT_NAME
+ ),
+ factory.create(
+ TileSpec.create(ColorInversionTile.TILE_SPEC),
+ AccessibilityShortcutController.COLOR_INVERSION_COMPONENT_NAME
+ ),
+ factory.create(
+ TileSpec.create(OneHandedModeTile.TILE_SPEC),
+ AccessibilityShortcutController.ONE_HANDED_COMPONENT_NAME
+ ),
+ factory.create(
+ TileSpec.create(ReduceBrightColorsTile.TILE_SPEC),
+ AccessibilityShortcutController.REDUCE_BRIGHT_COLORS_COMPONENT_NAME
+ ),
+ )
+
+ val autoAddables = A11yShortcutAutoAddableList.getA11yShortcutAutoAddables(factory)
+
+ assertThat(autoAddables).isNotEmpty()
+ assertThat(autoAddables).containsExactlyElementsIn(expected)
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/autoaddable/A11yShortcutAutoAddableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/autoaddable/A11yShortcutAutoAddableTest.kt
new file mode 100644
index 0000000..3b33a43
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/autoaddable/A11yShortcutAutoAddableTest.kt
@@ -0,0 +1,175 @@
+/*
+ * 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.qs.pipeline.domain.autoaddable
+
+import android.content.ComponentName
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.accessibility.data.repository.FakeAccessibilityQsShortcutsRepository
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.coroutines.collectValues
+import com.android.systemui.qs.pipeline.domain.model.AutoAddSignal
+import com.android.systemui.qs.pipeline.domain.model.AutoAddTracking
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class A11yShortcutAutoAddableTest : SysuiTestCase() {
+ private val testDispatcher = StandardTestDispatcher()
+ private val testScope = TestScope(testDispatcher)
+
+ private val a11yQsShortcutsRepository = FakeAccessibilityQsShortcutsRepository()
+ private val underTest =
+ A11yShortcutAutoAddable(a11yQsShortcutsRepository, testDispatcher, SPEC, TARGET_COMPONENT)
+
+ @Test
+ fun settingNotSet_noSignal() =
+ testScope.runTest {
+ val signal by collectLastValue(underTest.autoAddSignal(USER_ID))
+
+ assertThat(signal).isNull() // null means no emitted value
+ }
+
+ @Test
+ fun settingSetWithTarget_addSignal() =
+ testScope.runTest {
+ val signal by collectLastValue(underTest.autoAddSignal(USER_ID))
+ assertThat(signal).isNull()
+
+ a11yQsShortcutsRepository.setA11yQsShortcutTargets(
+ USER_ID,
+ setOf(TARGET_COMPONENT_FLATTEN)
+ )
+
+ assertThat(signal).isEqualTo(AutoAddSignal.Add(SPEC))
+ }
+
+ @Test
+ fun settingSetWithoutTarget_removeSignal() =
+ testScope.runTest {
+ val signal by collectLastValue(flow = underTest.autoAddSignal(USER_ID))
+ assertThat(signal).isNull()
+
+ a11yQsShortcutsRepository.setA11yQsShortcutTargets(
+ USER_ID,
+ setOf(OTHER_COMPONENT_FLATTEN)
+ )
+
+ assertThat(signal).isEqualTo(AutoAddSignal.Remove(SPEC))
+ }
+
+ @Test
+ fun settingSetWithMultipleComponents_containsTarget_addSignal() =
+ testScope.runTest {
+ val signal by collectLastValue(underTest.autoAddSignal(USER_ID))
+ assertThat(signal).isNull()
+
+ a11yQsShortcutsRepository.setA11yQsShortcutTargets(
+ USER_ID,
+ setOf(OTHER_COMPONENT_FLATTEN, TARGET_COMPONENT_FLATTEN)
+ )
+
+ assertThat(signal).isEqualTo(AutoAddSignal.Add(SPEC))
+ }
+
+ @Test
+ fun settingSetWithMultipleComponents_doesNotContainTarget_removeSignal() =
+ testScope.runTest {
+ val signal by collectLastValue(underTest.autoAddSignal(USER_ID))
+ assertThat(signal).isNull()
+
+ a11yQsShortcutsRepository.setA11yQsShortcutTargets(
+ USER_ID,
+ setOf(OTHER_COMPONENT_FLATTEN, OTHER_COMPONENT_FLATTEN)
+ )
+
+ assertThat(signal).isEqualTo(AutoAddSignal.Remove(SPEC))
+ }
+
+ @Test
+ fun multipleChangesWithTarget_onlyOneAddSignal() =
+ testScope.runTest {
+ val signals by collectValues(underTest.autoAddSignal(USER_ID))
+ assertThat(signals).isEmpty()
+
+ repeat(3) {
+ a11yQsShortcutsRepository.setA11yQsShortcutTargets(
+ USER_ID,
+ setOf(TARGET_COMPONENT_FLATTEN)
+ )
+ }
+
+ assertThat(signals.size).isEqualTo(1)
+ assertThat(signals[0]).isEqualTo(AutoAddSignal.Add(SPEC))
+ }
+
+ @Test
+ fun multipleChangesWithoutTarget_onlyOneRemoveSignal() =
+ testScope.runTest {
+ val signals by collectValues(underTest.autoAddSignal(USER_ID))
+ assertThat(signals).isEmpty()
+
+ repeat(3) {
+ a11yQsShortcutsRepository.setA11yQsShortcutTargets(
+ USER_ID,
+ setOf("$OTHER_COMPONENT_FLATTEN$it")
+ )
+ }
+
+ assertThat(signals.size).isEqualTo(1)
+ assertThat(signals[0]).isEqualTo(AutoAddSignal.Remove(SPEC))
+ }
+
+ @Test
+ fun settingSetWithTargetForUsers_onlySignalInThatUser() =
+ testScope.runTest {
+ val otherUserId = USER_ID + 1
+ val signalTargetUser by collectLastValue(underTest.autoAddSignal(USER_ID))
+ val signalOtherUser by collectLastValue(underTest.autoAddSignal(otherUserId))
+ assertThat(signalTargetUser).isNull()
+ assertThat(signalOtherUser).isNull()
+
+ a11yQsShortcutsRepository.setA11yQsShortcutTargets(
+ USER_ID,
+ setOf(TARGET_COMPONENT_FLATTEN)
+ )
+
+ assertThat(signalTargetUser).isEqualTo(AutoAddSignal.Add(SPEC))
+ assertThat(signalOtherUser).isNull()
+ }
+
+ @Test
+ fun strategyAlways() {
+ assertThat(underTest.autoAddTracking).isEqualTo(AutoAddTracking.Always)
+ }
+
+ companion object {
+ private val SPEC = TileSpec.create("spec")
+ private val TARGET_COMPONENT = ComponentName("FakePkgName", "FakeClassName")
+ private val TARGET_COMPONENT_FLATTEN = TARGET_COMPONENT.flattenToString()
+ private val OTHER_COMPONENT_FLATTEN =
+ ComponentName("FakePkgName", "OtherClassName").flattenToString()
+ private const val USER_ID = 0
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/adapter/QSSceneAdapterImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/adapter/QSSceneAdapterImplTest.kt
index d9b1ea1..cae20d0 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/adapter/QSSceneAdapterImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/adapter/QSSceneAdapterImplTest.kt
@@ -16,12 +16,16 @@
package com.android.systemui.qs.ui.adapter
+import android.content.res.Configuration
import android.os.Bundle
+import android.view.Surface
import android.view.View
import androidx.asynclayoutinflater.view.AsyncLayoutInflater
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
+import com.android.systemui.common.ui.data.repository.FakeConfigurationRepository
+import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.qs.QSImpl
import com.android.systemui.qs.dagger.QSComponent
@@ -34,6 +38,7 @@
import com.android.systemui.util.mockito.nullable
import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
+import java.util.Locale
import javax.inject.Provider
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.StandardTestDispatcher
@@ -81,11 +86,17 @@
.also { components.add(it) }
}
}
+ private val configuration = Configuration(context.resources.configuration)
+
+ private val fakeConfigurationRepository =
+ FakeConfigurationRepository().apply { onConfigurationChange(configuration) }
+ private val configurationInteractor = ConfigurationInteractor(fakeConfigurationRepository)
private val mockAsyncLayoutInflater =
mock<AsyncLayoutInflater>() {
whenever(inflate(anyInt(), nullable(), any())).then { invocation ->
val mockView = mock<View>()
+ whenever(mockView.context).thenReturn(context)
invocation
.getArgument<AsyncLayoutInflater.OnInflateFinishedListener>(2)
.onInflateFinished(
@@ -102,6 +113,7 @@
qsImplProvider,
testDispatcher,
testScope.backgroundScope,
+ configurationInteractor,
{ mockAsyncLayoutInflater },
)
@@ -297,6 +309,9 @@
@Test
fun reinflation_previousStateDestroyed() =
testScope.runTest {
+ // Run all flows... In particular, initial configuration propagation that could cause
+ // QSImpl to re-inflate.
+ runCurrent()
val qsImpl by collectLastValue(underTest.qsImpl)
underTest.inflate(context)
@@ -322,4 +337,81 @@
bundleArgCaptor.value,
)
}
+
+ @Test
+ fun changeInLocale_reinflation() =
+ testScope.runTest {
+ val qsImpl by collectLastValue(underTest.qsImpl)
+
+ underTest.inflate(context)
+ runCurrent()
+
+ val oldQsImpl = qsImpl!!
+
+ val newLocale =
+ if (configuration.locales[0] == Locale("en-US")) {
+ Locale("es-UY")
+ } else {
+ Locale("en-US")
+ }
+ configuration.setLocale(newLocale)
+ fakeConfigurationRepository.onConfigurationChange(configuration)
+ runCurrent()
+
+ assertThat(oldQsImpl).isNotSameInstanceAs(qsImpl!!)
+ }
+
+ @Test
+ fun changeInFontSize_reinflation() =
+ testScope.runTest {
+ val qsImpl by collectLastValue(underTest.qsImpl)
+
+ underTest.inflate(context)
+ runCurrent()
+
+ val oldQsImpl = qsImpl!!
+
+ configuration.fontScale *= 2
+ fakeConfigurationRepository.onConfigurationChange(configuration)
+ runCurrent()
+
+ assertThat(oldQsImpl).isNotSameInstanceAs(qsImpl!!)
+ }
+
+ @Test
+ fun changeInAssetPath_reinflation() =
+ testScope.runTest {
+ val qsImpl by collectLastValue(underTest.qsImpl)
+
+ underTest.inflate(context)
+ runCurrent()
+
+ val oldQsImpl = qsImpl!!
+
+ configuration.assetsSeq += 1
+ fakeConfigurationRepository.onConfigurationChange(configuration)
+ runCurrent()
+
+ assertThat(oldQsImpl).isNotSameInstanceAs(qsImpl!!)
+ }
+
+ @Test
+ fun otherChangesInConfiguration_noReinflation_configurationChangeDispatched() =
+ testScope.runTest {
+ val qsImpl by collectLastValue(underTest.qsImpl)
+
+ underTest.inflate(context)
+ runCurrent()
+
+ val oldQsImpl = qsImpl!!
+ configuration.densityDpi *= 2
+ configuration.windowConfiguration.maxBounds.scale(2f)
+ configuration.windowConfiguration.rotation = Surface.ROTATION_270
+ fakeConfigurationRepository.onConfigurationChange(configuration)
+ runCurrent()
+
+ assertThat(oldQsImpl).isSameInstanceAs(qsImpl!!)
+ verify(qsImpl!!).onConfigurationChanged(configuration)
+ verify(qsImpl!!.view).dispatchConfigurationChanged(configuration)
+ }
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModelTest.kt
index d7a7941..42200a3 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModelTest.kt
@@ -23,6 +23,8 @@
import com.android.systemui.flags.FakeFeatureFlagsClassic
import com.android.systemui.flags.Flags
import com.android.systemui.kosmos.testScope
+import com.android.systemui.qs.FooterActionsController
+import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel
import com.android.systemui.qs.ui.adapter.FakeQSSceneAdapter
import com.android.systemui.scene.domain.interactor.sceneInteractor
import com.android.systemui.scene.shared.model.Direction
@@ -39,12 +41,16 @@
import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy
import com.android.systemui.statusbar.pipeline.shared.data.repository.FakeConnectivityRepository
import com.android.systemui.testKosmos
+import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
@SmallTest
@RunWith(AndroidJUnit4::class)
@@ -56,6 +62,12 @@
private val mobileIconsInteractor = FakeMobileIconsInteractor(FakeMobileMappingsProxy(), mock())
private val flags = FakeFeatureFlagsClassic().also { it.set(Flags.NEW_NETWORK_SLICE_UI, false) }
private val qsFlexiglassAdapter = FakeQSSceneAdapter { mock() }
+ private val footerActionsViewModel = mock<FooterActionsViewModel>()
+ private val footerActionsViewModelFactory =
+ mock<FooterActionsViewModel.Factory> {
+ whenever(create(any())).thenReturn(footerActionsViewModel)
+ }
+ private val footerActionsController = mock<FooterActionsController>()
private var mobileIconsViewModel: MobileIconsViewModel =
MobileIconsViewModel(
@@ -94,6 +106,8 @@
shadeHeaderViewModel = shadeHeaderViewModel,
qsSceneAdapter = qsFlexiglassAdapter,
notifications = kosmos.notificationsPlaceholderViewModel,
+ footerActionsViewModelFactory,
+ footerActionsController,
)
}
@@ -125,4 +139,12 @@
)
)
}
+
+ @Test
+ fun gettingViewModelInitializesControllerOnlyOnce() {
+ underTest.getFooterActionsViewModel(mock())
+ underTest.getFooterActionsViewModel(mock())
+
+ verify(footerActionsController, times(1)).init()
+ }
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/WindowRootViewVisibilityInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/WindowRootViewVisibilityInteractorTest.kt
index f23716c..d5e43f4 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/WindowRootViewVisibilityInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/WindowRootViewVisibilityInteractorTest.kt
@@ -25,10 +25,13 @@
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
import com.android.systemui.keyguard.shared.model.StatusBarState
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testScope
import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAsleepForTest
import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAwakeForTest
import com.android.systemui.power.domain.interactor.PowerInteractorFactory
import com.android.systemui.scene.data.repository.WindowRootViewVisibilityRepository
+import com.android.systemui.scene.shared.flag.sceneContainerFlags
import com.android.systemui.statusbar.NotificationPresenter
import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationListRepository
import com.android.systemui.statusbar.notification.data.repository.setActiveNotifs
@@ -44,7 +47,6 @@
import com.android.systemui.util.time.FakeSystemClock
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.test.StandardTestDispatcher
-import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Test
@@ -56,7 +58,8 @@
@RunWith(AndroidJUnit4::class)
class WindowRootViewVisibilityInteractorTest : SysuiTestCase() {
- private val testScope = TestScope()
+ private val kosmos = Kosmos()
+ private val testScope = kosmos.testScope
private val testDispatcher = StandardTestDispatcher()
private val iStatusBarService = mock<IStatusBarService>()
private val executor = FakeExecutor(FakeSystemClock())
@@ -79,6 +82,8 @@
headsUpManager,
powerInteractor,
activeNotificationsInteractor,
+ kosmos.sceneContainerFlags,
+ kosmos::sceneInteractor,
)
.apply { setUp(notificationPresenter, notificationsController) }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ShadeControllerSceneImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ShadeControllerSceneImplTest.kt
new file mode 100644
index 0000000..51b8342
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ShadeControllerSceneImplTest.kt
@@ -0,0 +1,202 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.shade
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.deviceentry.data.repository.fakeDeviceEntryRepository
+import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor
+import com.android.systemui.flags.Flags
+import com.android.systemui.flags.fakeFeatureFlagsClassic
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testCase
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.scene.domain.interactor.sceneInteractor
+import com.android.systemui.scene.shared.flag.fakeSceneContainerFlags
+import com.android.systemui.scene.shared.model.ObservableTransitionState
+import com.android.systemui.scene.shared.model.SceneKey
+import com.android.systemui.scene.shared.model.SceneModel
+import com.android.systemui.shade.domain.interactor.ShadeInteractor
+import com.android.systemui.shade.domain.interactor.shadeInteractor
+import com.android.systemui.statusbar.CommandQueue
+import com.android.systemui.util.mockito.mock
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.MutableStateFlow
+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.never
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+
+@ExperimentalCoroutinesApi
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class ShadeControllerSceneImplTest : SysuiTestCase() {
+ private val kosmos = Kosmos()
+ private val testScope = kosmos.testScope
+ private val sceneInteractor = kosmos.sceneInteractor
+ private val deviceEntryInteractor = kosmos.deviceEntryInteractor
+
+ private lateinit var shadeInteractor: ShadeInteractor
+ private lateinit var underTest: ShadeControllerSceneImpl
+
+ @Before
+ fun setup() {
+ kosmos.testCase = this
+ kosmos.fakeSceneContainerFlags.enabled = true
+ kosmos.fakeFeatureFlagsClassic.apply {
+ set(Flags.FULL_SCREEN_USER_SWITCHER, false)
+ set(Flags.NSSL_DEBUG_LINES, false)
+ set(Flags.FULL_SCREEN_USER_SWITCHER, false)
+ }
+ kosmos.fakeDeviceEntryRepository.setUnlocked(true)
+ testScope.runCurrent()
+ shadeInteractor = kosmos.shadeInteractor
+ underTest = kosmos.shadeControllerSceneImpl
+ }
+
+ @Test
+ fun animateCollapseShade_noForceNoExpansion() =
+ testScope.runTest {
+ // GIVEN shade is collapsed and a post-collapse action is enqueued
+ val testRunnable = mock<Runnable>()
+ setDeviceEntered(true)
+ setCollapsed()
+ underTest.addPostCollapseAction(testRunnable)
+
+ // WHEN a collapse is requested
+ underTest.animateCollapseShade(0, force = false, delayed = false, 1f)
+ runCurrent()
+
+ // THEN the shade remains collapsed and the post-collapse action ran
+ assertThat(sceneInteractor.desiredScene.value.key).isEqualTo(SceneKey.Gone)
+ verify(testRunnable, times(1)).run()
+ }
+
+ @Test
+ fun animateCollapseShade_expandedExcludeFlagOn() =
+ testScope.runTest {
+ // GIVEN shade is fully expanded and a post-collapse action is enqueued
+ val testRunnable = mock<Runnable>()
+ underTest.addPostCollapseAction(testRunnable)
+ setDeviceEntered(true)
+ setShadeFullyExpanded()
+
+ // WHEN a collapse is requested with FLAG_EXCLUDE_NOTIFICATION_PANEL
+ underTest.animateCollapseShade(CommandQueue.FLAG_EXCLUDE_NOTIFICATION_PANEL)
+ runCurrent()
+
+ // THEN the shade remains expanded and the post-collapse action did not run
+ assertThat(sceneInteractor.desiredScene.value.key).isEqualTo(SceneKey.Shade)
+ assertThat(shadeInteractor.isAnyFullyExpanded.value).isTrue()
+ verify(testRunnable, never()).run()
+ }
+
+ @Test
+ fun animateCollapseShade_locked() =
+ testScope.runTest {
+ // GIVEN shade is fully expanded on lockscreen
+ setDeviceEntered(false)
+ setShadeFullyExpanded()
+
+ // WHEN a collapse is requested
+ underTest.animateCollapseShade()
+ runCurrent()
+
+ // THEN the shade collapses back to lockscreen and the post-collapse action ran
+ assertThat(sceneInteractor.desiredScene.value.key).isEqualTo(SceneKey.Lockscreen)
+ }
+
+ @Test
+ fun animateCollapseShade_unlocked() =
+ testScope.runTest {
+ // GIVEN shade is fully expanded on an unlocked device
+ setDeviceEntered(true)
+ setShadeFullyExpanded()
+
+ // WHEN a collapse is requested
+ underTest.animateCollapseShade()
+ runCurrent()
+
+ // THEN the shade collapses back to lockscreen and the post-collapse action ran
+ assertThat(sceneInteractor.desiredScene.value.key).isEqualTo(SceneKey.Gone)
+ }
+
+ @Test
+ fun onCollapseShade_runPostCollapseActionsCalled() =
+ testScope.runTest {
+ // GIVEN shade is expanded and a post-collapse action is enqueued
+ val testRunnable = mock<Runnable>()
+ setShadeFullyExpanded()
+ underTest.addPostCollapseAction(testRunnable)
+
+ // WHEN shade collapses
+ setCollapsed()
+
+ // THEN post-collapse action ran
+ verify(testRunnable, times(1)).run()
+ }
+
+ @Test
+ fun postOnShadeExpanded() =
+ testScope.runTest {
+ // GIVEN shade is collapsed and a post-collapse action is enqueued
+ val testRunnable = mock<Runnable>()
+ setCollapsed()
+ underTest.postOnShadeExpanded(testRunnable)
+
+ // WHEN shade expands
+ setShadeFullyExpanded()
+
+ // THEN post-collapse action ran
+ verify(testRunnable, times(1)).run()
+ }
+
+ private fun setScene(key: SceneKey) {
+ sceneInteractor.changeScene(SceneModel(key), "test")
+ sceneInteractor.setTransitionState(
+ MutableStateFlow<ObservableTransitionState>(ObservableTransitionState.Idle(key))
+ )
+ testScope.runCurrent()
+ }
+
+ private fun setDeviceEntered(isEntered: Boolean) {
+ setScene(
+ if (isEntered) {
+ SceneKey.Gone
+ } else {
+ SceneKey.Lockscreen
+ }
+ )
+ assertThat(deviceEntryInteractor.isDeviceEntered.value).isEqualTo(isEntered)
+ }
+
+ private fun setCollapsed() {
+ setScene(SceneKey.Gone)
+ assertThat(shadeInteractor.isAnyExpanded.value).isFalse()
+ }
+
+ private fun setShadeFullyExpanded() {
+ setScene(SceneKey.Shade)
+ assertThat(shadeInteractor.isAnyFullyExpanded.value).isTrue()
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerTest.kt
similarity index 69%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerTest.kt
index 0a10b2c..0c7ce97 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerTest.kt
@@ -16,11 +16,10 @@
package com.android.systemui.statusbar.notification.collection.render
+import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.dump.DumpManager
-import com.android.systemui.flags.FakeFeatureFlagsClassic
-import com.android.systemui.flags.Flags
import com.android.systemui.statusbar.notification.collection.GroupEntryBuilder
import com.android.systemui.statusbar.notification.collection.ListEntry
import com.android.systemui.statusbar.notification.collection.NotifPipeline
@@ -34,18 +33,19 @@
import org.junit.Assert.assertThrows
import org.junit.Before
import org.junit.Test
+import org.junit.runner.RunWith
import org.mockito.Mockito.never
import org.mockito.Mockito.verify
import org.mockito.Mockito.verifyNoMoreInteractions
import org.mockito.Mockito.`when` as whenever
@SmallTest
+@RunWith(AndroidJUnit4::class)
class GroupExpansionManagerTest : SysuiTestCase() {
- private lateinit var gem: GroupExpansionManagerImpl
+ private lateinit var underTest: GroupExpansionManagerImpl
private val dumpManager: DumpManager = mock()
private val groupMembershipManager: GroupMembershipManager = mock()
- private val featureFlags = FakeFeatureFlagsClassic()
private val pipeline: NotifPipeline = mock()
private lateinit var beforeRenderListListener: OnBeforeRenderListListener
@@ -85,79 +85,57 @@
whenever(groupMembershipManager.getGroupSummary(summary1)).thenReturn(summary1)
whenever(groupMembershipManager.getGroupSummary(summary2)).thenReturn(summary2)
- gem = GroupExpansionManagerImpl(dumpManager, groupMembershipManager, featureFlags)
+ underTest = GroupExpansionManagerImpl(dumpManager, groupMembershipManager)
}
@Test
- fun testNotifyOnlyOnChange_enabled() {
- featureFlags.set(Flags.NOTIFICATION_GROUP_EXPANSION_CHANGE, true)
-
+ fun notifyOnlyOnChange() {
var listenerCalledCount = 0
- gem.registerGroupExpansionChangeListener { _, _ -> listenerCalledCount++ }
+ underTest.registerGroupExpansionChangeListener { _, _ -> listenerCalledCount++ }
- gem.setGroupExpanded(summary1, false)
+ underTest.setGroupExpanded(summary1, false)
assertThat(listenerCalledCount).isEqualTo(0)
- gem.setGroupExpanded(summary1, true)
+ underTest.setGroupExpanded(summary1, true)
assertThat(listenerCalledCount).isEqualTo(1)
- gem.setGroupExpanded(summary2, true)
+ underTest.setGroupExpanded(summary2, true)
assertThat(listenerCalledCount).isEqualTo(2)
- gem.setGroupExpanded(summary1, true)
+ underTest.setGroupExpanded(summary1, true)
assertThat(listenerCalledCount).isEqualTo(2)
- gem.setGroupExpanded(summary2, false)
+ underTest.setGroupExpanded(summary2, false)
assertThat(listenerCalledCount).isEqualTo(3)
}
@Test
- fun testNotifyOnlyOnChange_disabled() {
- featureFlags.set(Flags.NOTIFICATION_GROUP_EXPANSION_CHANGE, false)
-
- var listenerCalledCount = 0
- gem.registerGroupExpansionChangeListener { _, _ -> listenerCalledCount++ }
-
- gem.setGroupExpanded(summary1, false)
- assertThat(listenerCalledCount).isEqualTo(1)
- gem.setGroupExpanded(summary1, true)
- assertThat(listenerCalledCount).isEqualTo(2)
- gem.setGroupExpanded(summary2, true)
- assertThat(listenerCalledCount).isEqualTo(3)
- gem.setGroupExpanded(summary1, true)
- assertThat(listenerCalledCount).isEqualTo(4)
- gem.setGroupExpanded(summary2, false)
- assertThat(listenerCalledCount).isEqualTo(5)
- }
-
- @Test
- fun testExpandUnattachedEntry() {
- featureFlags.set(Flags.NOTIFICATION_GROUP_EXPANSION_CHANGE, true)
-
+ fun expandUnattachedEntry() {
// First, expand the entry when it is attached.
- gem.setGroupExpanded(summary1, true)
- assertThat(gem.isGroupExpanded(summary1)).isTrue()
+ underTest.setGroupExpanded(summary1, true)
+ assertThat(underTest.isGroupExpanded(summary1)).isTrue()
// Un-attach it, and un-expand it.
NotificationEntryBuilder.setNewParent(summary1, null)
- gem.setGroupExpanded(summary1, false)
+ underTest.setGroupExpanded(summary1, false)
// Expanding again should throw.
- assertThrows(IllegalArgumentException::class.java) { gem.setGroupExpanded(summary1, true) }
+ assertThrows(IllegalArgumentException::class.java) {
+ underTest.setGroupExpanded(summary1, true)
+ }
}
@Test
- fun testSyncWithPipeline() {
- featureFlags.set(Flags.NOTIFICATION_GROUP_EXPANSION_CHANGE, true)
- gem.attach(pipeline)
+ fun syncWithPipeline() {
+ underTest.attach(pipeline)
beforeRenderListListener = withArgCaptor {
verify(pipeline).addOnBeforeRenderListListener(capture())
}
val listener: OnGroupExpansionChangeListener = mock()
- gem.registerGroupExpansionChangeListener(listener)
+ underTest.registerGroupExpansionChangeListener(listener)
beforeRenderListListener.onBeforeRenderList(entries)
verify(listener, never()).onGroupExpansionChange(any(), any())
// Expand one of the groups.
- gem.setGroupExpanded(summary1, true)
+ underTest.setGroupExpanded(summary1, true)
verify(listener).onGroupExpansionChange(summary1.row, true)
// Empty the pipeline list and verify that the group is no longer expanded.
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/render/GroupMembershipManagerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/render/GroupMembershipManagerTest.kt
new file mode 100644
index 0000000..2cbcc5a
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/render/GroupMembershipManagerTest.kt
@@ -0,0 +1,124 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.collection.render
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.statusbar.notification.collection.GroupEntry
+import com.android.systemui.statusbar.notification.collection.GroupEntryBuilder
+import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class GroupMembershipManagerTest : SysuiTestCase() {
+ private var underTest = GroupMembershipManagerImpl()
+
+ @Test
+ fun isChildInGroup_topLevel() {
+ val topLevelEntry = NotificationEntryBuilder().setParent(GroupEntry.ROOT_ENTRY).build()
+ assertThat(underTest.isChildInGroup(topLevelEntry)).isFalse()
+ }
+
+ @Test
+ fun isChildInGroup_noParent() {
+ val noParentEntry = NotificationEntryBuilder().setParent(null).build()
+ assertThat(underTest.isChildInGroup(noParentEntry)).isFalse()
+ }
+
+ @Test
+ fun isChildInGroup_summary() {
+ val groupKey = "group"
+ val summary =
+ NotificationEntryBuilder()
+ .setGroup(mContext, groupKey)
+ .setGroupSummary(mContext, true)
+ .build()
+ GroupEntryBuilder().setKey(groupKey).setSummary(summary).build()
+
+ assertThat(underTest.isChildInGroup(summary)).isFalse()
+ }
+
+ @Test
+ fun isGroupSummary_topLevelEntry() {
+ val entry = NotificationEntryBuilder().setParent(GroupEntry.ROOT_ENTRY).build()
+ assertThat(underTest.isGroupSummary(entry)).isFalse()
+ }
+
+ @Test
+ fun isGroupSummary_summary() {
+ val groupKey = "group"
+ val summary =
+ NotificationEntryBuilder()
+ .setGroup(mContext, groupKey)
+ .setGroupSummary(mContext, true)
+ .build()
+ GroupEntryBuilder().setKey(groupKey).setSummary(summary).build()
+
+ assertThat(underTest.isGroupSummary(summary)).isTrue()
+ }
+
+ @Test
+ fun isGroupSummary_child() {
+ val groupKey = "group"
+ val summary =
+ NotificationEntryBuilder()
+ .setGroup(mContext, groupKey)
+ .setGroupSummary(mContext, true)
+ .build()
+ val entry = NotificationEntryBuilder().setGroup(mContext, groupKey).build()
+ GroupEntryBuilder().setKey(groupKey).setSummary(summary).addChild(entry).build()
+
+ assertThat(underTest.isGroupSummary(entry)).isFalse()
+ }
+
+ @Test
+ fun getGroupSummary_topLevelEntry() {
+ val entry = NotificationEntryBuilder().setParent(GroupEntry.ROOT_ENTRY).build()
+ assertThat(underTest.getGroupSummary(entry)).isNull()
+ }
+
+ @Test
+ fun getGroupSummary_summary() {
+ val groupKey = "group"
+ val summary =
+ NotificationEntryBuilder()
+ .setGroup(mContext, groupKey)
+ .setGroupSummary(mContext, true)
+ .build()
+ GroupEntryBuilder().setKey(groupKey).setSummary(summary).build()
+
+ assertThat(underTest.getGroupSummary(summary)).isEqualTo(summary)
+ }
+
+ @Test
+ fun getGroupSummary_child() {
+ val groupKey = "group"
+ val summary =
+ NotificationEntryBuilder()
+ .setGroup(mContext, groupKey)
+ .setGroupSummary(mContext, true)
+ .build()
+ val entry = NotificationEntryBuilder().setGroup(mContext, groupKey).build()
+ GroupEntryBuilder().setKey(groupKey).setSummary(summary).addChild(entry).build()
+
+ assertThat(underTest.getGroupSummary(entry)).isEqualTo(summary)
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ActivityStarterImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ActivityStarterImplTest.kt
similarity index 94%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ActivityStarterImplTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ActivityStarterImplTest.kt
index 00a86ff..cc4ebd4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ActivityStarterImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ActivityStarterImplTest.kt
@@ -1,15 +1,17 @@
/*
- * Copyright (C) 2023 The Android Open Source Project
+ * Copyright (C) 2024 The Android Open Source Project
*
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
- * except in compliance with the License. You may obtain a copy of the License at
+ * 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.
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
*/
package com.android.systemui.statusbar.phone
@@ -18,9 +20,9 @@
import android.content.Intent
import android.os.RemoteException
import android.os.UserHandle
-import android.testing.AndroidTestingRunner
import android.view.View
import android.widget.FrameLayout
+import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.keyguard.KeyguardUpdateMonitor
import com.android.systemui.ActivityIntentHelper
@@ -66,7 +68,7 @@
import org.mockito.MockitoAnnotations
@SmallTest
-@RunWith(AndroidTestingRunner::class)
+@RunWith(AndroidJUnit4::class)
class ActivityStarterImplTest : SysuiTestCase() {
@Mock private lateinit var centralSurfaces: CentralSurfaces
@Mock private lateinit var assistManager: AssistManager
@@ -139,7 +141,7 @@
}
@Test
- fun startPendingIntentMaybeDismissingKeyguard_keyguardShowing_showOverLockscreen_activityLaunchAnimator() {
+ fun startPendingIntentMaybeDismissingKeyguard_keyguardShowing_showOverLs_launchAnimator() {
val pendingIntent = mock(PendingIntent::class.java)
val parent = FrameLayout(context)
val view =
@@ -214,7 +216,7 @@
mainExecutor.runAllReady()
verify(deviceProvisionedController).isDeviceProvisioned
- verify(shadeController).runPostCollapseRunnables()
+ verify(shadeController).collapseShadeForActivityStart()
}
@Test
@@ -226,7 +228,7 @@
mainExecutor.runAllReady()
verify(deviceProvisionedController).isDeviceProvisioned
- verify(shadeController, never()).runPostCollapseRunnables()
+ verify(shadeController, never()).collapseShadeForActivityStart()
}
@Test
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/util/kotlin/BooleanFlowOperatorsTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/util/kotlin/BooleanFlowOperatorsTest.kt
new file mode 100644
index 0000000..b4a0a37
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/util/kotlin/BooleanFlowOperatorsTest.kt
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.util.kotlin
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.testKosmos
+import com.android.systemui.util.kotlin.BooleanFlowOperators.and
+import com.android.systemui.util.kotlin.BooleanFlowOperators.not
+import com.android.systemui.util.kotlin.BooleanFlowOperators.or
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class BooleanFlowOperatorsTest : SysuiTestCase() {
+
+ val kosmos = testKosmos()
+ val testScope = kosmos.testScope
+
+ @Test
+ fun and_allTrue_returnsTrue() =
+ testScope.runTest {
+ val result by collectLastValue(and(TRUE, TRUE))
+ assertThat(result).isTrue()
+ }
+
+ @Test
+ fun and_anyFalse_returnsFalse() =
+ testScope.runTest {
+ val result by collectLastValue(and(TRUE, FALSE, TRUE))
+ assertThat(result).isFalse()
+ }
+
+ @Test
+ fun and_allFalse_returnsFalse() =
+ testScope.runTest {
+ val result by collectLastValue(and(FALSE, FALSE, FALSE))
+ assertThat(result).isFalse()
+ }
+
+ @Test
+ fun or_allTrue_returnsTrue() =
+ testScope.runTest {
+ val result by collectLastValue(or(TRUE, TRUE))
+ assertThat(result).isTrue()
+ }
+
+ @Test
+ fun or_anyTrue_returnsTrue() =
+ testScope.runTest {
+ val result by collectLastValue(or(FALSE, TRUE, FALSE))
+ assertThat(result).isTrue()
+ }
+
+ @Test
+ fun or_allFalse_returnsFalse() =
+ testScope.runTest {
+ val result by collectLastValue(or(FALSE, FALSE, FALSE))
+ assertThat(result).isFalse()
+ }
+
+ @Test
+ fun not_true_returnsFalse() =
+ testScope.runTest {
+ val result by collectLastValue(not(TRUE))
+ assertThat(result).isFalse()
+ }
+
+ @Test
+ fun not_false_returnsFalse() =
+ testScope.runTest {
+ val result by collectLastValue(not(FALSE))
+ assertThat(result).isTrue()
+ }
+
+ private companion object {
+ val TRUE: Flow<Boolean>
+ get() = flowOf(true)
+ val FALSE: Flow<Boolean>
+ get() = flowOf(false)
+ }
+}
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/VolumeDialogController.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/VolumeDialogController.java
index 3d9645a..b1736b1 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/VolumeDialogController.java
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/VolumeDialogController.java
@@ -227,5 +227,10 @@
void onCaptionEnabledStateChanged(Boolean isEnabled, Boolean checkBeforeSwitch);
// requires version 2
void onShowCsdWarning(@AudioManager.CsdWarning int csdWarning, int durationMs);
+
+ /**
+ * Callback function for when the volume changed due to a physical key press.
+ */
+ void onVolumeChangedFromKey();
}
}
diff --git a/packages/SystemUI/res/layout/internet_connectivity_dialog.xml b/packages/SystemUI/res/layout/internet_connectivity_dialog.xml
index 16eba22..1365a11 100644
--- a/packages/SystemUI/res/layout/internet_connectivity_dialog.xml
+++ b/packages/SystemUI/res/layout/internet_connectivity_dialog.xml
@@ -17,6 +17,7 @@
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
android:id="@+id/internet_connectivity_dialog"
android:layout_width="@dimen/large_dialog_width"
@@ -386,9 +387,8 @@
</LinearLayout>
</LinearLayout>
- <LinearLayout
+ <androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/button_layout"
- android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
@@ -398,53 +398,58 @@
android:clickable="false"
android:focusable="false">
- <LinearLayout
+ <Button
+ android:id="@+id/apm_button"
+ style="@style/Widget.Dialog.Button.BorderButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:layout_weight="1"
- android:layout_gravity="start|center_vertical"
- android:orientation="horizontal">
- <Button
- android:id="@+id/apm_button"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="@string/turn_off_airplane_mode"
- android:ellipsize="end"
- android:maxLines="1"
- style="@style/Widget.Dialog.Button.BorderButton"
- android:clickable="true"
- android:focusable="true"/>
+ android:layout_marginEnd="10dp"
+ android:clickable="true"
+ android:ellipsize="end"
+ android:focusable="true"
+ android:maxLines="1"
+ android:text="@string/turn_off_airplane_mode"
+ app:layout_constrainedWidth="true"
+ app:layout_constraintHorizontal_bias="0"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toStartOf="@id/share_wifi_button"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent" />
- <Button
- android:id="@+id/share_wifi_button"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="@string/share_wifi_button_text"
- style="?android:attr/buttonBarNeutralButtonStyle"
- android:maxLines="1"
- android:ellipsize="end"
- android:clickable="true"
- android:focusable="true"
- android:visibility="gone"/>
- </LinearLayout>
-
- <LinearLayout
+ <Button
+ android:id="@+id/share_wifi_button"
+ style="?android:attr/buttonBarNeutralButtonStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:layout_marginStart="16dp"
- android:layout_gravity="end|center_vertical">
- <Button
- android:id="@+id/done_button"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="@string/inline_done_button"
- style="@style/Widget.Dialog.Button"
- android:maxLines="1"
- android:ellipsize="end"
- android:clickable="true"
- android:focusable="true"/>
- </LinearLayout>
- </LinearLayout>
+ android:layout_marginEnd="10dp"
+ android:clickable="true"
+ android:ellipsize="end"
+ android:focusable="true"
+ android:maxLines="1"
+ android:visibility="gone"
+ app:layout_constraintHorizontal_bias="0"
+ android:text="@string/share_wifi_button_text"
+ app:layout_constrainedWidth="true"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toStartOf="@id/done_button"
+ app:layout_constraintStart_toEndOf="@id/apm_button"
+ app:layout_constraintTop_toTopOf="parent" />
+
+ <Button
+ android:id="@+id/done_button"
+ style="@style/Widget.Dialog.Button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:clickable="true"
+ android:ellipsize="end"
+ android:focusable="true"
+ android:maxLines="1"
+ android:text="@string/inline_done_button"
+ app:layout_constrainedWidth="true"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toTopOf="parent" />
+ </androidx.constraintlayout.widget.ConstraintLayout>
</LinearLayout>
</androidx.core.widget.NestedScrollView>
diff --git a/packages/SystemUI/res/layout/ongoing_privacy_chip.xml b/packages/SystemUI/res/layout/ongoing_privacy_chip.xml
index 2c7467d..fab7840 100644
--- a/packages/SystemUI/res/layout/ongoing_privacy_chip.xml
+++ b/packages/SystemUI/res/layout/ongoing_privacy_chip.xml
@@ -27,7 +27,7 @@
tools:parentTag="com.android.systemui.privacy.OngoingPrivacyChip">
>
- <LinearLayout
+ <com.android.systemui.animation.view.LaunchableLinearLayout
android:id="@+id/icons_container"
android:layout_height="@dimen/ongoing_appops_chip_height"
android:layout_width="wrap_content"
diff --git a/packages/SystemUI/res/layout/super_notification_shade.xml b/packages/SystemUI/res/layout/super_notification_shade.xml
index dca84b9..b792acc 100644
--- a/packages/SystemUI/res/layout/super_notification_shade.xml
+++ b/packages/SystemUI/res/layout/super_notification_shade.xml
@@ -27,10 +27,11 @@
android:fitsSystemWindows="true">
<!-- Placeholder for the communal UI that will be replaced if the feature is enabled. -->
- <ViewStub
+ <View
android:id="@+id/communal_ui_stub"
android:layout_width="match_parent"
- android:layout_height="match_parent" />
+ android:layout_height="match_parent"
+ android:visibility="gone" />
<com.android.systemui.scrim.ScrimView
android:id="@+id/scrim_behind"
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index c630a7f..4209c1f 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -1728,6 +1728,12 @@
<dimen name="communal_grid_height">630dp</dimen>
<!-- Number of columns for each communal card -->
<integer name="communal_grid_columns_per_card">6</integer>
+ <!-- Width of area on right edge of screen in which swipes will open the communal hub -->
+ <dimen name="communal_right_edge_swipe_region_width">16dp</dimen>
+ <!-- Height of area at top of communal hub where swipes should open the notification shade -->
+ <dimen name="communal_top_edge_swipe_region_height">32dp</dimen>
+ <!-- Height of area at bottom of communal hub where swipes should open the bouncer -->
+ <dimen name="communal_bottom_edge_swipe_region_height">32dp</dimen>
<dimen name="drag_and_drop_icon_size">70dp</dimen>
@@ -1799,6 +1805,9 @@
<dimen name="dream_overlay_complication_smartspace_padding">24dp</dimen>
<dimen name="dream_overlay_complication_smartspace_max_width">408dp</dimen>
+ <!-- The width of the swipe target to initiate opening communal hub over dreams. -->
+ <dimen name="communal_gesture_initiation_width">48dp</dimen>
+
<!-- The position of the end guide, which dream overlay complications can align their start with
if their end is aligned with the parent end. Represented as the percentage over from the
start of the parent container. -->
@@ -1932,5 +1941,9 @@
<dimen name="bouncer_user_switcher_view_mode_view_flipper_bottom_margin">0dp</dimen>
<!-- UDFPS view attributes -->
- <dimen name="udfps_icon_size">6mm</dimen>
+ <!-- UDFPS icon size in microns/um -->
+ <dimen name="udfps_icon_size" format="float">6000</dimen>
+ <!-- Microns/ums (1000 um = 1mm) per pixel for the given device. If unspecified, UI that
+ relies on this value will not be sized correctly. -->
+ <item name="pixel_pitch" format="float" type="dimen">-1</item>
</resources>
diff --git a/packages/SystemUI/res/values/ids.xml b/packages/SystemUI/res/values/ids.xml
index ec4c7d5..8ec5ccd 100644
--- a/packages/SystemUI/res/values/ids.xml
+++ b/packages/SystemUI/res/values/ids.xml
@@ -227,6 +227,7 @@
<item type="id" name="ambient_indication_container" />
<item type="id" name="status_view_media_container" />
<item type="id" name="smart_space_barrier_bottom" />
+ <item type="id" name="weather_clock_date_and_icons_barrier_bottom" />
<!-- Privacy dialog -->
<item type="id" name="privacy_dialog_close_app_button" />
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 9bc7681..2b43360 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -1089,6 +1089,8 @@
<string name="cta_label_to_open_widget_picker">Add more widgets</string>
<!-- Text for the popup to be displayed after dismissing the CTA tile. [CHAR LIMIT=50] -->
<string name="popup_on_dismiss_cta_tile_text">Long press to customize widgets</string>
+ <!-- Text for the button to configure widgets after long press. [CHAR LIMIT=50] -->
+ <string name="button_to_configure_widgets_text">Customize widgets</string>
<!-- Label for the button which configures widgets [CHAR LIMIT=NONE] -->
<string name="edit_widget">Edit widget</string>
<!-- Description for the button that removes a widget on click. [CHAR LIMIT=50] -->
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/IOverviewProxy.aidl b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/IOverviewProxy.aidl
index 7088829..d191a3c 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/IOverviewProxy.aidl
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/IOverviewProxy.aidl
@@ -20,7 +20,6 @@
import android.graphics.Region;
import android.os.Bundle;
import android.view.MotionEvent;
-import android.view.SurfaceControl;
import com.android.systemui.shared.recents.ISystemUiProxy;
// Next ID: 29
@@ -99,11 +98,6 @@
void enterStageSplitFromRunningApp(boolean leftOrTop) = 25;
/**
- * Sent when the surface for navigation bar is created or changed
- */
- void onNavigationBarSurface(in SurfaceControl surface) = 26;
-
- /**
* Sent when the task bar stash state is toggled.
*/
void onTaskbarToggled() = 27;
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/InputChannelCompat.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/InputChannelCompat.java
index 259cca8..9e92c93 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/InputChannelCompat.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/InputChannelCompat.java
@@ -16,8 +16,11 @@
package com.android.systemui.shared.system;
-import android.graphics.Matrix;
+import static android.os.Trace.TRACE_TAG_INPUT;
+
import android.os.Looper;
+import android.os.Trace;
+import android.util.Log;
import android.view.BatchedInputEventReceiver;
import android.view.Choreographer;
import android.view.InputChannel;
@@ -52,23 +55,24 @@
return target.addBatch(src);
}
- /** @see MotionEvent#createRotateMatrix */
- public static Matrix createRotationMatrix(
- /*@Surface.Rotation*/ int rotation, int displayW, int displayH) {
- return MotionEvent.createRotateMatrix(rotation, displayW, displayH);
- }
-
/**
* @see BatchedInputEventReceiver
*/
public static class InputEventReceiver {
+ private final String mName;
private final BatchedInputEventReceiver mReceiver;
+ @Deprecated
public InputEventReceiver(InputChannel inputChannel, Looper looper,
Choreographer choreographer, final InputEventListener listener) {
- mReceiver = new BatchedInputEventReceiver(inputChannel, looper, choreographer) {
+ this("unknown", inputChannel, looper, choreographer, listener);
+ }
+ public InputEventReceiver(String name, InputChannel inputChannel, Looper looper,
+ Choreographer choreographer, final InputEventListener listener) {
+ mName = name;
+ mReceiver = new BatchedInputEventReceiver(inputChannel, looper, choreographer) {
@Override
public void onInputEvent(InputEvent event) {
listener.onInputEvent(event);
@@ -89,6 +93,9 @@
*/
public void dispose() {
mReceiver.dispose();
+ Trace.instant(TRACE_TAG_INPUT, "InputMonitorCompat-" + mName + " receiver disposed");
+ Log.d(InputMonitorCompat.TAG, "Input event receiver for monitor (" + mName
+ + ") disposed");
}
}
}
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/InputMonitorCompat.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/InputMonitorCompat.java
index c4aac11..78beaf7 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/InputMonitorCompat.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/InputMonitorCompat.java
@@ -17,8 +17,13 @@
import android.hardware.input.InputManagerGlobal;
import android.os.Looper;
+import android.os.Trace;
+import android.util.Log;
import android.view.Choreographer;
import android.view.InputMonitor;
+import android.view.SurfaceControl;
+
+import androidx.annotation.NonNull;
import com.android.systemui.shared.system.InputChannelCompat.InputEventListener;
import com.android.systemui.shared.system.InputChannelCompat.InputEventReceiver;
@@ -27,14 +32,20 @@
* @see android.view.InputMonitor
*/
public class InputMonitorCompat {
+ static final String TAG = "InputMonitorCompat";
private final InputMonitor mInputMonitor;
+ private final String mName;
/**
* Monitor input on the specified display for gestures.
*/
- public InputMonitorCompat(String name, int displayId) {
+ public InputMonitorCompat(@NonNull String name, int displayId) {
+ mName = name + "-disp" + displayId;
mInputMonitor = InputManagerGlobal.getInstance()
.monitorGestureInput(name, displayId);
+ Trace.instant(Trace.TRACE_TAG_INPUT, "InputMonitorCompat-" + mName + " created");
+ Log.d(TAG, "Input monitor (" + mName + ") created");
+
}
/**
@@ -45,10 +56,19 @@
}
/**
+ * @see InputMonitor#getSurface()
+ */
+ public SurfaceControl getSurface() {
+ return mInputMonitor.getSurface();
+ }
+
+ /**
* @see InputMonitor#dispose()
*/
public void dispose() {
mInputMonitor.dispose();
+ Trace.instant(Trace.TRACE_TAG_INPUT, "InputMonitorCompat-" + mName + " disposed");
+ Log.d(TAG, "Input monitor (" + mName + ") disposed");
}
/**
@@ -56,7 +76,9 @@
*/
public InputEventReceiver getInputReceiver(Looper looper, Choreographer choreographer,
InputEventListener listener) {
- return new InputEventReceiver(mInputMonitor.getInputChannel(), looper, choreographer,
+ Trace.instant(Trace.TRACE_TAG_INPUT, "InputMonitorCompat-" + mName + " receiver created");
+ Log.d(TAG, "Input event receiver for monitor (" + mName + ") created");
+ return new InputEventReceiver(mName, mInputMonitor.getInputChannel(), looper, choreographer,
listener);
}
}
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java
index 033f93b..ad30317 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java
@@ -477,9 +477,13 @@
public void dump(PrintWriter pw, String[] args) {
pw.println("KeyguardClockSwitch:");
pw.println(" mSmallClockFrame = " + mSmallClockFrame);
- pw.println(" mSmallClockFrame.alpha = " + mSmallClockFrame.getAlpha());
+ if (mSmallClockFrame != null) {
+ pw.println(" mSmallClockFrame.alpha = " + mSmallClockFrame.getAlpha());
+ }
pw.println(" mLargeClockFrame = " + mLargeClockFrame);
- pw.println(" mLargeClockFrame.alpha = " + mLargeClockFrame.getAlpha());
+ if (mLargeClockFrame != null) {
+ pw.println(" mLargeClockFrame.alpha = " + mLargeClockFrame.getAlpha());
+ }
pw.println(" mStatusArea = " + mStatusArea);
pw.println(" mDisplayedClockSize = " + mDisplayedClockSize);
}
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java
index 8e5d0da..ecce223 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java
@@ -37,6 +37,7 @@
import android.app.admin.DevicePolicyManager;
import android.content.Intent;
import android.content.res.ColorStateList;
+import android.content.res.Configuration;
import android.content.res.Resources;
import android.hardware.biometrics.BiometricRequestConstants;
import android.media.AudioManager;
@@ -390,6 +391,11 @@
mSecurityViewFlipperController.updateConstraints(useSplitBouncer);
}
}
+
+ @Override
+ public void onConfigChanged(Configuration newConfig) {
+ configureMode();
+ }
};
private final KeyguardUpdateMonitorCallback mKeyguardUpdateMonitorCallback =
new KeyguardUpdateMonitorCallback() {
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
index fe96099..536f3af 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
@@ -114,7 +114,6 @@
import com.android.settingslib.fuelgauge.BatteryStatus;
import com.android.systemui.CoreStartable;
import com.android.systemui.Dumpable;
-import com.android.systemui.Flags;
import com.android.systemui.biometrics.AuthController;
import com.android.systemui.biometrics.FingerprintInteractiveToAuthProvider;
import com.android.systemui.broadcast.BroadcastDispatcher;
@@ -383,7 +382,6 @@
private List<SubscriptionInfo> mSubscriptionInfo;
@VisibleForTesting
protected int mFingerprintRunningState = BIOMETRIC_STATE_STOPPED;
- private boolean mFingerprintDetectRunning;
private boolean mIsDreaming;
private boolean mLogoutEnabled;
private int mActiveMobileDataSubscription = SubscriptionManager.INVALID_SUBSCRIPTION_ID;
@@ -1005,7 +1003,6 @@
final boolean wasCancellingRestarting = mFingerprintRunningState
== BIOMETRIC_STATE_CANCELLING_RESTARTING;
mFingerprintRunningState = BIOMETRIC_STATE_STOPPED;
- mFingerprintDetectRunning = false;
if (wasCancellingRestarting) {
KeyguardUpdateMonitor.this.updateFingerprintListeningState(BIOMETRIC_ACTION_UPDATE);
} else {
@@ -1114,9 +1111,6 @@
boolean wasRunning = mFingerprintRunningState == BIOMETRIC_STATE_RUNNING;
boolean isRunning = fingerprintRunningState == BIOMETRIC_STATE_RUNNING;
mFingerprintRunningState = fingerprintRunningState;
- if (mFingerprintRunningState == BIOMETRIC_STATE_STOPPED) {
- mFingerprintDetectRunning = false;
- }
mLogger.logFingerprintRunningState(mFingerprintRunningState);
// Clients of KeyguardUpdateMonitor don't care about the internal state about the
// asynchronousness of the cancel cycle. So only notify them if the actually running state
@@ -1644,11 +1638,11 @@
void setAssistantVisible(boolean assistantVisible) {
mAssistantVisible = assistantVisible;
mLogger.logAssistantVisible(mAssistantVisible);
- if (getFaceAuthInteractor() != null) {
- getFaceAuthInteractor().onAssistantTriggeredOnLockScreen();
- }
updateFingerprintListeningState(BIOMETRIC_ACTION_UPDATE);
if (mAssistantVisible) {
+ if (getFaceAuthInteractor() != null) {
+ getFaceAuthInteractor().onAssistantTriggeredOnLockScreen();
+ }
requestActiveUnlock(
ActiveUnlockConfig.ActiveUnlockRequestOrigin.ASSISTANT,
"assistant",
@@ -2105,7 +2099,6 @@
@VisibleForTesting
void resetBiometricListeningState() {
mFingerprintRunningState = BIOMETRIC_STATE_STOPPED;
- mFingerprintDetectRunning = false;
}
@VisibleForTesting
@@ -2544,10 +2537,8 @@
return;
}
final boolean shouldListenForFingerprint = shouldListenForFingerprint(isUdfpsSupported());
- final boolean running = mFingerprintRunningState == BIOMETRIC_STATE_RUNNING;
- final boolean runningOrRestarting = running
+ final boolean runningOrRestarting = mFingerprintRunningState == BIOMETRIC_STATE_RUNNING
|| mFingerprintRunningState == BIOMETRIC_STATE_CANCELLING_RESTARTING;
- final boolean runDetect = shouldRunFingerprintDetect();
if (runningOrRestarting && !shouldListenForFingerprint) {
if (action == BIOMETRIC_ACTION_START) {
mLogger.v("Ignoring stopListeningForFingerprint()");
@@ -2559,24 +2550,10 @@
mLogger.v("Ignoring startListeningForFingerprint()");
return;
}
- startListeningForFingerprint(runDetect);
- } else if (running && runDetect && !mFingerprintDetectRunning) {
- if (action == BIOMETRIC_ACTION_STOP) {
- mLogger.v("Ignoring startListeningForFingerprint(detect)");
- return;
- }
- // stop running authentication and start running fingerprint detection
- stopListeningForFingerprint();
- startListeningForFingerprint(true);
+ startListeningForFingerprint();
}
}
- private boolean shouldRunFingerprintDetect() {
- return !isUnlockingWithFingerprintAllowed()
- || (Flags.runFingerprintDetectOnDismissibleKeyguard()
- && getUserCanSkipBouncer(mSelectedUserInteractor.getSelectedUserId()));
- }
-
/**
* If a user is encrypted or not.
* This is NOT related to the lock screen being visible or not.
@@ -2832,6 +2809,7 @@
&& biometricEnabledForUser
&& !isUserInLockdown(user);
final boolean strongerAuthRequired = !isUnlockingWithFingerprintAllowed();
+ final boolean isSideFps = isSfpsSupported() && isSfpsEnrolled();
final boolean shouldListenBouncerState =
!strongerAuthRequired || !mPrimaryBouncerIsOrWillBeShowing;
@@ -2894,7 +2872,7 @@
}
}
- private void startListeningForFingerprint(boolean runDetect) {
+ private void startListeningForFingerprint() {
final int userId = mSelectedUserInteractor.getSelectedUserId();
final boolean unlockPossible = isUnlockWithFingerprintPossible(userId);
if (mFingerprintCancelSignal != null) {
@@ -2924,20 +2902,18 @@
mFingerprintInteractiveToAuthProvider.getVendorExtension(userId));
}
- if (runDetect) {
+ if (!isUnlockingWithFingerprintAllowed()) {
mLogger.v("startListeningForFingerprint - detect");
mFpm.detectFingerprint(
mFingerprintCancelSignal,
mFingerprintDetectionCallback,
fingerprintAuthenticateOptions);
- mFingerprintDetectRunning = true;
} else {
mLogger.v("startListeningForFingerprint");
mFpm.authenticate(null /* crypto */, mFingerprintCancelSignal,
mFingerprintAuthenticationCallback,
null /* handler */,
fingerprintAuthenticateOptions);
- mFingerprintDetectRunning = false;
}
setFingerprintRunningState(BIOMETRIC_STATE_RUNNING);
}
@@ -3962,7 +3938,6 @@
mSelectedUserInteractor.getSelectedUserId()));
pw.println(" getUserUnlockedWithBiometric()="
+ getUserUnlockedWithBiometric(mSelectedUserInteractor.getSelectedUserId()));
- pw.println(" mFingerprintDetectRunning=" + mFingerprintDetectRunning);
pw.println(" SIM States:");
for (SimData data : mSimDatas.values()) {
pw.println(" " + data.toString());
diff --git a/packages/SystemUI/src/com/android/systemui/GuestResumeSessionReceiver.java b/packages/SystemUI/src/com/android/systemui/GuestResumeSessionReceiver.java
index 0f5f869..4372826 100644
--- a/packages/SystemUI/src/com/android/systemui/GuestResumeSessionReceiver.java
+++ b/packages/SystemUI/src/com/android/systemui/GuestResumeSessionReceiver.java
@@ -20,11 +20,12 @@
import android.content.Context;
import android.content.DialogInterface;
import android.content.pm.UserInfo;
+import android.os.HandlerExecutor;
+import android.os.HandlerThread;
import android.os.UserHandle;
import androidx.annotation.NonNull;
-import com.android.systemui.res.R;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.logging.UiEventLogger;
import com.android.systemui.GuestResetOrExitSessionReceiver.ResetSessionDialogFactory;
@@ -32,6 +33,7 @@
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.qs.QSUserSwitcherEvent;
+import com.android.systemui.res.R;
import com.android.systemui.settings.UserTracker;
import com.android.systemui.statusbar.phone.SystemUIDialog;
import com.android.systemui.statusbar.policy.UserSwitcherController;
@@ -61,6 +63,7 @@
private final SecureSettings mSecureSettings;
private final ResetSessionDialogFactory mResetSessionDialogFactory;
private final GuestSessionNotification mGuestSessionNotification;
+ private final HandlerThread mHandlerThread;
@VisibleForTesting
public final UserTracker.Callback mUserChangedCallback =
@@ -111,13 +114,16 @@
mSecureSettings = secureSettings;
mGuestSessionNotification = guestSessionNotification;
mResetSessionDialogFactory = resetSessionDialogFactory;
+ mHandlerThread = new HandlerThread("GuestResumeSessionReceiver");
+ mHandlerThread.start();
}
/**
* Register this receiver with the {@link BroadcastDispatcher}
*/
public void register() {
- mUserTracker.addCallback(mUserChangedCallback, mMainExecutor);
+ mUserTracker.addCallback(mUserChangedCallback,
+ new HandlerExecutor(mHandlerThread.getThreadHandler()));
}
private void cancelDialog() {
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/AccessibilityModule.kt b/packages/SystemUI/src/com/android/systemui/accessibility/AccessibilityModule.kt
index 8c2d221..35f9344 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/AccessibilityModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/AccessibilityModule.kt
@@ -16,6 +16,8 @@
package com.android.systemui.accessibility
+import com.android.systemui.accessibility.data.repository.AccessibilityQsShortcutsRepository
+import com.android.systemui.accessibility.data.repository.AccessibilityQsShortcutsRepositoryImpl
import com.android.systemui.accessibility.data.repository.ColorCorrectionRepository
import com.android.systemui.accessibility.data.repository.ColorCorrectionRepositoryImpl
import com.android.systemui.accessibility.data.repository.ColorInversionRepository
@@ -31,4 +33,9 @@
@Binds
fun colorInversionRepository(impl: ColorInversionRepositoryImpl): ColorInversionRepository
+
+ @Binds
+ fun accessibilityQsShortcutsRepository(
+ impl: AccessibilityQsShortcutsRepositoryImpl
+ ): AccessibilityQsShortcutsRepository
}
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/data/repository/AccessibilityQsShortcutsRepository.kt b/packages/SystemUI/src/com/android/systemui/accessibility/data/repository/AccessibilityQsShortcutsRepository.kt
new file mode 100644
index 0000000..401ac0f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/data/repository/AccessibilityQsShortcutsRepository.kt
@@ -0,0 +1,54 @@
+/*
+ * 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.accessibility.data.repository
+
+import android.util.SparseArray
+import androidx.annotation.GuardedBy
+import com.android.systemui.dagger.SysUISingleton
+import javax.inject.Inject
+import kotlinx.coroutines.flow.SharedFlow
+
+/** Provides data related to accessibility quick setting shortcut option. */
+interface AccessibilityQsShortcutsRepository {
+ /**
+ * Observable for the a11y features the user chooses in the Settings app to use the quick
+ * setting option.
+ */
+ fun a11yQsShortcutTargets(userId: Int): SharedFlow<Set<String>>
+}
+
+@SysUISingleton
+class AccessibilityQsShortcutsRepositoryImpl
+@Inject
+constructor(
+ private val userA11yQsShortcutsRepositoryFactory: UserA11yQsShortcutsRepository.Factory,
+) : AccessibilityQsShortcutsRepository {
+
+ @GuardedBy("userA11yQsShortcutsRepositories")
+ private val userA11yQsShortcutsRepositories = SparseArray<UserA11yQsShortcutsRepository>()
+
+ override fun a11yQsShortcutTargets(userId: Int): SharedFlow<Set<String>> {
+ return synchronized(userA11yQsShortcutsRepositories) {
+ if (userId !in userA11yQsShortcutsRepositories) {
+ val userA11yQsShortcutsRepository =
+ userA11yQsShortcutsRepositoryFactory.create(userId)
+ userA11yQsShortcutsRepositories.put(userId, userA11yQsShortcutsRepository)
+ }
+ userA11yQsShortcutsRepositories.get(userId).targets
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/data/repository/UserA11yQsShortcutsRepository.kt b/packages/SystemUI/src/com/android/systemui/accessibility/data/repository/UserA11yQsShortcutsRepository.kt
new file mode 100644
index 0000000..ed91f03
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/data/repository/UserA11yQsShortcutsRepository.kt
@@ -0,0 +1,78 @@
+/*
+ * 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.accessibility.data.repository
+
+import android.provider.Settings
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.qs.pipeline.data.repository.TileSpecRepository
+import com.android.systemui.util.settings.SecureSettings
+import com.android.systemui.util.settings.SettingsProxyExt.observerFlow
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onStart
+import kotlinx.coroutines.flow.shareIn
+
+/**
+ * Single user version of [AccessibilityQsShortcutsRepository]. It provides a similar interface as
+ * [TileSpecRepository], but focusing solely on the user it was created for. It observes the changes
+ * on the [Settings.Secure.ACCESSIBILITY_QS_TARGETS] for a given user
+ */
+class UserA11yQsShortcutsRepository
+@AssistedInject
+constructor(
+ @Assisted private val userId: Int,
+ private val secureSettings: SecureSettings,
+ @Application private val applicationScope: CoroutineScope,
+ @Background private val backgroundDispatcher: CoroutineDispatcher,
+) {
+ val targets =
+ secureSettings
+ .observerFlow(userId, SETTING_NAME)
+ // Force an update
+ .onStart { emit(Unit) }
+ .map { getA11yQsShortcutTargets(userId) }
+ .flowOn(backgroundDispatcher)
+ .shareIn(
+ scope = applicationScope,
+ started = SharingStarted.WhileSubscribed(),
+ replay = 1
+ )
+
+ private fun getA11yQsShortcutTargets(userId: Int): Set<String> {
+ val settingValue = secureSettings.getStringForUser(SETTING_NAME, userId) ?: ""
+ return settingValue.split(SETTING_SEPARATOR).filterNot { it.isEmpty() }.toSet()
+ }
+
+ companion object {
+ const val SETTING_NAME = Settings.Secure.ACCESSIBILITY_QS_TARGETS
+ const val SETTING_SEPARATOR = ":"
+ }
+
+ @AssistedFactory
+ interface Factory {
+ fun create(
+ userId: Int,
+ ): UserA11yQsShortcutsRepository
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleController.kt b/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleController.kt
index 86f372a..71d0e7d 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleController.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleController.kt
@@ -35,7 +35,6 @@
import com.android.systemui.biometrics.data.repository.FacePropertyRepository
import com.android.systemui.biometrics.shared.model.UdfpsOverlayParams
import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.deviceentry.shared.DeviceEntryUdfpsRefactor
import com.android.systemui.keyguard.WakefulnessLifecycle
import com.android.systemui.log.core.LogLevel
import com.android.systemui.plugins.statusbar.StatusBarStateController
@@ -51,7 +50,6 @@
import com.android.systemui.statusbar.policy.ConfigurationController
import com.android.systemui.statusbar.policy.KeyguardStateController
import com.android.systemui.util.ViewController
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import java.io.PrintWriter
import javax.inject.Inject
import javax.inject.Provider
@@ -64,7 +62,6 @@
*
* The ripple uses the accent color of the current theme.
*/
-@ExperimentalCoroutinesApi
@SysUISingleton
class AuthRippleController @Inject constructor(
private val sysuiContext: Context,
@@ -316,18 +313,6 @@
mView.fadeDwellRipple()
}
}
-
- override fun onBiometricDetected(
- userId: Int,
- biometricSourceType: BiometricSourceType,
- isStrongBiometric: Boolean
- ) {
- // TODO (b/309804148): add support detect auth ripple for deviceEntryUdfpsRefactor
- if (!DeviceEntryUdfpsRefactor.isEnabled &&
- keyguardUpdateMonitor.getUserCanSkipBouncer(userId)) {
- showUnlockRipple(biometricSourceType)
- }
- }
}
private val configurationChangedListener =
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/BiometricStatusRepository.kt b/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/BiometricStatusRepository.kt
index ad2136a..d28dbc0 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/BiometricStatusRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/BiometricStatusRepository.kt
@@ -94,6 +94,10 @@
override fun onAuthenticationStopped() {
updateFingerprintAuthenticateReason(AuthenticationReason.NotRunning)
}
+
+ override fun onAuthenticationSucceeded(requestReason: Int, userId: Int) {}
+
+ override fun onAuthenticationFailed(requestReason: Int, userId: Int) {}
}
updateFingerprintAuthenticateReason(AuthenticationReason.NotRunning)
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/SideFpsSensorInteractor.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/SideFpsSensorInteractor.kt
index 348b54e..c3dc2d4 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/SideFpsSensorInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/SideFpsSensorInteractor.kt
@@ -26,20 +26,24 @@
import com.android.systemui.biometrics.shared.model.FingerprintSensorType
import com.android.systemui.biometrics.shared.model.isDefaultOrientation
import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.keyguard.data.repository.BiometricSettingsRepository
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.log.SideFpsLogger
import com.android.systemui.res.R
import java.util.Optional
import javax.inject.Inject
+import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
+@ExperimentalCoroutinesApi
@SysUISingleton
class SideFpsSensorInteractor
@Inject
@@ -49,6 +53,7 @@
windowManager: WindowManager,
displayStateInteractor: DisplayStateInteractor,
fingerprintInteractiveToAuthProvider: Optional<FingerprintInteractiveToAuthProvider>,
+ biometricSettingsRepository: BiometricSettingsRepository,
keyguardTransitionInteractor: KeyguardTransitionInteractor,
private val logger: SideFpsLogger,
) {
@@ -84,13 +89,24 @@
.map { it ?: 0L }
.onEach { logger.authDurationChanged(it) }
+ private val isSettingEnabled: Flow<Boolean> =
+ biometricSettingsRepository.isFingerprintEnrolledAndEnabled
+ .flatMapLatest { enabledAndEnrolled ->
+ if (!enabledAndEnrolled || fingerprintInteractiveToAuthProvider.isEmpty) {
+ flowOf(false)
+ } else {
+ fingerprintInteractiveToAuthProvider.get().enabledForCurrentUser
+ }
+ }
+ .onEach { logger.restToUnlockSettingEnabledChanged(it) }
+
val isProlongedTouchRequiredForAuthentication: Flow<Boolean> =
- if (fingerprintInteractiveToAuthProvider.isEmpty || !isProlongedTouchEnabledForDevice) {
+ if (!isProlongedTouchEnabledForDevice) {
flowOf(false)
} else {
combine(
isAvailable,
- fingerprintInteractiveToAuthProvider.get().enabledForCurrentUser
+ isSettingEnabled,
) { sfpsAvailable, isSettingEnabled ->
sfpsAvailable && isSettingEnabled
}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/UdfpsOverlayInteractor.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/UdfpsOverlayInteractor.kt
index 4fc1b58..a77cc1f 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/UdfpsOverlayInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/UdfpsOverlayInteractor.kt
@@ -17,6 +17,7 @@
package com.android.systemui.biometrics.domain.interactor
import android.content.Context
+import android.util.Log
import android.view.MotionEvent
import com.android.systemui.biometrics.AuthController
import com.android.systemui.biometrics.shared.model.UdfpsOverlayParams
@@ -42,12 +43,23 @@
class UdfpsOverlayInteractor
@Inject
constructor(
- @Application context: Context,
+ @Application private val context: Context,
private val authController: AuthController,
private val selectedUserInteractor: SelectedUserInteractor,
@Application scope: CoroutineScope
) {
- private val iconSize: Int = context.resources.getDimensionPixelSize(R.dimen.udfps_icon_size)
+ private fun calculateIconSize(): Int {
+ val pixelPitch = context.resources.getFloat(R.dimen.pixel_pitch)
+ if (pixelPitch <= 0) {
+ Log.e(
+ "UdfpsOverlayInteractor",
+ "invalid pixelPitch: $pixelPitch. Pixel pitch must be updated per device."
+ )
+ }
+ return (context.resources.getFloat(R.dimen.udfps_icon_size) / pixelPitch).toInt()
+ }
+
+ private var iconSize: Int = calculateIconSize()
/** Whether a touch is within the under-display fingerprint sensor area */
fun isTouchWithinUdfpsArea(ev: MotionEvent): Boolean {
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricCustomizedViewBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricCustomizedViewBinder.kt
index 16e7f05..96582cb 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricCustomizedViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricCustomizedViewBinder.kt
@@ -114,7 +114,7 @@
private fun PromptContentItem.doesExceedMaxLinesIfTwoColumn(
resources: Resources,
): Boolean {
- val passedInText: CharSequence =
+ val passedInText: String =
when (this) {
is PromptContentItemPlainText -> text
is PromptContentItemBulletedText -> text
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt
index dca0338..0f1340a 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt
@@ -21,6 +21,7 @@
import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.Drawable
import android.hardware.biometrics.BiometricPrompt
+import android.hardware.biometrics.Flags.customBiometricPrompt
import android.hardware.biometrics.PromptContentView
import android.util.Log
import android.view.HapticFeedbackConstants
@@ -240,7 +241,7 @@
promptSelectorInteractor.prompt
.map {
when {
- it == null -> null
+ !customBiometricPrompt() || it == null -> null
it.logoRes != -1 -> context.resources.getDrawable(it.logoRes, context.theme)
it.logoBitmap != null -> BitmapDrawable(context.resources, it.logoBitmap)
else -> context.packageManager.getApplicationIcon(it.opPackageName)
@@ -258,7 +259,9 @@
/** Custom content view for the prompt. */
val contentView: Flow<PromptContentView?> =
- promptSelectorInteractor.prompt.map { it?.contentView }.distinctUntilChanged()
+ promptSelectorInteractor.prompt
+ .map { if (customBiometricPrompt()) it?.contentView else null }
+ .distinctUntilChanged()
private val originalDescription =
promptSelectorInteractor.prompt.map { it?.description ?: "" }.distinctUntilChanged()
diff --git a/packages/SystemUI/src/com/android/systemui/common/ui/domain/interactor/ConfigurationInteractor.kt b/packages/SystemUI/src/com/android/systemui/common/ui/domain/interactor/ConfigurationInteractor.kt
index 1353985..5f6ff82 100644
--- a/packages/SystemUI/src/com/android/systemui/common/ui/domain/interactor/ConfigurationInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/common/ui/domain/interactor/ConfigurationInteractor.kt
@@ -72,6 +72,9 @@
val onAnyConfigurationChange: Flow<Unit> =
repository.onAnyConfigurationChange.onStart { emit(Unit) }
+ /** Emits the new configuration on any configuration change */
+ val configurationValues: Flow<Configuration> = repository.configurationValues
+
/** Emits the current resolution scaling factor */
val scaleForResolution: Flow<Float> = repository.scaleForResolution
}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalRepository.kt b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalRepository.kt
index 1f4be40..addd880 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalRepository.kt
@@ -20,13 +20,18 @@
import com.android.systemui.communal.shared.model.CommunalSceneKey
import com.android.systemui.communal.shared.model.ObservableCommunalTransitionState
import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.flags.FeatureFlagsClassic
import com.android.systemui.flags.Flags
import com.android.systemui.scene.data.repository.SceneContainerRepository
import com.android.systemui.scene.shared.flag.SceneContainerFlags
import com.android.systemui.scene.shared.model.SceneKey
+import com.android.systemui.user.data.repository.UserRepository
+import com.android.systemui.util.settings.SecureSettings
+import com.android.systemui.util.settings.SettingsProxyExt.observerFlow
import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
@@ -34,16 +39,26 @@
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onStart
+import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.withContext
/** Encapsulates the state of communal mode. */
interface CommunalRepository {
/** Whether communal features are enabled. */
val isCommunalEnabled: Boolean
+ /**
+ * A {@link StateFlow} that tracks whether communal hub is enabled (it can be disabled in
+ * settings).
+ */
+ val communalEnabledState: StateFlow<Boolean>
+
/** Whether the communal hub is showing. */
val isCommunalHubShowing: Flow<Boolean>
@@ -72,13 +87,36 @@
class CommunalRepositoryImpl
@Inject
constructor(
+ @Application private val applicationScope: CoroutineScope,
@Background backgroundScope: CoroutineScope,
+ @Background private val backgroundDispatcher: CoroutineDispatcher,
private val featureFlagsClassic: FeatureFlagsClassic,
sceneContainerFlags: SceneContainerFlags,
sceneContainerRepository: SceneContainerRepository,
+ userRepository: UserRepository,
+ private val secureSettings: SecureSettings
) : CommunalRepository {
+
+ private val communalEnabledSettingState: Flow<Boolean> =
+ userRepository.selectedUserInfo
+ .flatMapLatest { userInfo -> observeSettings(userInfo.id) }
+ .shareIn(scope = applicationScope, started = SharingStarted.WhileSubscribed())
+
+ override val communalEnabledState: StateFlow<Boolean> =
+ if (featureFlagsClassic.isEnabled(Flags.COMMUNAL_SERVICE_ENABLED) && communalHub()) {
+ communalEnabledSettingState
+ .filterNotNull()
+ .stateIn(
+ scope = applicationScope,
+ started = SharingStarted.Eagerly,
+ initialValue = true
+ )
+ } else {
+ MutableStateFlow(false)
+ }
+
override val isCommunalEnabled: Boolean
- get() = featureFlagsClassic.isEnabled(Flags.COMMUNAL_SERVICE_ENABLED) && communalHub()
+ get() = communalEnabledState.value
private val _desiredScene: MutableStateFlow<CommunalSceneKey> =
MutableStateFlow(CommunalSceneKey.DEFAULT)
@@ -115,4 +153,26 @@
} else {
desiredScene.map { sceneKey -> sceneKey == CommunalSceneKey.Communal }
}
+
+ private fun observeSettings(userId: Int): Flow<Boolean> =
+ secureSettings
+ .observerFlow(
+ userId = userId,
+ names =
+ arrayOf(
+ GLANCEABLE_HUB_ENABLED,
+ )
+ )
+ // Force an update
+ .onStart { emit(Unit) }
+ .map { readFromSettings(userId) }
+
+ private suspend fun readFromSettings(userId: Int): Boolean =
+ withContext(backgroundDispatcher) {
+ secureSettings.getIntForUser(GLANCEABLE_HUB_ENABLED, 1, userId) == 1
+ }
+
+ companion object {
+ private const val GLANCEABLE_HUB_ENABLED = "glanceable_hub_enabled"
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepository.kt b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepository.kt
index 3287ed4..f36547b 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepository.kt
@@ -27,21 +27,17 @@
import com.android.systemui.communal.widgets.CommunalAppWidgetHost
import com.android.systemui.communal.widgets.WidgetConfigurator
import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.log.LogBuffer
import com.android.systemui.log.core.Logger
import com.android.systemui.log.dagger.CommunalLog
+import com.android.systemui.util.kotlin.getValue
import java.util.Optional
import javax.inject.Inject
import kotlin.coroutines.cancellation.CancellationException
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.flatMapLatest
-import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
@@ -67,18 +63,15 @@
* @param widgetIdToPriorityMap mapping of the widget ids to the priority of the widget.
*/
fun updateWidgetOrder(widgetIdToPriorityMap: Map<Int, Int>) {}
-
- /** Update whether the app widget host should be active. */
- fun updateAppWidgetHostActive(active: Boolean)
}
@SysUISingleton
class CommunalWidgetRepositoryImpl
@Inject
constructor(
- private val appWidgetManager: Optional<AppWidgetManager>,
+ appWidgetManagerOptional: Optional<AppWidgetManager>,
private val appWidgetHost: CommunalAppWidgetHost,
- @Application private val applicationScope: CoroutineScope,
+ @Background private val bgScope: CoroutineScope,
@Background private val bgDispatcher: CoroutineDispatcher,
private val communalWidgetHost: CommunalWidgetHost,
private val communalWidgetDao: CommunalWidgetDao,
@@ -90,41 +83,22 @@
private val logger = Logger(logBuffer, TAG)
- override fun updateAppWidgetHostActive(active: Boolean) {
- if (active == isHostActive.value) {
- return
- }
+ private val appWidgetManager by appWidgetManagerOptional
- if (active) {
- appWidgetHost.startListening()
- } else {
- appWidgetHost.stopListening()
- }
- isHostActive.value = active
- }
-
- private val isHostActive = MutableStateFlow(false)
-
- @OptIn(ExperimentalCoroutinesApi::class)
override val communalWidgets: Flow<List<CommunalWidgetContentModel>> =
- isHostActive.flatMapLatest { isHostActive ->
- if (!isHostActive || !appWidgetManager.isPresent) {
- return@flatMapLatest flowOf(emptyList())
- }
- communalWidgetDao
- .getWidgets()
- .map { it.map(::mapToContentModel) }
- // As this reads from a database and triggers IPCs to AppWidgetManager,
- // it should be executed in the background.
- .flowOn(bgDispatcher)
- }
+ communalWidgetDao
+ .getWidgets()
+ .map { it.mapNotNull(::mapToContentModel) }
+ // As this reads from a database and triggers IPCs to AppWidgetManager,
+ // it should be executed in the background.
+ .flowOn(bgDispatcher)
override fun addWidget(
provider: ComponentName,
priority: Int,
configurator: WidgetConfigurator?
) {
- applicationScope.launch(bgDispatcher) {
+ bgScope.launch {
val id = communalWidgetHost.allocateIdAndBindWidget(provider)
if (id == null) {
logger.e("Failed to allocate widget id to ${provider.flattenToString()}")
@@ -170,7 +144,7 @@
}
override fun deleteWidget(widgetId: Int) {
- applicationScope.launch(bgDispatcher) {
+ bgScope.launch {
communalWidgetDao.deleteWidgetById(widgetId)
appWidgetHost.deleteAppWidgetId(widgetId)
logger.i("Deleted widget with id $widgetId.")
@@ -178,7 +152,7 @@
}
override fun updateWidgetOrder(widgetIdToPriorityMap: Map<Int, Int>) {
- applicationScope.launch(bgDispatcher) {
+ bgScope.launch {
communalWidgetDao.updateWidgetOrder(widgetIdToPriorityMap)
logger.i({ "Updated the order of widget list with ids: $str1." }) {
str1 = widgetIdToPriorityMap.toString()
@@ -189,11 +163,12 @@
@WorkerThread
private fun mapToContentModel(
entry: Map.Entry<CommunalItemRank, CommunalWidgetItem>
- ): CommunalWidgetContentModel {
+ ): CommunalWidgetContentModel? {
val (_, widgetId) = entry.value
+ val providerInfo = appWidgetManager?.getAppWidgetInfo(widgetId) ?: return null
return CommunalWidgetContentModel(
appWidgetId = widgetId,
- providerInfo = appWidgetManager.get().getAppWidgetInfo(widgetId),
+ providerInfo = providerInfo,
priority = entry.key.rank,
)
}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt
index 92d01db..28adb77 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt
@@ -36,18 +36,23 @@
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
import com.android.systemui.smartspace.data.repository.SmartspaceRepository
+import com.android.systemui.user.data.repository.UserRepository
+import com.android.systemui.util.kotlin.BooleanFlowOperators.and
+import com.android.systemui.util.kotlin.BooleanFlowOperators.not
+import com.android.systemui.util.kotlin.BooleanFlowOperators.or
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.stateIn
/** Encapsulates business-logic related to communal mode. */
@@ -62,23 +67,39 @@
private val communalPrefsRepository: CommunalPrefsRepository,
mediaRepository: CommunalMediaRepository,
smartspaceRepository: SmartspaceRepository,
+ userRepository: UserRepository,
keyguardInteractor: KeyguardInteractor,
private val appWidgetHost: CommunalAppWidgetHost,
private val editWidgetsActivityStarter: EditWidgetsActivityStarter
) {
+ private val _editModeOpen = MutableStateFlow(false)
+
+ /** Whether edit mode is currently open. */
+ val editModeOpen: StateFlow<Boolean> = _editModeOpen.asStateFlow()
/** Whether communal features are enabled. */
val isCommunalEnabled: Boolean
get() = communalRepository.isCommunalEnabled
+ /** A {@link StateFlow} that tracks whether communal features are enabled. */
+ val communalEnabledState: StateFlow<Boolean>
+ get() = communalRepository.communalEnabledState
+
/** Whether communal features are enabled and available. */
val isCommunalAvailable: StateFlow<Boolean> =
flowOf(isCommunalEnabled)
.flatMapLatest { enabled ->
- if (enabled) keyguardInteractor.isEncryptedOrLockdown.map { !it } else flowOf(false)
+ if (enabled) {
+ val isMainUser = userRepository.selectedUserInfo.map { it.isMain }
+ and(
+ isMainUser,
+ not(keyguardInteractor.isEncryptedOrLockdown),
+ or(keyguardInteractor.isKeyguardVisible, keyguardInteractor.isDreaming),
+ )
+ } else {
+ flowOf(false)
+ }
}
- .distinctUntilChanged()
- .onEach { available -> widgetRepository.updateAppWidgetHostActive(available) }
.stateIn(
scope = applicationScope,
started = SharingStarted.WhileSubscribed(),
@@ -145,13 +166,15 @@
it is ObservableCommunalTransitionState.Idle && it.scene == CommunalSceneKey.Communal
}
- val isKeyguardVisible: Flow<Boolean> = keyguardInteractor.isKeyguardVisible
-
/** Callback received whenever the [SceneTransitionLayout] finishes a scene transition. */
fun onSceneChanged(newScene: CommunalSceneKey) {
communalRepository.setDesiredScene(newScene)
}
+ fun setEditModeOpen(isOpen: Boolean) {
+ _editModeOpen.value = isOpen
+ }
+
/** Show the widget editor Activity. */
fun showWidgetEditor() {
editWidgetsActivityStarter.startActivity()
@@ -263,6 +286,12 @@
companion object {
/**
+ * The user activity timeout which should be used when the communal hub is opened. A value
+ * of -1 means that the user's chosen screen timeout will be used instead.
+ */
+ const val AWAKE_INTERVAL_MS = -1
+
+ /**
* Calculates the content size dynamically based on the total number of contents of that
* type.
*
diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalTutorialInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalTutorialInteractor.kt
index 5ca89f2..309c84e 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalTutorialInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalTutorialInteractor.kt
@@ -27,12 +27,15 @@
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
/** Encapsulates business-logic related to communal tutorial state. */
@@ -45,17 +48,24 @@
private val communalTutorialRepository: CommunalTutorialRepository,
keyguardInteractor: KeyguardInteractor,
private val communalRepository: CommunalRepository,
+ communalInteractor: CommunalInteractor,
) {
/** An observable for whether the tutorial is available. */
- val isTutorialAvailable: Flow<Boolean> =
+ val isTutorialAvailable: StateFlow<Boolean> =
combine(
+ communalInteractor.isCommunalAvailable,
keyguardInteractor.isKeyguardVisible,
communalTutorialRepository.tutorialSettingState,
- ) { isKeyguardVisible, tutorialSettingState ->
- isKeyguardVisible &&
+ ) { isCommunalAvailable, isKeyguardVisible, tutorialSettingState ->
+ isCommunalAvailable &&
+ isKeyguardVisible &&
tutorialSettingState != Settings.Secure.HUB_MODE_TUTORIAL_COMPLETED
}
- .distinctUntilChanged()
+ .stateIn(
+ scope = scope,
+ started = SharingStarted.WhileSubscribed(),
+ initialValue = false,
+ )
/**
* A flow of the new tutorial state after transitioning. The new state will be calculated based
diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/binder/CommunalTutorialIndicatorViewBinder.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/binder/CommunalTutorialIndicatorViewBinder.kt
index 4dfc371..0120b5c 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/ui/binder/CommunalTutorialIndicatorViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/ui/binder/CommunalTutorialIndicatorViewBinder.kt
@@ -18,7 +18,6 @@
package com.android.systemui.communal.ui.binder
import android.widget.TextView
-import androidx.core.view.isGone
import androidx.core.view.isVisible
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.repeatOnLifecycle
@@ -32,16 +31,14 @@
fun bind(
view: TextView,
viewModel: CommunalTutorialIndicatorViewModel,
+ isPreviewMode: Boolean = false,
): DisposableHandle {
val disposableHandle =
view.repeatWhenAttached {
repeatOnLifecycle(Lifecycle.State.STARTED) {
launch {
- viewModel.showIndicator.collect { isVisible ->
- updateView(
- view = view,
- isIndicatorVisible = isVisible,
- )
+ viewModel.showIndicator(isPreviewMode).collect { showIndicator ->
+ view.isVisible = showIndicator
}
}
@@ -51,18 +48,4 @@
return disposableHandle
}
-
- private fun updateView(
- isIndicatorVisible: Boolean,
- view: TextView,
- ) {
- if (!isIndicatorVisible) {
- view.isGone = true
- return
- }
-
- if (!view.isVisible) {
- view.isVisible = true
- }
- }
}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/view/layout/sections/CommunalTutorialIndicatorSection.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/view/layout/sections/CommunalTutorialIndicatorSection.kt
index 027cc96..2d9dd50 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/ui/view/layout/sections/CommunalTutorialIndicatorSection.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/ui/view/layout/sections/CommunalTutorialIndicatorSection.kt
@@ -120,6 +120,7 @@
ConstraintSet.PARENT_ID,
ConstraintSet.BOTTOM
)
+ setVisibility(tutorialIndicatorId, View.GONE)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt
index acc7981..1e64d3f 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt
@@ -35,8 +35,6 @@
) {
val isCommunalAvailable: StateFlow<Boolean> = communalInteractor.isCommunalAvailable
- val isKeyguardVisible: Flow<Boolean> = communalInteractor.isKeyguardVisible
-
val currentScene: StateFlow<CommunalSceneKey> = communalInteractor.desiredScene
/** Whether widgets are currently being re-ordered. */
diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt
index 237a0c0..4b98f1a 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt
@@ -75,4 +75,7 @@
_reorderingWidgets.value = false
uiEventLogger.log(CommunalUiEvent.COMMUNAL_HUB_REORDER_WIDGET_CANCEL)
}
+
+ /** Sets whether edit mode is currently open */
+ fun setEditModeOpen(isOpen: Boolean) = communalInteractor.setEditModeOpen(isOpen)
}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalTutorialIndicatorViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalTutorialIndicatorViewModel.kt
index 274e61a..63a4972 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalTutorialIndicatorViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalTutorialIndicatorViewModel.kt
@@ -20,17 +20,30 @@
import com.android.systemui.keyguard.domain.interactor.KeyguardBottomAreaInteractor
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.distinctUntilChanged
/** View model for communal tutorial indicator on keyguard */
class CommunalTutorialIndicatorViewModel
@Inject
constructor(
- communalTutorialInteractor: CommunalTutorialInteractor,
+ private val communalTutorialInteractor: CommunalTutorialInteractor,
bottomAreaInteractor: KeyguardBottomAreaInteractor,
) {
- /** An observable for whether the tutorial indicator view should be visible. */
- val showIndicator: Flow<Boolean> = communalTutorialInteractor.isTutorialAvailable
+ /**
+ * An observable for whether the tutorial indicator view should be visible.
+ *
+ * @param isPreviewMode Whether for preview keyguard mode in wallpaper settings.
+ */
+ fun showIndicator(isPreviewMode: Boolean): StateFlow<Boolean> {
+ return if (isPreviewMode) {
+ MutableStateFlow(false).asStateFlow()
+ } else {
+ communalTutorialInteractor.isTutorialAvailable
+ }
+ }
/** An observable for the alpha level for the tutorial indicator. */
val alpha: Flow<Float> = bottomAreaInteractor.alpha.distinctUntilChanged()
diff --git a/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartable.kt b/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartable.kt
new file mode 100644
index 0000000..586df32
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartable.kt
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.communal.widgets
+
+import com.android.systemui.CoreStartable
+import com.android.systemui.communal.domain.interactor.CommunalInteractor
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.util.kotlin.BooleanFlowOperators.or
+import com.android.systemui.util.kotlin.pairwise
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.withContext
+
+@SysUISingleton
+class CommunalAppWidgetHostStartable
+@Inject
+constructor(
+ private val appWidgetHost: CommunalAppWidgetHost,
+ private val communalInteractor: CommunalInteractor,
+ @Background private val bgScope: CoroutineScope,
+ @Main private val uiDispatcher: CoroutineDispatcher
+) : CoreStartable {
+ override fun start() {
+ or(communalInteractor.isCommunalAvailable, communalInteractor.editModeOpen)
+ // Only trigger updates on state changes, ignoring the initial false value.
+ .pairwise(false)
+ .filter { (previous, new) -> previous != new }
+ .onEach { (_, shouldListen) -> updateAppWidgetHostActive(shouldListen) }
+ .launchIn(bgScope)
+ }
+
+ private suspend fun updateAppWidgetHostActive(active: Boolean) =
+ // Always ensure this is called on the main/ui thread.
+ withContext(uiDispatcher) {
+ if (active) {
+ appWidgetHost.startListening()
+ } else {
+ appWidgetHost.stopListening()
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt b/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt
index c7a14f9..a257543 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt
@@ -86,6 +86,8 @@
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
+ communalViewModel.setEditModeOpen(true)
+
val windowInsetsController = window.decorView.windowInsetsController
windowInsetsController?.hide(WindowInsets.Type.systemBars())
window.setDecorFitsSystemWindows(false)
@@ -138,13 +140,16 @@
override fun onStart() {
super.onStart()
-
uiEventLogger.log(CommunalUiEvent.COMMUNAL_HUB_EDIT_MODE_SHOWN)
}
override fun onStop() {
super.onStop()
-
uiEventLogger.log(CommunalUiEvent.COMMUNAL_HUB_EDIT_MODE_GONE)
}
+
+ override fun onDestroy() {
+ super.onDestroy()
+ communalViewModel.setEditModeOpen(false)
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/compose/BaseComposeFacade.kt b/packages/SystemUI/src/com/android/systemui/compose/BaseComposeFacade.kt
index 641064b..947cb02 100644
--- a/packages/SystemUI/src/com/android/systemui/compose/BaseComposeFacade.kt
+++ b/packages/SystemUI/src/com/android/systemui/compose/BaseComposeFacade.kt
@@ -17,6 +17,7 @@
package com.android.systemui.compose
+import android.app.Dialog
import android.content.Context
import android.view.View
import android.view.WindowInsets
@@ -26,11 +27,14 @@
import com.android.systemui.bouncer.ui.viewmodel.BouncerViewModel
import com.android.systemui.communal.ui.viewmodel.BaseCommunalViewModel
import com.android.systemui.communal.widgets.WidgetConfigurator
+import com.android.systemui.keyboard.stickykeys.ui.viewmodel.StickyKeysIndicatorViewModel
import com.android.systemui.people.ui.viewmodel.PeopleViewModel
import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel
import com.android.systemui.scene.shared.model.Scene
import com.android.systemui.scene.shared.model.SceneKey
import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModel
+import com.android.systemui.statusbar.phone.SystemUIDialogFactory
+import com.android.systemui.volume.panel.ui.viewmodel.VolumePanelViewModel
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.StateFlow
@@ -70,6 +74,12 @@
onEditDone: () -> Unit,
)
+ fun setVolumePanelActivityContent(
+ activity: ComponentActivity,
+ viewModel: VolumePanelViewModel,
+ onDismissAnimationFinished: () -> Unit,
+ )
+
/** Create a [View] to represent [viewModel] on screen. */
fun createFooterActionsView(
context: Context,
@@ -86,6 +96,12 @@
sceneByKey: Map<SceneKey, Scene>,
): View
+ /** Creates sticky key dialog presenting provided [viewModel] */
+ fun createStickyKeysDialog(
+ dialogFactory: SystemUIDialogFactory,
+ viewModel: StickyKeysIndicatorViewModel
+ ): Dialog
+
/** Create a [View] to represent [viewModel] on screen. */
fun createCommunalView(
context: Context,
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt
index 8d82b55..95233f7 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt
@@ -25,6 +25,7 @@
import com.android.systemui.biometrics.BiometricNotificationService
import com.android.systemui.clipboardoverlay.ClipboardListener
import com.android.systemui.communal.log.CommunalLoggerStartable
+import com.android.systemui.communal.widgets.CommunalAppWidgetHostStartable
import com.android.systemui.controls.dagger.StartControlsStartableModule
import com.android.systemui.dagger.qualifiers.PerUser
import com.android.systemui.dreams.AssistantAttentionMonitor
@@ -324,4 +325,11 @@
@IntoMap
@ClassKey(CommunalLoggerStartable::class)
abstract fun bindCommunalLoggerStartable(impl: CommunalLoggerStartable): CoreStartable
+
+ @Binds
+ @IntoMap
+ @ClassKey(CommunalAppWidgetHostStartable::class)
+ abstract fun bindCommunalAppWidgetHostStartable(
+ impl: CommunalAppWidgetHostStartable
+ ): CoreStartable
}
diff --git a/packages/SystemUI/src/com/android/systemui/doze/dagger/DozeModule.java b/packages/SystemUI/src/com/android/systemui/doze/dagger/DozeModule.java
index db0c3c6..0fd6887 100644
--- a/packages/SystemUI/src/com/android/systemui/doze/dagger/DozeModule.java
+++ b/packages/SystemUI/src/com/android/systemui/doze/dagger/DozeModule.java
@@ -18,10 +18,7 @@
import android.content.Context;
import android.hardware.Sensor;
-import android.os.Handler;
-import com.android.systemui.res.R;
-import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.dagger.qualifiers.UiBackground;
import com.android.systemui.doze.DozeAuthRemover;
import com.android.systemui.doze.DozeBrightnessHostForwarder;
@@ -40,6 +37,7 @@
import com.android.systemui.doze.DozeTriggers;
import com.android.systemui.doze.DozeUi;
import com.android.systemui.doze.DozeWallpaperState;
+import com.android.systemui.res.R;
import com.android.systemui.statusbar.phone.DozeParameters;
import com.android.systemui.statusbar.policy.DevicePostureController;
import com.android.systemui.util.sensors.AsyncSensorManager;
@@ -75,9 +73,8 @@
@Provides
@DozeScope
- static WakeLock providesDozeWakeLock(DelayedWakeLock.Builder delayedWakeLockBuilder,
- @Main Handler handler) {
- return delayedWakeLockBuilder.setHandler(handler).setTag("Doze").build();
+ static WakeLock providesDozeWakeLock(DelayedWakeLock.Factory delayedWakeLockFactory) {
+ return delayedWakeLockFactory.create("Doze");
}
@Provides
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/touch/CommunalTouchHandler.java b/packages/SystemUI/src/com/android/systemui/dreams/touch/CommunalTouchHandler.java
new file mode 100644
index 0000000..c9b56a2
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/dreams/touch/CommunalTouchHandler.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.dreams.touch;
+
+import static com.android.systemui.dreams.touch.dagger.ShadeModule.COMMUNAL_GESTURE_INITIATION_WIDTH;
+
+import android.graphics.Rect;
+import android.graphics.Region;
+import android.view.GestureDetector;
+import android.view.MotionEvent;
+
+import com.android.systemui.statusbar.NotificationShadeWindowController;
+import com.android.systemui.statusbar.phone.CentralSurfaces;
+
+import java.util.Optional;
+
+import javax.inject.Inject;
+import javax.inject.Named;
+
+/** {@link DreamTouchHandler} responsible for handling touches to open communal hub. **/
+public class CommunalTouchHandler implements DreamTouchHandler {
+ private final int mInitiationWidth;
+ private final NotificationShadeWindowController mNotificationShadeWindowController;
+ private final Optional<CentralSurfaces> mCentralSurfaces;
+
+ @Inject
+ public CommunalTouchHandler(
+ Optional<CentralSurfaces> centralSurfaces,
+ NotificationShadeWindowController notificationShadeWindowController,
+ @Named(COMMUNAL_GESTURE_INITIATION_WIDTH) int initiationWidth) {
+ mInitiationWidth = initiationWidth;
+ mCentralSurfaces = centralSurfaces;
+ mNotificationShadeWindowController = notificationShadeWindowController;
+ }
+
+ @Override
+ public void onSessionStart(TouchSession session) {
+ mCentralSurfaces.ifPresent(surfaces -> handleSessionStart(surfaces, session));
+ }
+
+ @Override
+ public void getTouchInitiationRegion(Rect bounds, Region region) {
+ final Rect outBounds = new Rect(bounds);
+ outBounds.inset(outBounds.width() - mInitiationWidth, 0, 0, 0);
+ region.op(outBounds, Region.Op.UNION);
+ }
+
+ private void handleSessionStart(CentralSurfaces surfaces, TouchSession session) {
+ // Force the notification shade window open (otherwise the hub won't show while swiping).
+ mNotificationShadeWindowController.setForcePluginOpen(true, this);
+
+ session.registerInputListener(ev -> {
+ surfaces.handleDreamTouch((MotionEvent) ev);
+ if (ev != null && ((MotionEvent) ev).getAction() == MotionEvent.ACTION_UP) {
+ var unused = session.pop();
+ }
+ });
+
+ session.registerGestureListener(new GestureDetector.SimpleOnGestureListener() {
+ @Override
+ public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX,
+ float distanceY) {
+ return true;
+ }
+
+ @Override
+ public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
+ float velocityY) {
+ return true;
+ }
+ });
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/touch/dagger/ShadeModule.java b/packages/SystemUI/src/com/android/systemui/dreams/touch/dagger/ShadeModule.java
index 94fe4bd..0f08d37 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/touch/dagger/ShadeModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/touch/dagger/ShadeModule.java
@@ -18,11 +18,13 @@
import android.content.res.Resources;
-import com.android.systemui.res.R;
import com.android.systemui.dagger.qualifiers.Main;
+import com.android.systemui.dreams.touch.CommunalTouchHandler;
import com.android.systemui.dreams.touch.DreamTouchHandler;
import com.android.systemui.dreams.touch.ShadeTouchHandler;
+import com.android.systemui.res.R;
+import dagger.Binds;
import dagger.Module;
import dagger.Provides;
import dagger.multibindings.IntoSet;
@@ -33,7 +35,7 @@
* Dependencies for swipe down to notification over dream.
*/
@Module
-public class ShadeModule {
+public abstract class ShadeModule {
/**
* The height, defined in pixels, of the gesture initiation region at the top of the screen for
* swiping down notifications.
@@ -41,15 +43,22 @@
public static final String NOTIFICATION_SHADE_GESTURE_INITIATION_HEIGHT =
"notification_shade_gesture_initiation_height";
+ /** Width of swipe gesture edge to show communal hub. */
+ public static final String COMMUNAL_GESTURE_INITIATION_WIDTH =
+ "communal_gesture_initiation_width";
+
/**
* Provides {@link ShadeTouchHandler} to handle notification swipe down over dream.
*/
- @Provides
+ @Binds
@IntoSet
- public static DreamTouchHandler providesNotificationShadeTouchHandler(
- ShadeTouchHandler touchHandler) {
- return touchHandler;
- }
+ public abstract DreamTouchHandler providesNotificationShadeTouchHandler(
+ ShadeTouchHandler touchHandler);
+
+ /** Provides {@link CommunalTouchHandler}. */
+ @Binds
+ @IntoSet
+ public abstract DreamTouchHandler bindCommunalTouchHandler(CommunalTouchHandler touchHandler);
/**
* Provides the height of the gesture area for notification swipe down.
@@ -59,4 +68,13 @@
public static int providesNotificationShadeGestureRegionHeight(@Main Resources resources) {
return resources.getDimensionPixelSize(R.dimen.dream_overlay_status_bar_height);
}
+
+ /**
+ * Provides the width of the gesture area for swiping open communal hub.
+ */
+ @Provides
+ @Named(COMMUNAL_GESTURE_INITIATION_WIDTH)
+ public static int providesCommunalGestureInitiationWidth(@Main Resources resources) {
+ return resources.getDimensionPixelSize(R.dimen.communal_gesture_initiation_width);
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
index 846736c..d2883cc 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
@@ -44,11 +44,11 @@
// 100 - notification
// TODO(b/297792660): Tracking Bug
@JvmField val UNCLEARED_TRANSIENT_HUN_FIX =
- unreleasedFlag("uncleared_transient_hun_fix", teamfood = true)
+ releasedFlag("uncleared_transient_hun_fix")
// TODO(b/298308067): Tracking Bug
@JvmField val SWIPE_UNCLEARED_TRANSIENT_VIEW_FIX =
- unreleasedFlag("swipe_uncleared_transient_view_fix", teamfood = true)
+ releasedFlag("swipe_uncleared_transient_view_fix")
// TODO(b/254512751): Tracking Bug
val NOTIFICATION_PIPELINE_DEVELOPER_LOGGING =
@@ -102,12 +102,6 @@
default = true
)
- /** Only notify group expansion listeners when a change happens. */
- // TODO(b/292213543): Tracking Bug
- @JvmField
- val NOTIFICATION_GROUP_EXPANSION_CHANGE =
- releasedFlag("notification_group_expansion_change")
-
// TODO(b/301955929)
@JvmField
val NOTIF_LS_BACKGROUND_THREAD =
@@ -574,9 +568,6 @@
@JvmField
val ENABLE_NEW_PRIVACY_DIALOG = releasedFlag("enable_new_privacy_dialog")
- // TODO(b/289573946): Tracking Bug
- @JvmField val PRECOMPUTED_TEXT = releasedFlag("precomputed_text")
-
// TODO(b/302087895): Tracking Bug
@JvmField val CALL_LAYOUT_ASYNC_SET_DATA =
unreleasedFlag("call_layout_async_set_data", teamfood = true)
diff --git a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java
index 3de9e68..a95ddb5 100644
--- a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java
+++ b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java
@@ -2437,6 +2437,7 @@
return true;
}
});
+ mGlobalActionsLayout.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
mGlobalActionsLayout.setRotationListener(this::onRotate);
mGlobalActionsLayout.setAdapter(mAdapter);
mContainer = findViewById(com.android.systemui.res.R.id.global_actions_container);
diff --git a/packages/SystemUI/src/com/android/systemui/haptics/slider/SeekableSliderHapticPlugin.kt b/packages/SystemUI/src/com/android/systemui/haptics/slider/SeekableSliderHapticPlugin.kt
new file mode 100644
index 0000000..58fb6a9
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/haptics/slider/SeekableSliderHapticPlugin.kt
@@ -0,0 +1,171 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.haptics.slider
+
+import android.view.MotionEvent
+import android.view.VelocityTracker
+import android.widget.SeekBar
+import androidx.annotation.VisibleForTesting
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.statusbar.VibratorHelper
+import com.android.systemui.util.time.SystemClock
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+
+/**
+ * A plugin added to a manager of a [android.widget.SeekBar] that adds dynamic haptic feedback.
+ *
+ * A [SeekableSliderEventProducer] is used as the producer of slider events, a
+ * [SliderHapticFeedbackProvider] is used as the listener of slider states to play haptic feedback
+ * depending on the state, and a [SeekableSliderTracker] is used as the state machine handler that
+ * tracks and manipulates the slider state.
+ */
+class SeekableSliderHapticPlugin
+@JvmOverloads
+constructor(
+ vibratorHelper: VibratorHelper,
+ systemClock: SystemClock,
+ @Main private val mainDispatcher: CoroutineDispatcher,
+ @Application private val applicationScope: CoroutineScope,
+ sliderHapticFeedbackConfig: SliderHapticFeedbackConfig = SliderHapticFeedbackConfig(),
+ sliderTrackerConfig: SeekableSliderTrackerConfig = SeekableSliderTrackerConfig(),
+) {
+
+ private val velocityTracker = VelocityTracker.obtain()
+
+ private val sliderEventProducer = SeekableSliderEventProducer()
+
+ private val sliderHapticFeedbackProvider =
+ SliderHapticFeedbackProvider(
+ vibratorHelper,
+ velocityTracker,
+ sliderHapticFeedbackConfig,
+ systemClock,
+ )
+
+ private val sliderTracker =
+ SeekableSliderTracker(
+ sliderHapticFeedbackProvider,
+ sliderEventProducer,
+ mainDispatcher,
+ sliderTrackerConfig,
+ )
+
+ val isTracking: Boolean
+ get() = sliderTracker.isTracking
+
+ val trackerState: SliderState
+ get() = sliderTracker.currentState
+
+ /**
+ * A waiting [Job] for a timer that estimates the key-up event when a key-down event is
+ * received.
+ *
+ * This is useful for the cases where the slider is being operated by an external key, but the
+ * release of the key is not easily accessible (e.g., the volume keys)
+ */
+ private var keyUpJob: Job? = null
+
+ @VisibleForTesting
+ val isKeyUpTimerWaiting: Boolean
+ get() = keyUpJob != null && keyUpJob?.isActive == true
+
+ /**
+ * Start the plugin.
+ *
+ * This starts the tracking of slider states, events and triggering of haptic feedback.
+ */
+ fun start() {
+ if (!isTracking) {
+ sliderTracker.startTracking()
+ }
+ }
+
+ /**
+ * Stop the plugin
+ *
+ * This stops the tracking of slider states, events and triggers of haptic feedback.
+ */
+ fun stop() = sliderTracker.stopTracking()
+
+ /** React to a touch event */
+ fun onTouchEvent(event: MotionEvent?) {
+ when (event?.actionMasked) {
+ MotionEvent.ACTION_UP,
+ MotionEvent.ACTION_CANCEL -> velocityTracker.clear()
+ MotionEvent.ACTION_DOWN,
+ MotionEvent.ACTION_MOVE -> velocityTracker.addMovement(event)
+ }
+ }
+
+ /** onStartTrackingTouch event from the slider's [android.widget.SeekBar] */
+ fun onStartTrackingTouch(seekBar: SeekBar) {
+ if (isTracking) {
+ sliderEventProducer.onStartTrackingTouch(seekBar)
+ }
+ }
+
+ /** onProgressChanged event from the slider's [android.widget.SeekBar] */
+ fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
+ if (isTracking) {
+ sliderEventProducer.onProgressChanged(seekBar, progress, fromUser)
+ }
+ }
+
+ /** onStopTrackingTouch event from the slider's [android.widget.SeekBar] */
+ fun onStopTrackingTouch(seekBar: SeekBar) {
+ if (isTracking) {
+ sliderEventProducer.onStopTrackingTouch(seekBar)
+ }
+ }
+
+ /** onArrowUp event recorded */
+ fun onArrowUp() {
+ if (isTracking) {
+ sliderEventProducer.onArrowUp()
+ }
+ }
+
+ /**
+ * An external key was pressed (e.g., a volume key).
+ *
+ * This event is used to estimate the key-up event based on by running a timer as a waiting
+ * coroutine in the [keyUpTimerScope]. A key-up event in a slider corresponds to an onArrowUp
+ * event. Therefore, [onArrowUp] must be called after the timeout.
+ */
+ fun onKeyDown() {
+ if (!isTracking) return
+
+ if (isKeyUpTimerWaiting) {
+ // Cancel the ongoing wait
+ keyUpJob?.cancel()
+ }
+ keyUpJob =
+ applicationScope.launch {
+ delay(KEY_UP_TIMEOUT)
+ onArrowUp()
+ }
+ }
+
+ companion object {
+ const val KEY_UP_TIMEOUT = 100L
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/PhysicalKeyboardCoreStartable.kt b/packages/SystemUI/src/com/android/systemui/keyboard/PhysicalKeyboardCoreStartable.kt
index d078688..26e83a3 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/PhysicalKeyboardCoreStartable.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/PhysicalKeyboardCoreStartable.kt
@@ -17,11 +17,14 @@
package com.android.systemui.keyboard
+import com.android.hardware.input.Flags.keyboardA11yStickyKeysFlag
import com.android.systemui.CoreStartable
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.flags.FeatureFlags
import com.android.systemui.flags.Flags
import com.android.systemui.keyboard.backlight.ui.KeyboardBacklightDialogCoordinator
+import com.android.systemui.keyboard.stickykeys.ui.StickyKeysIndicatorCoordinator
+import dagger.Lazy
import javax.inject.Inject
/** A [CoreStartable] that launches components interested in physical keyboard interaction. */
@@ -29,12 +32,16 @@
class PhysicalKeyboardCoreStartable
@Inject
constructor(
- private val keyboardBacklightDialogCoordinator: KeyboardBacklightDialogCoordinator,
+ private val keyboardBacklightDialogCoordinator: Lazy<KeyboardBacklightDialogCoordinator>,
+ private val stickyKeysIndicatorCoordinator: Lazy<StickyKeysIndicatorCoordinator>,
private val featureFlags: FeatureFlags,
) : CoreStartable {
override fun start() {
if (featureFlags.isEnabled(Flags.KEYBOARD_BACKLIGHT_INDICATOR)) {
- keyboardBacklightDialogCoordinator.startListening()
+ keyboardBacklightDialogCoordinator.get().startListening()
+ }
+ if (keyboardA11yStickyKeysFlag()) {
+ stickyKeysIndicatorCoordinator.get().startListening()
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/stickykeys/StickyKeysLogger.kt b/packages/SystemUI/src/com/android/systemui/keyboard/stickykeys/StickyKeysLogger.kt
index 37034f6..4ed8120 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/stickykeys/StickyKeysLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/stickykeys/StickyKeysLogger.kt
@@ -26,11 +26,20 @@
private const val TAG = "stickyKeys"
class StickyKeysLogger @Inject constructor(@KeyboardLog private val buffer: LogBuffer) {
- fun logNewStickyKeysReceived(linkedHashMap: Map<ModifierKey, Locked>) {
+ fun logNewStickyKeysReceived(stickyKeys: Map<ModifierKey, Locked>) {
buffer.log(
TAG,
LogLevel.VERBOSE,
- { str1 = linkedHashMap.toString() },
+ { str1 = stickyKeys.toString() },
+ { "new sticky keys state received: $str1" }
+ )
+ }
+
+ fun logNewUiState(stickyKeys: Map<ModifierKey, Locked>) {
+ buffer.log(
+ TAG,
+ LogLevel.INFO,
+ { str1 = stickyKeys.toString() },
{ "new sticky keys state received: $str1" }
)
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/stickykeys/ui/StickyKeysIndicatorCoordinator.kt b/packages/SystemUI/src/com/android/systemui/keyboard/stickykeys/ui/StickyKeysIndicatorCoordinator.kt
new file mode 100644
index 0000000..b68551b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/stickykeys/ui/StickyKeysIndicatorCoordinator.kt
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyboard.stickykeys.ui
+
+import android.app.Dialog
+import android.util.Log
+import android.view.Gravity
+import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
+import android.view.Window
+import android.view.WindowManager
+import com.android.systemui.compose.ComposeFacade
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.keyboard.stickykeys.StickyKeysLogger
+import com.android.systemui.keyboard.stickykeys.ui.viewmodel.StickyKeysIndicatorViewModel
+import com.android.systemui.statusbar.phone.SystemUIDialogFactory
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+import javax.inject.Inject
+
+@SysUISingleton
+class StickyKeysIndicatorCoordinator
+@Inject
+constructor(
+ @Application private val applicationScope: CoroutineScope,
+ private val dialogFactory: SystemUIDialogFactory,
+ private val viewModel: StickyKeysIndicatorViewModel,
+ private val stickyKeysLogger: StickyKeysLogger,
+) {
+
+ private var dialog: Dialog? = null
+
+ fun startListening() {
+ // this check needs to be moved to PhysicalKeyboardCoreStartable
+ if (!ComposeFacade.isComposeAvailable()) {
+ Log.e("StickyKeysIndicatorCoordinator", "Compose is required for this UI")
+ return
+ }
+ applicationScope.launch {
+ viewModel.indicatorContent.collect { stickyKeys ->
+ stickyKeysLogger.logNewUiState(stickyKeys)
+ if (stickyKeys.isEmpty()) {
+ dialog?.dismiss()
+ dialog = null
+ } else if (dialog == null) {
+ dialog = ComposeFacade.createStickyKeysDialog(dialogFactory, viewModel).apply {
+ setCanceledOnTouchOutside(false)
+ window?.setAttributes()
+ show()
+ }
+ }
+ }
+ }
+ }
+
+ private fun Window.setAttributes() {
+ setType(WindowManager.LayoutParams.TYPE_STATUS_BAR_SUB_PANEL)
+ addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED)
+ clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)
+ setGravity(Gravity.TOP or Gravity.END)
+ attributes = WindowManager.LayoutParams().apply {
+ copyFrom(attributes)
+ width = WRAP_CONTENT
+ title = "StickyKeysIndicator"
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
index 5cebd96..50caf17 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
@@ -2507,8 +2507,18 @@
String message = "";
switch (msg.what) {
case SHOW:
- message = "SHOW";
- handleShow((Bundle) msg.obj);
+ // There is a potential race condition when SysUI starts up. CentralSurfaces
+ // must invoke #registerCentralSurfaces on this class before any messages can be
+ // processed. If this happens, repost the message with a small delay and try
+ // again.
+ if (mCentralSurfaces == null) {
+ message = "DELAYING SHOW";
+ Message newMsg = mHandler.obtainMessage(SHOW, (Bundle) msg.obj);
+ mHandler.sendMessageDelayed(newMsg, 100);
+ } else {
+ message = "SHOW";
+ handleShow((Bundle) msg.obj);
+ }
break;
case HIDE:
message = "HIDE";
@@ -2595,8 +2605,18 @@
Trace.endSection();
break;
case SYSTEM_READY:
- message = "SYSTEM_READY";
- handleSystemReady();
+ // There is a potential race condition when SysUI starts up. CentralSurfaces
+ // must invoke #registerCentralSurfaces on this class before any messages can be
+ // processed. If this happens, repost the message with a small delay and try
+ // again.
+ if (mCentralSurfaces == null) {
+ message = "DELAYING SYSTEM_READY";
+ Message newMsg = mHandler.obtainMessage(SYSTEM_READY);
+ mHandler.sendMessageDelayed(newMsg, 100);
+ } else {
+ message = "SYSTEM_READY";
+ handleSystemReady();
+ }
break;
}
Log.d(TAG, "KeyguardViewMediator queue processing message: " + message);
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt
index d012d24..1437194 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt
@@ -132,6 +132,9 @@
*/
val isDozing: StateFlow<Boolean>
+ /** Keyguard can be clipped at the top as the shade is dragged */
+ val topClippingBounds: MutableStateFlow<Int?>
+
/**
* Observable for whether the device is dreaming.
*
@@ -326,6 +329,8 @@
private val _clockShouldBeCentered = MutableStateFlow(true)
override val clockShouldBeCentered: Flow<Boolean> = _clockShouldBeCentered.asStateFlow()
+ override val topClippingBounds = MutableStateFlow<Int?>(null)
+
override val isKeyguardShowing: Flow<Boolean> =
conflatedCallbackFlow {
val callback =
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAlternateBouncerTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAlternateBouncerTransitionInteractor.kt
index 7b1466c..a97c152 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAlternateBouncerTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAlternateBouncerTransitionInteractor.kt
@@ -17,14 +17,14 @@
package com.android.systemui.keyguard.domain.interactor
import android.animation.ValueAnimator
+import com.android.systemui.communal.domain.interactor.CommunalInteractor
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.power.domain.interactor.PowerInteractor
-import com.android.systemui.util.kotlin.Utils.Companion.toQuad
-import com.android.systemui.util.kotlin.Utils.Companion.toQuint
+import com.android.systemui.util.kotlin.Utils.Companion.sample
import com.android.systemui.util.kotlin.sample
import com.android.wm.shell.animation.Interpolators
import javax.inject.Inject
@@ -32,7 +32,6 @@
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
-import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
@@ -46,6 +45,7 @@
@Background bgDispatcher: CoroutineDispatcher,
@Main mainDispatcher: CoroutineDispatcher,
private val keyguardInteractor: KeyguardInteractor,
+ private val communalInteractor: CommunalInteractor,
private val powerInteractor: PowerInteractor,
) :
TransitionInteractor(
@@ -57,12 +57,12 @@
override fun start() {
listenForAlternateBouncerToGone()
- listenForAlternateBouncerToLockscreenAodOrDozing()
+ listenForAlternateBouncerToLockscreenHubAodOrDozing()
listenForAlternateBouncerToPrimaryBouncer()
listenForTransitionToCamera(scope, keyguardInteractor)
}
- private fun listenForAlternateBouncerToLockscreenAodOrDozing() {
+ private fun listenForAlternateBouncerToLockscreenHubAodOrDozing() {
scope.launch {
keyguardInteractor.alternateBouncerShowing
// Add a slight delay, as alternateBouncer and primaryBouncer showing event changes
@@ -70,14 +70,11 @@
// happening prematurely.
.onEach { delay(50) }
.sample(
- combine(
- keyguardInteractor.primaryBouncerShowing,
- startedKeyguardTransitionStep,
- powerInteractor.isAwake,
- keyguardInteractor.isAodAvailable,
- ::toQuad
- ),
- ::toQuint
+ keyguardInteractor.primaryBouncerShowing,
+ startedKeyguardTransitionStep,
+ powerInteractor.isAwake,
+ keyguardInteractor.isAodAvailable,
+ communalInteractor.isIdleOnCommunal
)
.collect {
(
@@ -85,7 +82,8 @@
isPrimaryBouncerShowing,
lastStartedTransitionStep,
isAwake,
- isAodAvailable) ->
+ isAodAvailable,
+ isIdleOnCommunal) ->
if (
!isAlternateBouncerShowing &&
!isPrimaryBouncerShowing &&
@@ -99,7 +97,11 @@
KeyguardState.DOZING
}
} else {
- KeyguardState.LOCKSCREEN
+ if (isIdleOnCommunal) {
+ KeyguardState.GLANCEABLE_HUB
+ } else {
+ KeyguardState.LOCKSCREEN
+ }
}
startTransitionTo(to)
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractor.kt
index 3292ea8..9d38be9 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractor.kt
@@ -18,6 +18,7 @@
import android.animation.ValueAnimator
import com.android.app.animation.Interpolators
+import com.android.app.tracing.coroutines.launch
import com.android.systemui.Flags
import com.android.systemui.communal.shared.model.CommunalSceneKey
import com.android.systemui.dagger.SysUISingleton
@@ -39,12 +40,13 @@
@Inject
constructor(
@Background private val scope: CoroutineScope,
+ @Main mainDispatcher: CoroutineDispatcher,
+ @Background bgDispatcher: CoroutineDispatcher,
private val glanceableHubTransitions: GlanceableHubTransitions,
+ private val keyguardInteractor: KeyguardInteractor,
override val transitionRepository: KeyguardTransitionRepository,
transitionInteractor: KeyguardTransitionInteractor,
private val powerInteractor: PowerInteractor,
- @Main mainDispatcher: CoroutineDispatcher,
- @Background bgDispatcher: CoroutineDispatcher,
) :
TransitionInteractor(
fromState = KeyguardState.GLANCEABLE_HUB,
@@ -58,6 +60,10 @@
}
listenForHubToLockscreen()
listenForHubToDozing()
+ listenForHubToPrimaryBouncer()
+ listenForHubToAlternateBouncer()
+ listenForHubToOccluded()
+ listenForHubToGone()
}
override fun getDefaultAnimatorForTransitionsToState(toState: KeyguardState): ValueAnimator {
@@ -75,10 +81,42 @@
glanceableHubTransitions.listenForLockscreenAndHubTransition(
transitionName = "listenForHubToLockscreen",
transitionOwnerName = TAG,
- toScene = CommunalSceneKey.Blank
+ toScene = CommunalSceneKey.Blank,
)
}
+ private fun listenForHubToPrimaryBouncer() {
+ scope.launch("$TAG#listenForHubToPrimaryBouncer") {
+ keyguardInteractor.primaryBouncerShowing
+ .sample(startedKeyguardTransitionStep, ::Pair)
+ .collect { pair ->
+ val (isBouncerShowing, lastStartedTransitionStep) = pair
+ if (
+ isBouncerShowing &&
+ lastStartedTransitionStep.to == KeyguardState.GLANCEABLE_HUB
+ ) {
+ startTransitionTo(KeyguardState.PRIMARY_BOUNCER)
+ }
+ }
+ }
+ }
+
+ private fun listenForHubToAlternateBouncer() {
+ scope.launch("$TAG#listenForHubToAlternateBouncer") {
+ keyguardInteractor.alternateBouncerShowing
+ .sample(startedKeyguardTransitionStep, ::Pair)
+ .collect { pair ->
+ val (isAlternateBouncerShowing, lastStartedTransitionStep) = pair
+ if (
+ isAlternateBouncerShowing &&
+ lastStartedTransitionStep.to == KeyguardState.GLANCEABLE_HUB
+ ) {
+ startTransitionTo(KeyguardState.ALTERNATE_BOUNCER)
+ }
+ }
+ }
+ }
+
private fun listenForHubToDozing() {
scope.launch {
powerInteractor.isAsleep.sample(startedKeyguardTransitionStep, ::Pair).collect {
@@ -93,8 +131,32 @@
}
}
+ private fun listenForHubToOccluded() {
+ scope.launch {
+ keyguardInteractor.isKeyguardOccluded.sample(startedKeyguardState, ::Pair).collect {
+ (isOccluded, keyguardState) ->
+ if (isOccluded && keyguardState == fromState) {
+ startTransitionTo(KeyguardState.OCCLUDED)
+ }
+ }
+ }
+ }
+
+ private fun listenForHubToGone() {
+ scope.launch {
+ keyguardInteractor.isKeyguardGoingAway
+ .sample(startedKeyguardTransitionStep, ::Pair)
+ .collect { (isKeyguardGoingAway, lastStartedStep) ->
+ if (isKeyguardGoingAway && lastStartedStep.to == fromState) {
+ startTransitionTo(KeyguardState.GONE)
+ }
+ }
+ }
+ }
+
companion object {
const val TAG = "FromGlanceableHubTransitionInteractor"
- val DEFAULT_DURATION = 500.milliseconds
+ val DEFAULT_DURATION = 400.milliseconds
+ val TO_LOCKSCREEN_DURATION = DEFAULT_DURATION
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractor.kt
index 742790e..7477624 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractor.kt
@@ -18,6 +18,7 @@
import android.animation.ValueAnimator
import com.android.app.animation.Interpolators
+import com.android.systemui.communal.domain.interactor.CommunalInteractor
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.dagger.qualifiers.Main
@@ -25,6 +26,7 @@
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.keyguard.shared.model.TransitionModeOnCanceled
import com.android.systemui.power.domain.interactor.PowerInteractor
+import com.android.systemui.util.kotlin.Utils.Companion.sample
import com.android.systemui.util.kotlin.Utils.Companion.toTriple
import com.android.systemui.util.kotlin.sample
import javax.inject.Inject
@@ -45,6 +47,7 @@
@Main mainDispatcher: CoroutineDispatcher,
private val keyguardInteractor: KeyguardInteractor,
private val powerInteractor: PowerInteractor,
+ private val communalInteractor: CommunalInteractor,
) :
TransitionInteractor(
fromState = KeyguardState.GONE,
@@ -56,18 +59,27 @@
override fun start() {
listenForGoneToAodOrDozing()
listenForGoneToDreaming()
- listenForGoneToLockscreen()
+ listenForGoneToLockscreenOrHub()
listenForGoneToDreamingLockscreenHosted()
}
// Primarily for when the user chooses to lock down the device
- private fun listenForGoneToLockscreen() {
+ private fun listenForGoneToLockscreenOrHub() {
scope.launch {
keyguardInteractor.isKeyguardShowing
- .sample(startedKeyguardTransitionStep, ::Pair)
- .collect { (isKeyguardShowing, lastStartedStep) ->
+ .sample(
+ startedKeyguardTransitionStep,
+ communalInteractor.isIdleOnCommunal,
+ )
+ .collect { (isKeyguardShowing, lastStartedStep, isIdleOnCommunal) ->
if (isKeyguardShowing && lastStartedStep.to == KeyguardState.GONE) {
- startTransitionTo(KeyguardState.LOCKSCREEN)
+ val to =
+ if (isIdleOnCommunal) {
+ KeyguardState.GLANCEABLE_HUB
+ } else {
+ KeyguardState.LOCKSCREEN
+ }
+ startTransitionTo(to)
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractor.kt
index 40061f4..efb604d 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractor.kt
@@ -18,12 +18,14 @@
import android.animation.ValueAnimator
import com.android.app.animation.Interpolators
+import com.android.systemui.communal.domain.interactor.CommunalInteractor
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.power.domain.interactor.PowerInteractor
+import com.android.systemui.util.kotlin.Utils.Companion.sample
import com.android.systemui.util.kotlin.Utils.Companion.toTriple
import com.android.systemui.util.kotlin.sample
import javax.inject.Inject
@@ -44,6 +46,7 @@
@Main mainDispatcher: CoroutineDispatcher,
private val keyguardInteractor: KeyguardInteractor,
private val powerInteractor: PowerInteractor,
+ private val communalInteractor: CommunalInteractor,
) :
TransitionInteractor(
fromState = KeyguardState.OCCLUDED,
@@ -53,7 +56,7 @@
) {
override fun start() {
- listenForOccludedToLockscreen()
+ listenForOccludedToLockscreenOrHub()
listenForOccludedToDreaming()
listenForOccludedToAodOrDozing()
listenForOccludedToGone()
@@ -86,18 +89,15 @@
}
}
- private fun listenForOccludedToLockscreen() {
+ private fun listenForOccludedToLockscreenOrHub() {
scope.launch {
keyguardInteractor.isKeyguardOccluded
.sample(
- combine(
- keyguardInteractor.isKeyguardShowing,
- startedKeyguardTransitionStep,
- ::Pair
- ),
- ::toTriple
+ keyguardInteractor.isKeyguardShowing,
+ startedKeyguardTransitionStep,
+ communalInteractor.isIdleOnCommunal,
)
- .collect { (isOccluded, isShowing, lastStartedKeyguardState) ->
+ .collect { (isOccluded, isShowing, lastStartedKeyguardState, isIdleOnCommunal) ->
// Occlusion signals come from the framework, and should interrupt any
// existing transition
if (
@@ -105,7 +105,13 @@
isShowing &&
lastStartedKeyguardState.to == KeyguardState.OCCLUDED
) {
- startTransitionTo(KeyguardState.LOCKSCREEN)
+ val to =
+ if (isIdleOnCommunal) {
+ KeyguardState.GLANCEABLE_HUB
+ } else {
+ KeyguardState.LOCKSCREEN
+ }
+ startTransitionTo(to)
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractor.kt
index c62055f..33b6373 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractor.kt
@@ -18,6 +18,7 @@
import android.animation.ValueAnimator
import com.android.keyguard.KeyguardSecurityModel
+import com.android.systemui.communal.domain.interactor.CommunalInteractor
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.dagger.qualifiers.Main
@@ -29,8 +30,8 @@
import com.android.systemui.keyguard.shared.model.TransitionModeOnCanceled
import com.android.systemui.power.domain.interactor.PowerInteractor
import com.android.systemui.user.domain.interactor.SelectedUserInteractor
+import com.android.systemui.util.kotlin.Utils.Companion.sample
import com.android.systemui.util.kotlin.Utils.Companion.toQuad
-import com.android.systemui.util.kotlin.Utils.Companion.toQuint
import com.android.systemui.util.kotlin.Utils.Companion.toTriple
import com.android.systemui.util.kotlin.sample
import com.android.wm.shell.animation.Interpolators
@@ -54,6 +55,7 @@
@Background bgDispatcher: CoroutineDispatcher,
@Main mainDispatcher: CoroutineDispatcher,
private val keyguardInteractor: KeyguardInteractor,
+ private val communalInteractor: CommunalInteractor,
private val flags: FeatureFlags,
private val keyguardSecurityModel: KeyguardSecurityModel,
private val selectedUserInteractor: SelectedUserInteractor,
@@ -69,7 +71,7 @@
override fun start() {
listenForPrimaryBouncerToGone()
listenForPrimaryBouncerToAodOrDozing()
- listenForPrimaryBouncerToLockscreenOrOccluded()
+ listenForPrimaryBouncerToLockscreenHubOrOccluded()
listenForPrimaryBouncerToDreamingLockscreenHosted()
listenForTransitionToCamera(scope, keyguardInteractor)
}
@@ -125,18 +127,15 @@
scope.launch { startTransitionTo(KeyguardState.GONE) }
}
- private fun listenForPrimaryBouncerToLockscreenOrOccluded() {
+ private fun listenForPrimaryBouncerToLockscreenHubOrOccluded() {
scope.launch {
keyguardInteractor.primaryBouncerShowing
.sample(
- combine(
- powerInteractor.isAwake,
- startedKeyguardTransitionStep,
- keyguardInteractor.isKeyguardOccluded,
- keyguardInteractor.isActiveDreamLockscreenHosted,
- ::toQuad
- ),
- ::toQuint
+ powerInteractor.isAwake,
+ startedKeyguardTransitionStep,
+ keyguardInteractor.isKeyguardOccluded,
+ keyguardInteractor.isActiveDreamLockscreenHosted,
+ communalInteractor.isIdleOnCommunal
)
.collect {
(
@@ -144,16 +143,23 @@
isAwake,
lastStartedTransitionStep,
occluded,
- isActiveDreamLockscreenHosted) ->
+ isActiveDreamLockscreenHosted,
+ isIdleOnCommunal) ->
if (
!isBouncerShowing &&
lastStartedTransitionStep.to == KeyguardState.PRIMARY_BOUNCER &&
isAwake &&
!isActiveDreamLockscreenHosted
) {
- startTransitionTo(
- if (occluded) KeyguardState.OCCLUDED else KeyguardState.LOCKSCREEN
- )
+ val toState =
+ if (occluded) {
+ KeyguardState.OCCLUDED
+ } else if (isIdleOnCommunal) {
+ KeyguardState.GLANCEABLE_HUB
+ } else {
+ KeyguardState.LOCKSCREEN
+ }
+ startTransitionTo(toState)
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/GlanceableHubTransitions.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/GlanceableHubTransitions.kt
index cb50839..ca66153 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/GlanceableHubTransitions.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/GlanceableHubTransitions.kt
@@ -23,6 +23,7 @@
import com.android.systemui.communal.domain.interactor.CommunalTransitionProgress
import com.android.systemui.communal.shared.model.CommunalSceneKey
import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.keyguard.shared.model.TransitionInfo
@@ -30,12 +31,15 @@
import com.android.systemui.util.kotlin.sample
import java.util.UUID
import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.flowOn
class GlanceableHubTransitions
@Inject
constructor(
@Application private val scope: CoroutineScope,
+ @Background private val bgDispatcher: CoroutineDispatcher,
private val transitionInteractor: KeyguardTransitionInteractor,
private val transitionRepository: KeyguardTransitionRepository,
private val communalInteractor: CommunalInteractor,
@@ -66,7 +70,10 @@
scope.launch("$transitionOwnerName#$transitionName") {
communalInteractor
.transitionProgressToScene(toScene)
- .sample(transitionInteractor.startedKeyguardTransitionStep, ::Pair)
+ .sample(
+ transitionInteractor.startedKeyguardTransitionStep.flowOn(bgDispatcher),
+ ::Pair
+ )
.collect { pair ->
val (transitionProgress, lastStartedStep) = pair
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt
index 6170356..36bd905 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt
@@ -98,7 +98,7 @@
val dozeAmount: Flow<Float> = repository.linearDozeAmount
/** Whether the system is in doze mode. */
- val isDozing: Flow<Boolean> = repository.isDozing
+ val isDozing: StateFlow<Boolean> = repository.isDozing
/** Receive an event for doze time tick */
val dozeTimeTick: Flow<Long> = repository.dozeTimeTick
@@ -171,6 +171,14 @@
/** Whether the keyguard is going away. */
val isKeyguardGoingAway: Flow<Boolean> = repository.isKeyguardGoingAway
+ /** Keyguard can be clipped at the top as the shade is dragged */
+ val topClippingBounds: Flow<Int?> =
+ combine(configurationInteractor.onAnyConfigurationChange, repository.topClippingBounds) {
+ _,
+ topClippingBounds ->
+ topClippingBounds
+ }
+
/** Last point that [KeyguardRootView] view was tapped */
val lastRootViewTapPosition: Flow<Point?> = repository.lastRootViewTapPosition.asStateFlow()
@@ -328,6 +336,10 @@
repository.keyguardDoneAnimationsFinished()
}
+ fun setTopClippingBounds(top: Int?) {
+ repository.topClippingBounds.value = top
+ }
+
companion object {
private const val TAG = "KeyguardInteractor"
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBlueprintViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBlueprintViewBinder.kt
index 3dd3e07..8d6493f 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBlueprintViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBlueprintViewBinder.kt
@@ -24,6 +24,7 @@
import androidx.constraintlayout.widget.ConstraintSet
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.repeatOnLifecycle
+import com.android.systemui.Flags.keyguardBottomAreaRefactor
import com.android.systemui.keyguard.ui.view.layout.blueprints.transitions.BaseBlueprintTransition
import com.android.systemui.keyguard.ui.viewmodel.KeyguardBlueprintViewModel
import com.android.systemui.lifecycle.repeatWhenAttached
@@ -53,7 +54,8 @@
}
// Apply transition.
- if (prevBluePrint != null && prevBluePrint != blueprint) {
+ if (!keyguardBottomAreaRefactor() && prevBluePrint != null &&
+ prevBluePrint != blueprint) {
TransitionManager.beginDelayedTransition(
constraintLayout,
BaseBlueprintTransition()
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinder.kt
index 400b8bf..3c3ebdf 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinder.kt
@@ -89,6 +89,12 @@
}
}
}
+ launch {
+ if (!migrateClocksToBlueprint()) return@launch
+ viewModel.isAodIconsVisible.collect {
+ applyConstraints(clockSection, keyguardRootView, true)
+ }
+ }
}
}
}
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 2aebd99..48092c6 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
@@ -21,6 +21,7 @@
import android.annotation.DrawableRes
import android.annotation.SuppressLint
import android.graphics.Point
+import android.graphics.Rect
import android.view.HapticFeedbackConstants
import android.view.View
import android.view.View.OnLayoutChangeListener
@@ -158,6 +159,23 @@
}
launch {
+ val clipBounds = Rect()
+ viewModel.topClippingBounds.collect { clipTop ->
+ if (clipTop == null) {
+ view.setClipBounds(null)
+ } else {
+ clipBounds.apply {
+ top = clipTop
+ left = view.getLeft()
+ right = view.getRight()
+ bottom = view.getBottom()
+ }
+ view.setClipBounds(clipBounds)
+ }
+ }
+ }
+
+ launch {
viewModel.lockscreenStateAlpha.collect { alpha ->
childViews[statusViewId]?.alpha = alpha
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt
index eb3afb7..841bad4 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt
@@ -39,6 +39,7 @@
import android.view.ViewGroup
import android.view.WindowManager
import android.widget.FrameLayout
+import android.widget.TextView
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.view.isInvisible
import com.android.keyguard.ClockEventController
@@ -48,6 +49,8 @@
import com.android.systemui.biometrics.domain.interactor.UdfpsOverlayInteractor
import com.android.systemui.broadcast.BroadcastDispatcher
import com.android.systemui.common.ui.ConfigurationState
+import com.android.systemui.communal.ui.binder.CommunalTutorialIndicatorViewBinder
+import com.android.systemui.communal.ui.viewmodel.CommunalTutorialIndicatorViewModel
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.dagger.qualifiers.Main
@@ -133,6 +136,7 @@
private val screenOffAnimationController: ScreenOffAnimationController,
private val shadeInteractor: ShadeInteractor,
private val secureSettings: SecureSettings,
+ private val communalTutorialViewModel: CommunalTutorialIndicatorViewModel,
) {
val hostToken: IBinder? = bundle.getBinder(KEY_HOST_TOKEN)
private val width: Int = bundle.getInt(KEY_VIEW_WIDTH)
@@ -408,6 +412,8 @@
smartSpaceView?.let {
KeyguardPreviewSmartspaceViewBinder.bind(it, smartspaceViewModel)
}
+
+ setupCommunalTutorialIndicator(keyguardRootView)
}
)
}
@@ -601,6 +607,17 @@
}
}
+ private fun setupCommunalTutorialIndicator(keyguardRootView: ConstraintLayout) {
+ keyguardRootView.findViewById<TextView>(R.id.communal_tutorial_indicator)?.let {
+ indicatorView ->
+ CommunalTutorialIndicatorViewBinder.bind(
+ indicatorView,
+ communalTutorialViewModel,
+ isPreviewMode = true,
+ )
+ }
+ }
+
private suspend fun fetchThemeStyleFromSetting(): Style {
val overlayPackageJson =
withContext(backgroundDispatcher) {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodNotificationIconsSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodNotificationIconsSection.kt
index ed7abff..ad589df 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodNotificationIconsSection.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodNotificationIconsSection.kt
@@ -57,7 +57,6 @@
private var nicBindingDisposable: DisposableHandle? = null
private val nicId = R.id.aod_notification_icon_container
private lateinit var nic: NotificationIconContainer
- private val smartSpaceBarrier = View.generateViewId()
override fun addViews(constraintLayout: ConstraintLayout) {
if (!KeyguardShadeMigrationNssl.isEnabled) {
@@ -121,8 +120,20 @@
} else {
connect(nicId, TOP, R.id.keyguard_status_view, topAlignment, bottomMargin)
}
- connect(nicId, START, PARENT_ID, START)
- connect(nicId, END, PARENT_ID, END)
+ connect(
+ nicId,
+ START,
+ PARENT_ID,
+ START,
+ context.resources.getDimensionPixelSize(R.dimen.status_view_margin_horizontal)
+ )
+ connect(
+ nicId,
+ END,
+ PARENT_ID,
+ END,
+ context.resources.getDimensionPixelSize(R.dimen.status_view_margin_horizontal)
+ )
constrainHeight(
nicId,
context.resources.getDimensionPixelSize(R.dimen.notification_shelf_height)
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt
index b344d3b..fe4f07d 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt
@@ -19,6 +19,7 @@
import android.content.Context
import android.view.View
+import androidx.constraintlayout.widget.Barrier
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.constraintlayout.widget.ConstraintSet
import androidx.constraintlayout.widget.ConstraintSet.BOTTOM
@@ -30,11 +31,13 @@
import androidx.constraintlayout.widget.ConstraintSet.VISIBLE
import androidx.constraintlayout.widget.ConstraintSet.WRAP_CONTENT
import com.android.systemui.Flags
+import com.android.systemui.customization.R as customizationR
import com.android.systemui.keyguard.domain.interactor.KeyguardBlueprintInteractor
import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor
import com.android.systemui.keyguard.shared.model.KeyguardSection
import com.android.systemui.keyguard.ui.binder.KeyguardClockViewBinder
import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel
+import com.android.systemui.keyguard.ui.viewmodel.KeyguardSmartspaceViewModel
import com.android.systemui.plugins.clocks.ClockController
import com.android.systemui.plugins.clocks.ClockFaceLayout
import com.android.systemui.res.R
@@ -61,6 +64,7 @@
protected val keyguardClockViewModel: KeyguardClockViewModel,
private val context: Context,
private val splitShadeStateController: SplitShadeStateController,
+ val smartspaceViewModel: KeyguardSmartspaceViewModel,
val blueprintInteractor: Lazy<KeyguardBlueprintInteractor>,
) : KeyguardSection() {
override fun addViews(constraintLayout: ConstraintLayout) {}
@@ -117,6 +121,35 @@
private fun getLargeClockFace(clock: ClockController): ClockFaceLayout = clock.largeClock.layout
private fun getSmallClockFace(clock: ClockController): ClockFaceLayout = clock.smallClock.layout
+
+ fun constrainWeatherClockDateIconsBarrier(constraints: ConstraintSet) {
+ constraints.apply {
+ if (keyguardClockViewModel.isAodIconsVisible.value) {
+ createBarrier(
+ R.id.weather_clock_date_and_icons_barrier_bottom,
+ Barrier.BOTTOM,
+ 0,
+ *intArrayOf(sharedR.id.bc_smartspace_view, R.id.aod_notification_icon_container)
+ )
+ } else {
+ if (smartspaceViewModel.bcSmartspaceVisibility.value == VISIBLE) {
+ createBarrier(
+ R.id.weather_clock_date_and_icons_barrier_bottom,
+ Barrier.BOTTOM,
+ 0,
+ (sharedR.id.bc_smartspace_view)
+ )
+ } else {
+ createBarrier(
+ R.id.weather_clock_date_and_icons_barrier_bottom,
+ Barrier.BOTTOM,
+ getDimen(ENHANCED_SMARTSPACE_HEIGHT),
+ (R.id.lockscreen_clock_view)
+ )
+ }
+ }
+ }
+ }
open fun applyDefaultConstraints(constraints: ConstraintSet) {
val guideline =
if (keyguardClockViewModel.clockShouldBeCentered.value) PARENT_ID
@@ -128,16 +161,14 @@
var largeClockTopMargin =
context.resources.getDimensionPixelSize(R.dimen.status_bar_height) +
context.resources.getDimensionPixelSize(
- com.android.systemui.customization.R.dimen.small_clock_padding_top
+ customizationR.dimen.small_clock_padding_top
) +
context.resources.getDimensionPixelSize(R.dimen.keyguard_smartspace_top_offset)
largeClockTopMargin += getDimen(DATE_WEATHER_VIEW_HEIGHT)
largeClockTopMargin += getDimen(ENHANCED_SMARTSPACE_HEIGHT)
if (!keyguardClockViewModel.useLargeClock) {
largeClockTopMargin -=
- context.resources.getDimensionPixelSize(
- com.android.systemui.customization.R.dimen.small_clock_height
- )
+ context.resources.getDimensionPixelSize(customizationR.dimen.small_clock_height)
}
connect(R.id.lockscreen_clock_view_large, TOP, PARENT_ID, TOP, largeClockTopMargin)
constrainHeight(R.id.lockscreen_clock_view_large, WRAP_CONTENT)
@@ -145,18 +176,15 @@
constrainWidth(R.id.lockscreen_clock_view, WRAP_CONTENT)
constrainHeight(
R.id.lockscreen_clock_view,
- context.resources.getDimensionPixelSize(
- com.android.systemui.customization.R.dimen.small_clock_height
- )
+ context.resources.getDimensionPixelSize(customizationR.dimen.small_clock_height)
)
connect(
R.id.lockscreen_clock_view,
START,
PARENT_ID,
START,
- context.resources.getDimensionPixelSize(
- com.android.systemui.customization.R.dimen.clock_padding_start
- )
+ context.resources.getDimensionPixelSize(customizationR.dimen.clock_padding_start) +
+ context.resources.getDimensionPixelSize(R.dimen.status_view_margin_horizontal)
)
var smallClockTopMargin =
if (splitShadeStateController.shouldUseSplitNotificationShade(context.resources)) {
@@ -167,12 +195,12 @@
}
if (keyguardClockViewModel.useLargeClock) {
smallClockTopMargin -=
- context.resources.getDimensionPixelSize(
- com.android.systemui.customization.R.dimen.small_clock_height
- )
+ context.resources.getDimensionPixelSize(customizationR.dimen.small_clock_height)
}
connect(R.id.lockscreen_clock_view, TOP, PARENT_ID, TOP, smallClockTopMargin)
}
+
+ constrainWeatherClockDateIconsBarrier(constraints)
}
private fun getDimen(name: String): Int {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultNotificationStackScrollLayoutSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultNotificationStackScrollLayoutSection.kt
index 8c5e9b4..d75a72f 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultNotificationStackScrollLayoutSection.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultNotificationStackScrollLayoutSection.kt
@@ -18,7 +18,6 @@
package com.android.systemui.keyguard.ui.view.layout.sections
import android.content.Context
-import android.view.View
import androidx.constraintlayout.widget.ConstraintSet
import androidx.constraintlayout.widget.ConstraintSet.BOTTOM
import androidx.constraintlayout.widget.ConstraintSet.END
@@ -67,7 +66,6 @@
notificationStackSizeCalculator,
mainDispatcher,
) {
- private val smartSpaceBarrier = View.generateViewId()
override fun applyConstraints(constraintSet: ConstraintSet) {
if (!KeyguardShadeMigrationNssl.isEnabled) {
return
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SmartspaceSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SmartspaceSection.kt
index 37842a8..2f99719 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SmartspaceSection.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SmartspaceSection.kt
@@ -31,7 +31,8 @@
import com.android.systemui.keyguard.ui.binder.KeyguardSmartspaceViewBinder
import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel
import com.android.systemui.keyguard.ui.viewmodel.KeyguardSmartspaceViewModel
-import com.android.systemui.shared.R
+import com.android.systemui.res.R as R
+import com.android.systemui.shared.R as sharedR
import com.android.systemui.statusbar.lockscreen.LockscreenSmartspaceController
import dagger.Lazy
import javax.inject.Inject
@@ -100,94 +101,94 @@
if (!migrateClocksToBlueprint()) {
return
}
+ val horizontalPaddingStart =
+ context.resources.getDimensionPixelSize(R.dimen.below_clock_padding_start) +
+ context.resources.getDimensionPixelSize(R.dimen.status_view_margin_horizontal)
+ val horizontalPaddingEnd =
+ context.resources.getDimensionPixelSize(R.dimen.below_clock_padding_end) +
+ context.resources.getDimensionPixelSize(R.dimen.status_view_margin_horizontal)
constraintSet.apply {
// migrate addDateWeatherView, addWeatherView from KeyguardClockSwitchController
- constrainHeight(R.id.date_smartspace_view, ConstraintSet.WRAP_CONTENT)
- constrainWidth(R.id.date_smartspace_view, ConstraintSet.WRAP_CONTENT)
+ constrainHeight(sharedR.id.date_smartspace_view, ConstraintSet.WRAP_CONTENT)
+ constrainWidth(sharedR.id.date_smartspace_view, ConstraintSet.WRAP_CONTENT)
connect(
- R.id.date_smartspace_view,
+ sharedR.id.date_smartspace_view,
ConstraintSet.START,
ConstraintSet.PARENT_ID,
ConstraintSet.START,
- context.resources.getDimensionPixelSize(
- com.android.systemui.res.R.dimen.below_clock_padding_start
- )
+ horizontalPaddingStart
)
- constrainWidth(R.id.weather_smartspace_view, ConstraintSet.WRAP_CONTENT)
+ constrainWidth(sharedR.id.weather_smartspace_view, ConstraintSet.WRAP_CONTENT)
connect(
- R.id.weather_smartspace_view,
+ sharedR.id.weather_smartspace_view,
ConstraintSet.TOP,
- R.id.date_smartspace_view,
+ sharedR.id.date_smartspace_view,
ConstraintSet.TOP
)
connect(
- R.id.weather_smartspace_view,
+ sharedR.id.weather_smartspace_view,
ConstraintSet.BOTTOM,
- R.id.date_smartspace_view,
+ sharedR.id.date_smartspace_view,
ConstraintSet.BOTTOM
)
connect(
- R.id.weather_smartspace_view,
+ sharedR.id.weather_smartspace_view,
ConstraintSet.START,
- R.id.date_smartspace_view,
+ sharedR.id.date_smartspace_view,
ConstraintSet.END,
4
)
// migrate addSmartspaceView from KeyguardClockSwitchController
- constrainHeight(R.id.bc_smartspace_view, ConstraintSet.WRAP_CONTENT)
+ constrainHeight(sharedR.id.bc_smartspace_view, ConstraintSet.WRAP_CONTENT)
connect(
- R.id.bc_smartspace_view,
+ sharedR.id.bc_smartspace_view,
ConstraintSet.START,
ConstraintSet.PARENT_ID,
ConstraintSet.START,
- context.resources.getDimensionPixelSize(
- com.android.systemui.res.R.dimen.below_clock_padding_start
- )
+ horizontalPaddingStart
)
connect(
- R.id.bc_smartspace_view,
+ sharedR.id.bc_smartspace_view,
ConstraintSet.END,
if (keyguardClockViewModel.clockShouldBeCentered.value) ConstraintSet.PARENT_ID
- else com.android.systemui.res.R.id.split_shade_guideline,
+ else R.id.split_shade_guideline,
ConstraintSet.END,
- context.resources.getDimensionPixelSize(
- com.android.systemui.res.R.dimen.below_clock_padding_end
- )
+ horizontalPaddingEnd
)
if (keyguardClockViewModel.hasCustomWeatherDataDisplay.value) {
- clear(R.id.date_smartspace_view, ConstraintSet.TOP)
+ clear(sharedR.id.date_smartspace_view, ConstraintSet.TOP)
connect(
- R.id.date_smartspace_view,
+ sharedR.id.date_smartspace_view,
ConstraintSet.BOTTOM,
- R.id.bc_smartspace_view,
+ sharedR.id.bc_smartspace_view,
ConstraintSet.TOP
)
} else {
- clear(R.id.date_smartspace_view, ConstraintSet.BOTTOM)
+ clear(sharedR.id.date_smartspace_view, ConstraintSet.BOTTOM)
connect(
- R.id.date_smartspace_view,
+ sharedR.id.date_smartspace_view,
ConstraintSet.TOP,
- com.android.systemui.res.R.id.lockscreen_clock_view,
+ R.id.lockscreen_clock_view,
ConstraintSet.BOTTOM
)
connect(
- R.id.bc_smartspace_view,
+ sharedR.id.bc_smartspace_view,
ConstraintSet.TOP,
- R.id.date_smartspace_view,
+ sharedR.id.date_smartspace_view,
ConstraintSet.BOTTOM
)
}
createBarrier(
- com.android.systemui.res.R.id.smart_space_barrier_bottom,
+ R.id.smart_space_barrier_bottom,
Barrier.BOTTOM,
0,
*intArrayOf(
- R.id.bc_smartspace_view,
- R.id.date_smartspace_view,
- R.id.weather_smartspace_view,
+ sharedR.id.bc_smartspace_view,
+ sharedR.id.date_smartspace_view,
+ sharedR.id.weather_smartspace_view,
)
)
}
@@ -212,7 +213,7 @@
private fun updateVisibility(constraintSet: ConstraintSet) {
constraintSet.apply {
setVisibility(
- R.id.weather_smartspace_view,
+ sharedR.id.weather_smartspace_view,
when (keyguardClockViewModel.hasCustomWeatherDataDisplay.value) {
true -> ConstraintSet.GONE
false ->
@@ -223,7 +224,7 @@
}
)
setVisibility(
- R.id.date_smartspace_view,
+ sharedR.id.date_smartspace_view,
if (keyguardClockViewModel.hasCustomWeatherDataDisplay.value) ConstraintSet.GONE
else ConstraintSet.VISIBLE
)
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToLockscreenTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToLockscreenTransitionViewModel.kt
index bc51821..6aa2eca 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToLockscreenTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToLockscreenTransitionViewModel.kt
@@ -17,7 +17,7 @@
package com.android.systemui.keyguard.ui.viewmodel
import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.keyguard.domain.interactor.FromLockscreenTransitionInteractor
+import com.android.systemui.keyguard.domain.interactor.FromGlanceableHubTransitionInteractor
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow
import javax.inject.Inject
@@ -37,7 +37,7 @@
) {
private val transitionAnimation =
animationFlow.setup(
- duration = FromLockscreenTransitionInteractor.TO_GLANCEABLE_HUB_DURATION,
+ duration = FromGlanceableHubTransitionInteractor.TO_LOCKSCREEN_DURATION,
from = KeyguardState.GLANCEABLE_HUB,
to = KeyguardState.LOCKSCREEN,
)
@@ -45,10 +45,20 @@
// TODO(b/315205222): implement full animation spec instead of just a simple fade.
val keyguardAlpha: Flow<Float> =
transitionAnimation.sharedFlow(
- duration = FromLockscreenTransitionInteractor.TO_GLANCEABLE_HUB_DURATION,
+ duration = FromGlanceableHubTransitionInteractor.TO_LOCKSCREEN_DURATION,
onStep = { it },
onFinish = { 1f },
onCancel = { 0f },
name = "GLANCEABLE_HUB->LOCKSCREEN: keyguardAlpha",
)
+
+ // TODO(b/315205216): implement full animation spec instead of just a simple fade.
+ val notificationAlpha: Flow<Float> =
+ transitionAnimation.sharedFlow(
+ duration = FromGlanceableHubTransitionInteractor.TO_LOCKSCREEN_DURATION,
+ onStep = { it },
+ onFinish = { 1f },
+ onCancel = { 0f },
+ name = "GLANCEABLE_HUB->LOCKSCREEN: notificationAlpha",
+ )
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModel.kt
index f37d9f8..6763e0a 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModel.kt
@@ -27,6 +27,7 @@
import com.android.systemui.keyguard.shared.model.SettingsClockSize
import com.android.systemui.plugins.clocks.ClockController
import com.android.systemui.res.R
+import com.android.systemui.statusbar.notification.domain.interactor.NotificationsKeyguardInteractor
import com.android.systemui.statusbar.policy.SplitShadeStateController
import com.android.systemui.util.Utils
import javax.inject.Inject
@@ -44,6 +45,7 @@
private val keyguardClockInteractor: KeyguardClockInteractor,
@Application private val applicationScope: CoroutineScope,
private val splitShadeStateController: SplitShadeStateController,
+ notifsKeyguardInteractor: NotificationsKeyguardInteractor,
) {
var burnInLayer: Layer? = null
val useLargeClock: Boolean
@@ -91,6 +93,13 @@
initialValue = false
)
+ val isAodIconsVisible: StateFlow<Boolean> =
+ notifsKeyguardInteractor.areNotificationsFullyHidden.stateIn(
+ scope = applicationScope,
+ started = SharingStarted.WhileSubscribed(),
+ initialValue = false
+ )
+
// Needs to use a non application context to get display cutout.
fun getSmallClockTopMargin(context: Context) =
if (splitShadeStateController.shouldUseSplitNotificationShade(context.resources)) {
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 5d36da9..709e184 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
@@ -21,6 +21,7 @@
import android.view.View.VISIBLE
import com.android.systemui.Flags.newAodTransition
import com.android.systemui.common.shared.model.NotificationContainerBounds
+import com.android.systemui.communal.domain.interactor.CommunalInteractor
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
@@ -56,6 +57,7 @@
private val deviceEntryInteractor: DeviceEntryInteractor,
private val dozeParameters: DozeParameters,
private val keyguardInteractor: KeyguardInteractor,
+ communalInteractor: CommunalInteractor,
keyguardTransitionInteractor: KeyguardTransitionInteractor,
private val notificationsKeyguardInteractor: NotificationsKeyguardInteractor,
aodToLockscreenTransitionViewModel: AodToLockscreenTransitionViewModel,
@@ -80,13 +82,31 @@
val notificationBounds: StateFlow<NotificationContainerBounds> =
keyguardInteractor.notificationContainerBounds
+ /**
+ * The keyguard root view can be clipped as the shade is pulled down, typically only for
+ * non-split shade cases.
+ */
+ val topClippingBounds: Flow<Int?> = keyguardInteractor.topClippingBounds
+
/** An observable for the alpha level for the entire keyguard root view. */
val alpha: Flow<Float> =
- merge(
- aodAlphaViewModel.alpha,
- lockscreenToGlanceableHubTransitionViewModel.keyguardAlpha,
- glanceableHubToLockscreenTransitionViewModel.keyguardAlpha,
- )
+ combine(
+ communalInteractor.isIdleOnCommunal,
+ merge(
+ aodAlphaViewModel.alpha,
+ lockscreenToGlanceableHubTransitionViewModel.keyguardAlpha,
+ glanceableHubToLockscreenTransitionViewModel.keyguardAlpha,
+ )
+ ) { isIdleOnCommunal, alpha ->
+ if (isIdleOnCommunal) {
+ // Keyguard should not show while the communal hub is fully visible. This check
+ // is added since at the moment, closing the notification shade will cause the
+ // keyguard alpha to be set back to 1.
+ 0f
+ } else {
+ alpha
+ }
+ }
.distinctUntilChanged()
/** Specific alpha value for elements visible during [KeyguardState.LOCKSCREEN] */
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGlanceableHubTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGlanceableHubTransitionViewModel.kt
index 3ea83ae..3afa49e 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGlanceableHubTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGlanceableHubTransitionViewModel.kt
@@ -51,4 +51,14 @@
onCancel = { 1f },
name = "LOCKSCREEN->GLANCEABLE_HUB: keyguardAlpha",
)
+
+ // TODO(b/315205216): implement full animation spec instead of just a simple fade.
+ val notificationAlpha: Flow<Float> =
+ transitionAnimation.sharedFlow(
+ duration = FromLockscreenTransitionInteractor.TO_GLANCEABLE_HUB_DURATION,
+ onStep = { 1f - it },
+ onFinish = { 0f },
+ onCancel = { 1f },
+ name = "LOCKSCREEN->GLANCEABLE_HUB: notificationAlpha",
+ )
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/SideFpsProgressBarViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/SideFpsProgressBarViewModel.kt
index 693e3b7..ca9c857 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/SideFpsProgressBarViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/SideFpsProgressBarViewModel.kt
@@ -20,7 +20,7 @@
import android.content.Context
import android.graphics.Point
import androidx.annotation.VisibleForTesting
-import androidx.core.animation.doOnEnd
+import androidx.core.animation.addListener
import com.android.systemui.Flags
import com.android.systemui.biometrics.domain.interactor.DisplayStateInteractor
import com.android.systemui.biometrics.domain.interactor.SideFpsSensorInteractor
@@ -30,15 +30,18 @@
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFingerprintAuthInteractor
+import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
import com.android.systemui.keyguard.shared.model.AcquiredFingerprintAuthenticationStatus
import com.android.systemui.keyguard.shared.model.ErrorFingerprintAuthenticationStatus
import com.android.systemui.keyguard.shared.model.FailFingerprintAuthenticationStatus
import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus
+import com.android.systemui.power.domain.interactor.PowerInteractor
import com.android.systemui.res.R
import com.android.systemui.statusbar.phone.DozeServiceHost
import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
@@ -46,13 +49,14 @@
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onCompletion
-import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
+@ExperimentalCoroutinesApi
@SysUISingleton
class SideFpsProgressBarViewModel
@Inject
@@ -63,9 +67,11 @@
// todo (b/317432075) Injecting DozeServiceHost directly instead of using it through
// DozeInteractor as DozeServiceHost already depends on DozeInteractor.
private val dozeServiceHost: DozeServiceHost,
+ private val keyguardInteractor: KeyguardInteractor,
displayStateInteractor: DisplayStateInteractor,
@Main private val mainDispatcher: CoroutineDispatcher,
@Application private val applicationScope: CoroutineScope,
+ private val powerInteractor: PowerInteractor,
) {
private val _progress = MutableStateFlow(0.0f)
private val _visible = MutableStateFlow(false)
@@ -176,48 +182,54 @@
return@collectLatest
}
animatorJob =
- combine(
- sfpsSensorInteractor.authenticationDuration,
- fpAuthRepository.authenticationStatus,
- ::Pair
- )
- .onEach { (authDuration, authStatus) ->
- when (authStatus) {
- is AcquiredFingerprintAuthenticationStatus -> {
- if (authStatus.fingerprintCaptureStarted) {
- _visible.value = true
- dozeServiceHost.fireSideFpsAcquisitionStarted()
- _animator?.cancel()
- _animator =
- ValueAnimator.ofFloat(0.0f, 1.0f)
- .setDuration(authDuration)
- .apply {
- addUpdateListener {
- _progress.value = it.animatedValue as Float
- }
- addListener(
- doOnEnd {
- if (_progress.value == 0.0f) {
- _visible.value = false
- }
+ sfpsSensorInteractor.authenticationDuration
+ .flatMapLatest { authDuration ->
+ _animator?.cancel()
+ fpAuthRepository.authenticationStatus.map { authStatus ->
+ when (authStatus) {
+ is AcquiredFingerprintAuthenticationStatus -> {
+ if (authStatus.fingerprintCaptureStarted) {
+ if (keyguardInteractor.isDozing.value) {
+ dozeServiceHost.fireSideFpsAcquisitionStarted()
+ } else {
+ powerInteractor
+ .wakeUpForSideFingerprintAcquisition()
+ }
+ _animator?.cancel()
+ _animator =
+ ValueAnimator.ofFloat(0.0f, 1.0f)
+ .setDuration(authDuration)
+ .apply {
+ addUpdateListener {
+ _progress.value =
+ it.animatedValue as Float
}
- )
- }
- _animator?.start()
- } else if (authStatus.fingerprintCaptureCompleted) {
- onFingerprintCaptureCompleted()
- } else {
- // Abandoned FP Auth attempt
- _animator?.reverse()
+ addListener(
+ onEnd = {
+ if (_progress.value == 0.0f) {
+ _visible.value = false
+ }
+ },
+ onStart = { _visible.value = true },
+ onCancel = { _visible.value = false }
+ )
+ }
+ _animator?.start()
+ } else if (authStatus.fingerprintCaptureCompleted) {
+ onFingerprintCaptureCompleted()
+ } else {
+ // Abandoned FP Auth attempt
+ _animator?.reverse()
+ }
}
+ is ErrorFingerprintAuthenticationStatus ->
+ onFingerprintCaptureCompleted()
+ is FailFingerprintAuthenticationStatus ->
+ onFingerprintCaptureCompleted()
+ is SuccessFingerprintAuthenticationStatus ->
+ onFingerprintCaptureCompleted()
+ else -> Unit
}
- is ErrorFingerprintAuthenticationStatus ->
- onFingerprintCaptureCompleted()
- is FailFingerprintAuthenticationStatus ->
- onFingerprintCaptureCompleted()
- is SuccessFingerprintAuthenticationStatus ->
- onFingerprintCaptureCompleted()
- else -> Unit
}
}
.flowOn(mainDispatcher)
diff --git a/packages/SystemUI/src/com/android/systemui/log/SideFpsLogger.kt b/packages/SystemUI/src/com/android/systemui/log/SideFpsLogger.kt
index 171656a..ce64ac1 100644
--- a/packages/SystemUI/src/com/android/systemui/log/SideFpsLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/log/SideFpsLogger.kt
@@ -117,4 +117,13 @@
{ "SideFpsSensor auth duration changed: $long1" }
)
}
+
+ fun restToUnlockSettingEnabledChanged(enabled: Boolean) {
+ buffer.log(
+ TAG,
+ LogLevel.DEBUG,
+ { bool1 = enabled },
+ { "restToUnlockSettingEnabled: $bool1" }
+ )
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/resume/MediaResumeListener.kt b/packages/SystemUI/src/com/android/systemui/media/controls/resume/MediaResumeListener.kt
index 23ee00d..a3029b2 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/resume/MediaResumeListener.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/resume/MediaResumeListener.kt
@@ -146,7 +146,7 @@
null,
UserHandle.ALL
)
- userTracker.addCallback(userTrackerCallback, mainExecutor)
+ userTracker.addCallback(userTrackerCallback, backgroundExecutor)
loadSavedComponents()
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java
index 0a72a2f..068e5fd 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java
@@ -504,24 +504,6 @@
}
};
- private final ViewRootImpl.SurfaceChangedCallback mSurfaceChangedCallback =
- new SurfaceChangedCallback() {
- @Override
- public void surfaceCreated(Transaction t) {
- notifyNavigationBarSurface();
- }
-
- @Override
- public void surfaceDestroyed() {
- notifyNavigationBarSurface();
- }
-
- @Override
- public void surfaceReplaced(Transaction t) {
- notifyNavigationBarSurface();
- }
- };
-
private boolean mScreenPinningActive = false;
private final TaskStackChangeListener mTaskStackListener = new TaskStackChangeListener() {
@Override
@@ -787,8 +769,6 @@
mView.getViewTreeObserver().addOnComputeInternalInsetsListener(
mOnComputeInternalInsetsListener);
- mView.getViewRootImpl().addSurfaceChangedCallback(mSurfaceChangedCallback);
- notifyNavigationBarSurface();
mPipOptional.ifPresent(mView::addPipExclusionBoundsChangeListener);
mBackAnimation.ifPresent(mView::registerBackAnimation);
@@ -860,13 +840,8 @@
mHandler.removeCallbacks(mEnableLayoutTransitions);
mNavBarHelper.removeNavTaskStateUpdater(mNavbarTaskbarStateUpdater);
mPipOptional.ifPresent(mView::removePipExclusionBoundsChangeListener);
- ViewRootImpl viewRoot = mView.getViewRootImpl();
- if (viewRoot != null) {
- viewRoot.removeSurfaceChangedCallback(mSurfaceChangedCallback);
- }
mFrame = null;
mOrientationHandle = null;
- notifyNavigationBarSurface();
}
// TODO: Remove this when we update nav bar recreation
@@ -1026,17 +1001,6 @@
}
}
- private void notifyNavigationBarSurface() {
- ViewRootImpl viewRoot = mView.getViewRootImpl();
- SurfaceControl surface = mView.getParent() != null
- && viewRoot != null
- && viewRoot.getSurfaceControl() != null
- && viewRoot.getSurfaceControl().isValid()
- ? viewRoot.getSurfaceControl()
- : null;
- mOverviewProxyService.onNavigationBarSurfaceChanged(surface);
- }
-
private int deltaRotation(int oldRotation, int newRotation) {
int delta = newRotation - oldRotation;
if (delta < 0) delta += 4;
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanelController.kt b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanelController.kt
index fa03dc2..93a6eee 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanelController.kt
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanelController.kt
@@ -34,6 +34,8 @@
import androidx.core.os.postDelayed
import androidx.core.view.isVisible
import androidx.dynamicanimation.animation.DynamicAnimation
+import com.android.internal.jank.Cuj.CUJ_BACK_PANEL_ARROW
+import com.android.internal.jank.InteractionJankMonitor
import com.android.internal.util.LatencyTracker
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.plugins.NavigationEdgeBackPlugin
@@ -86,6 +88,7 @@
private val vibratorHelper: VibratorHelper,
private val configurationController: ConfigurationController,
private val latencyTracker: LatencyTracker,
+ private val interactionJankMonitor: InteractionJankMonitor,
) : ViewController<BackPanel>(BackPanel(context, latencyTracker)), NavigationEdgeBackPlugin {
/**
@@ -103,6 +106,7 @@
private val vibratorHelper: VibratorHelper,
private val configurationController: ConfigurationController,
private val latencyTracker: LatencyTracker,
+ private val interactionJankMonitor: InteractionJankMonitor,
) {
/** Construct a [BackPanelController]. */
fun create(context: Context): BackPanelController {
@@ -115,6 +119,7 @@
vibratorHelper,
configurationController,
latencyTracker,
+ interactionJankMonitor
)
backPanelController.init()
return backPanelController
@@ -183,7 +188,7 @@
/* Arrow is animating in */
ENTRY,
- /* could be entry, neutral, or stretched, releasing will commit back */
+ /* releasing will commit back */
ACTIVE,
/* releasing will cancel back */
@@ -366,6 +371,7 @@
// Receiving a CANCEL implies that something else intercepted
// the gesture, i.e., the user did not cancel their gesture.
// Therefore, disappear immediately, with minimum fanfare.
+ interactionJankMonitor.cancel(CUJ_BACK_PANEL_ARROW)
updateArrowState(GestureState.GONE)
velocityTracker = null
}
@@ -813,7 +819,7 @@
scale =
when (currentState) {
GestureState.ACTIVE,
- GestureState.FLUNG, -> params.activeIndicator.scale
+ GestureState.FLUNG -> params.activeIndicator.scale
GestureState.COMMITTED -> params.committedIndicator.scale
else -> params.preThresholdIndicator.scale
},
@@ -877,6 +883,16 @@
previousState = currentState
currentState = newState
+ // First, update the jank tracker
+ when (currentState) {
+ GestureState.ENTRY -> {
+ interactionJankMonitor.cancel(CUJ_BACK_PANEL_ARROW)
+ interactionJankMonitor.begin(mView, CUJ_BACK_PANEL_ARROW)
+ }
+ GestureState.GONE -> interactionJankMonitor.end(CUJ_BACK_PANEL_ARROW)
+ else -> {}
+ }
+
when (currentState) {
GestureState.CANCELLED -> {
backCallback.cancelBack()
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java
index 58e0428..91c86df 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java
@@ -84,6 +84,7 @@
import com.android.systemui.settings.UserTracker;
import com.android.systemui.shared.system.ActivityManagerWrapper;
import com.android.systemui.shared.system.InputChannelCompat;
+import com.android.systemui.shared.system.InputMonitorCompat;
import com.android.systemui.shared.system.QuickStepContract;
import com.android.systemui.shared.system.SysUiStatsLog;
import com.android.systemui.shared.system.TaskStackChangeListener;
@@ -261,7 +262,7 @@
private boolean mIsTrackpadThreeFingerSwipe;
private boolean mIsButtonForcedVisible;
- private InputMonitor mInputMonitor;
+ private InputMonitorCompat mInputMonitor;
private InputChannelCompat.InputEventReceiver mInputEventReceiver;
private NavigationEdgeBackPlugin mEdgeBackPlugin;
@@ -665,10 +666,8 @@
}
// Register input event receiver
- mInputMonitor = mContext.getSystemService(InputManager.class).monitorGestureInput(
- "edge-swipe", mDisplayId);
- mInputEventReceiver = new InputChannelCompat.InputEventReceiver(
- mInputMonitor.getInputChannel(), Looper.getMainLooper(),
+ mInputMonitor = new InputMonitorCompat("edge-swipe", mDisplayId);
+ mInputEventReceiver = mInputMonitor.getInputReceiver(Looper.getMainLooper(),
Choreographer.getInstance(), this::onInputEvent);
// Add a nav bar panel window
diff --git a/packages/SystemUI/src/com/android/systemui/power/PowerUI.java b/packages/SystemUI/src/com/android/systemui/power/PowerUI.java
index 958ace35..21de185 100644
--- a/packages/SystemUI/src/com/android/systemui/power/PowerUI.java
+++ b/packages/SystemUI/src/com/android/systemui/power/PowerUI.java
@@ -26,6 +26,8 @@
import android.database.ContentObserver;
import android.os.BatteryManager;
import android.os.Handler;
+import android.os.HandlerExecutor;
+import android.os.HandlerThread;
import android.os.IThermalEventListener;
import android.os.IThermalService;
import android.os.PowerManager;
@@ -95,6 +97,7 @@
private Future mLastShowWarningTask;
private boolean mEnableSkinTemperatureWarning;
private boolean mEnableUsbTemperatureAlarm;
+ private final HandlerThread mHandlerThread;
private int mLowBatteryAlertCloseLevel;
private final int[] mLowBatteryReminderLevels = new int[2];
@@ -167,6 +170,8 @@
mPowerManager = powerManager;
mWakefulnessLifecycle = wakefulnessLifecycle;
mUserTracker = userTracker;
+ mHandlerThread = new HandlerThread("PowerUI");
+ mHandlerThread.start();
}
public void start() {
@@ -185,7 +190,8 @@
false, obs, UserHandle.USER_ALL);
updateBatteryWarningLevels();
mReceiver.init();
- mUserTracker.addCallback(mUserChangedCallback, mContext.getMainExecutor());
+ mUserTracker.addCallback(mUserChangedCallback,
+ new HandlerExecutor(mHandlerThread.getThreadHandler()));
mWakefulnessLifecycle.addObserver(mWakefulnessObserver);
// Check to see if we need to let the user know that the phone previously shut down due
diff --git a/packages/SystemUI/src/com/android/systemui/power/domain/interactor/PowerInteractor.kt b/packages/SystemUI/src/com/android/systemui/power/domain/interactor/PowerInteractor.kt
index d9e3e55..3f8834a 100644
--- a/packages/SystemUI/src/com/android/systemui/power/domain/interactor/PowerInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/power/domain/interactor/PowerInteractor.kt
@@ -119,6 +119,11 @@
}
}
+ /** Wakes up the device for the Side FPS acquisition event. */
+ fun wakeUpForSideFingerprintAcquisition() {
+ repository.wakeUp("SFPS_FP_ACQUISITION_STARTED", PowerManager.WAKE_REASON_BIOMETRIC)
+ }
+
/**
* Called from [KeyguardService] to inform us that the device has started waking up. This is the
* canonical source of wakefulness information for System UI. This method should not be called
diff --git a/packages/SystemUI/src/com/android/systemui/privacy/OngoingPrivacyChip.kt b/packages/SystemUI/src/com/android/systemui/privacy/OngoingPrivacyChip.kt
index 8e1b00d..7a4be3f 100644
--- a/packages/SystemUI/src/com/android/systemui/privacy/OngoingPrivacyChip.kt
+++ b/packages/SystemUI/src/com/android/systemui/privacy/OngoingPrivacyChip.kt
@@ -23,11 +23,11 @@
import android.view.ViewGroup
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
+import android.widget.FrameLayout
import android.widget.ImageView
import android.widget.LinearLayout
import com.android.settingslib.Utils
import com.android.systemui.res.R
-import com.android.systemui.animation.view.LaunchableFrameLayout
import com.android.systemui.statusbar.events.BackgroundAnimatableView
class OngoingPrivacyChip @JvmOverloads constructor(
@@ -35,7 +35,7 @@
attrs: AttributeSet? = null,
defStyleAttrs: Int = 0,
defStyleRes: Int = 0
-) : LaunchableFrameLayout(context, attrs, defStyleAttrs, defStyleRes), BackgroundAnimatableView {
+) : FrameLayout(context, attrs, defStyleAttrs, defStyleRes), BackgroundAnimatableView {
private var configuration: Configuration
private var iconMargin = 0
@@ -43,6 +43,8 @@
private var iconColor = 0
private val iconsContainer: LinearLayout
+ val launchableContentView
+ get() = iconsContainer
var privacyList = emptyList<PrivacyItem>()
set(value) {
diff --git a/packages/SystemUI/src/com/android/systemui/privacy/PrivacyDialogControllerV2.kt b/packages/SystemUI/src/com/android/systemui/privacy/PrivacyDialogControllerV2.kt
index 76ef8a2..f121630 100644
--- a/packages/SystemUI/src/com/android/systemui/privacy/PrivacyDialogControllerV2.kt
+++ b/packages/SystemUI/src/com/android/systemui/privacy/PrivacyDialogControllerV2.kt
@@ -26,9 +26,9 @@
import android.os.UserHandle
import android.permission.PermissionGroupUsage
import android.permission.PermissionManager
-import android.view.View
import androidx.annotation.MainThread
import androidx.annotation.WorkerThread
+import androidx.core.view.isVisible
import com.android.internal.logging.UiEventLogger
import com.android.systemui.animation.DialogLaunchAnimator
import com.android.systemui.appops.AppOpsController
@@ -214,7 +214,7 @@
* @param context A context to use to create the dialog.
* @see filterAndSelect
*/
- fun showDialog(context: Context, view: View? = null) {
+ fun showDialog(context: Context, privacyChip: OngoingPrivacyChip? = null) {
dismissDialog()
backgroundExecutor.execute {
val usage = permGroupUsage()
@@ -277,8 +277,8 @@
)
d.setShowForAllUsers(true)
d.addOnDismissListener(onDialogDismissed)
- if (view != null) {
- val controller = getPrivacyDialogController(view)
+ if (privacyChip != null) {
+ val controller = getPrivacyDialogController(privacyChip)
if (controller == null) {
d.show()
} else {
@@ -296,10 +296,13 @@
}
}
- private fun getPrivacyDialogController(source: View): DialogLaunchAnimator.Controller? {
- val delegate = DialogLaunchAnimator.Controller.fromView(source) ?: return null
+ private fun getPrivacyDialogController(
+ source: OngoingPrivacyChip
+ ): DialogLaunchAnimator.Controller? {
+ val delegate =
+ DialogLaunchAnimator.Controller.fromView(source.launchableContentView) ?: return null
return object : DialogLaunchAnimator.Controller by delegate {
- override fun shouldAnimateExit() = false
+ override fun shouldAnimateExit() = source.isVisible
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java b/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java
index a3b9254..a2dfc01 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java
@@ -27,8 +27,11 @@
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
+import android.view.ViewGroup;
import android.widget.FrameLayout;
+import androidx.annotation.Nullable;
+
import com.android.systemui.Dumpable;
import com.android.systemui.qs.customize.QSCustomizer;
import com.android.systemui.res.R;
@@ -53,6 +56,7 @@
private QuickStatusBarHeader mHeader;
private float mQsExpansion;
private QSCustomizer mQSCustomizer;
+ private QSPanel mQSPanel;
private NonInterceptingScrollView mQSPanelContainer;
private int mHorizontalMargins;
@@ -72,6 +76,7 @@
protected void onFinishInflate() {
super.onFinishInflate();
mQSPanelContainer = findViewById(R.id.expanded_qs_scroll_view);
+ mQSPanel = findViewById(R.id.quick_settings_panel);
mHeader = findViewById(R.id.header);
mQSCustomizer = findViewById(R.id.qs_customize);
setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO);
@@ -79,6 +84,13 @@
void setSceneContainerEnabled(boolean enabled) {
mSceneContainerEnabled = enabled;
+ if (enabled) {
+ mQSPanelContainer.removeAllViews();
+ removeView(mQSPanelContainer);
+ LayoutParams lp = new LayoutParams(LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.WRAP_CONTENT);
+ addView(mQSPanel, 0, lp);
+ }
}
@Override
@@ -97,20 +109,26 @@
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// QSPanel will show as many rows as it can (up to TileLayout.MAX_ROWS) such that the
// bottom and footer are inside the screen.
- MarginLayoutParams layoutParams = (MarginLayoutParams) mQSPanelContainer.getLayoutParams();
-
int availableHeight = View.MeasureSpec.getSize(heightMeasureSpec);
- int maxQs = availableHeight - layoutParams.topMargin - layoutParams.bottomMargin
- - getPaddingBottom();
- int padding = mPaddingLeft + mPaddingRight + layoutParams.leftMargin
- + layoutParams.rightMargin;
- final int qsPanelWidthSpec = getChildMeasureSpec(widthMeasureSpec, padding,
- layoutParams.width);
- mQSPanelContainer.measure(qsPanelWidthSpec,
- MeasureSpec.makeMeasureSpec(maxQs, MeasureSpec.AT_MOST));
- int width = mQSPanelContainer.getMeasuredWidth() + padding;
- super.onMeasure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
- MeasureSpec.makeMeasureSpec(availableHeight, MeasureSpec.EXACTLY));
+
+ if (!mSceneContainerEnabled) {
+ MarginLayoutParams layoutParams =
+ (MarginLayoutParams) mQSPanelContainer.getLayoutParams();
+ int maxQs = availableHeight - layoutParams.topMargin - layoutParams.bottomMargin
+ - getPaddingBottom();
+ int padding = mPaddingLeft + mPaddingRight + layoutParams.leftMargin
+ + layoutParams.rightMargin;
+ final int qsPanelWidthSpec = getChildMeasureSpec(widthMeasureSpec, padding,
+ layoutParams.width);
+ mQSPanelContainer.measure(qsPanelWidthSpec,
+ MeasureSpec.makeMeasureSpec(maxQs, MeasureSpec.AT_MOST));
+ int width = mQSPanelContainer.getMeasuredWidth() + padding;
+ super.onMeasure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
+ MeasureSpec.makeMeasureSpec(availableHeight, MeasureSpec.EXACTLY));
+ } else {
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ }
+
// QSCustomizer will always be the height of the screen, but do this after
// other measuring to avoid changing the height of the QS.
mQSCustomizer.measure(widthMeasureSpec,
@@ -130,12 +148,15 @@
@Override
protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
- // Do not measure QSPanel again when doing super.onMeasure.
- // This prevents the pages in PagedTileLayout to be remeasured with a different (incorrect)
- // size to the one used for determining the number of rows and then the number of pages.
- if (child != mQSPanelContainer) {
- super.measureChildWithMargins(child, parentWidthMeasureSpec, widthUsed,
- parentHeightMeasureSpec, heightUsed);
+ if (!mSceneContainerEnabled) {
+ // Do not measure QSPanel again when doing super.onMeasure.
+ // This prevents the pages in PagedTileLayout to be remeasured with a different
+ // (incorrect) size to the one used for determining the number of rows and then the
+ // number of pages.
+ if (child != mQSPanelContainer) {
+ super.measureChildWithMargins(child, parentWidthMeasureSpec, widthUsed,
+ parentHeightMeasureSpec, heightUsed);
+ }
}
}
@@ -151,6 +172,7 @@
updateClippingPath();
}
+ @Nullable
public NonInterceptingScrollView getQSPanelContainer() {
return mQSPanelContainer;
}
@@ -172,11 +194,19 @@
.getDimensionPixelSize(
R.dimen.large_screen_shade_header_height);
}
- mQSPanelContainer.setPaddingRelative(
- mQSPanelContainer.getPaddingStart(),
- mSceneContainerEnabled ? 0 : topPadding,
- mQSPanelContainer.getPaddingEnd(),
- mQSPanelContainer.getPaddingBottom());
+ if (mQSPanelContainer != null) {
+ mQSPanelContainer.setPaddingRelative(
+ mQSPanelContainer.getPaddingStart(),
+ mSceneContainerEnabled ? 0 : topPadding,
+ mQSPanelContainer.getPaddingEnd(),
+ mQSPanelContainer.getPaddingBottom());
+ } else {
+ mQSPanel.setPaddingRelative(
+ mQSPanel.getPaddingStart(),
+ mSceneContainerEnabled ? 0 : topPadding,
+ mQSPanel.getPaddingEnd(),
+ mQSPanel.getPaddingBottom());
+ }
int horizontalMargins = getResources().getDimensionPixelSize(R.dimen.qs_horizontal_margin);
int horizontalPadding = getResources().getDimensionPixelSize(
@@ -220,7 +250,9 @@
public void setExpansion(float expansion) {
mQsExpansion = expansion;
- mQSPanelContainer.setScrollingEnabled(expansion > 0f);
+ if (mQSPanelContainer != null) {
+ mQSPanelContainer.setScrollingEnabled(expansion > 0f);
+ }
updateExpansion();
}
@@ -239,7 +271,7 @@
lp.rightMargin = mHorizontalMargins;
lp.leftMargin = mHorizontalMargins;
}
- if (view == mQSPanelContainer) {
+ if (view == mQSPanelContainer || view == mQSPanel) {
// QS panel lays out some of its content full width
qsPanelController.setContentMargins(mContentHorizontalPadding,
mContentHorizontalPadding);
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSContainerImplController.java b/packages/SystemUI/src/com/android/systemui/qs/QSContainerImplController.java
index 7b001c7..ffbc560 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSContainerImplController.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSContainerImplController.java
@@ -81,6 +81,9 @@
public void onInit() {
mQuickStatusBarHeaderController.init();
mView.setSceneContainerEnabled(mSceneContainerEnabled);
+ if (mSceneContainerEnabled && mQsPanelController != null) {
+ mQSPanelContainer.setOnTouchListener(null);
+ }
}
public void setListening(boolean listening) {
@@ -91,13 +94,17 @@
protected void onViewAttached() {
mView.updateResources(mQsPanelController, mQuickStatusBarHeaderController);
mConfigurationController.addCallback(mConfigurationListener);
- mQSPanelContainer.setOnTouchListener(mContainerTouchHandler);
+ if (!mSceneContainerEnabled && mQSPanelContainer != null) {
+ mQSPanelContainer.setOnTouchListener(mContainerTouchHandler);
+ }
}
@Override
protected void onViewDetached() {
mConfigurationController.removeCallback(mConfigurationListener);
- mQSPanelContainer.setOnTouchListener(null);
+ if (mQSPanelContainer != null) {
+ mQSPanelContainer.setOnTouchListener(null);
+ }
}
public QSContainerImpl getView() {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSImpl.java b/packages/SystemUI/src/com/android/systemui/qs/QSImpl.java
index 7f91fd2..290821e 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSImpl.java
@@ -61,6 +61,7 @@
import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel;
import com.android.systemui.qs.logging.QSLogger;
import com.android.systemui.res.R;
+import com.android.systemui.scene.shared.flag.SceneContainerFlags;
import com.android.systemui.shade.transition.LargeScreenShadeInterpolator;
import com.android.systemui.statusbar.CommandQueue;
import com.android.systemui.statusbar.StatusBarState;
@@ -171,8 +172,11 @@
private CommandQueue mCommandQueue;
private View mRootView;
+ @Nullable
private View mFooterActionsView;
+ private final SceneContainerFlags mSceneContainerFlags;
+
@Inject
public QSImpl(RemoteInputQuickSettingsDisabler remoteInputQsDisabler,
SysuiStatusBarStateController statusBarStateController, CommandQueue commandQueue,
@@ -185,7 +189,8 @@
FooterActionsViewModel.Factory footerActionsViewModelFactory,
FooterActionsViewBinder footerActionsViewBinder,
LargeScreenShadeInterpolator largeScreenShadeInterpolator,
- FeatureFlags featureFlags) {
+ FeatureFlags featureFlags,
+ SceneContainerFlags sceneContainerFlags) {
mRemoteInputQuickSettingsDisabler = remoteInputQsDisabler;
mQsMediaHost = qsMediaHost;
mQqsMediaHost = qqsMediaHost;
@@ -201,6 +206,7 @@
mFooterActionsViewModelFactory = footerActionsViewModelFactory;
mFooterActionsViewBinder = footerActionsViewBinder;
mListeningAndVisibilityLifecycleOwner = new ListeningAndVisibilityLifecycleOwner();
+ mSceneContainerFlags = sceneContainerFlags;
}
/**
@@ -216,10 +222,17 @@
mQSPanelController.init();
mQuickQSPanelController.init();
- mQSFooterActionsViewModel = mFooterActionsViewModelFactory
- .create(mListeningAndVisibilityLifecycleOwner);
- bindFooterActionsView(mRootView);
- mFooterActionsController.init();
+ if (!mSceneContainerFlags.isEnabled()) {
+ mQSFooterActionsViewModel = mFooterActionsViewModelFactory
+ .create(mListeningAndVisibilityLifecycleOwner);
+ bindFooterActionsView(mRootView);
+ mFooterActionsController.init();
+ } else {
+ View footerView = mRootView.findViewById(R.id.qs_footer_actions);
+ if (footerView != null) {
+ ((ViewGroup) footerView.getParent()).removeView(footerView);
+ }
+ }
mQSPanelScrollView = mRootView.findViewById(R.id.expanded_qs_scroll_view);
mQSPanelScrollView.addOnLayoutChangeListener(
@@ -234,6 +247,7 @@
mScrollListener.onQsPanelScrollChanged(scrollY);
}
});
+ mQSPanelScrollView.setScrollingEnabled(!mSceneContainerFlags.isEnabled());
mHeader = mRootView.findViewById(R.id.header);
mFooter = qsComponent.getQSFooter();
@@ -481,7 +495,9 @@
boolean footerVisible = qsPanelVisible && (mQsExpanded || !keyguardShowing
|| mHeaderAnimating || mShowCollapsedOnKeyguard);
mFooter.setVisibility(footerVisible ? View.VISIBLE : View.INVISIBLE);
- mFooterActionsView.setVisibility(footerVisible ? View.VISIBLE : View.INVISIBLE);
+ if (mFooterActionsView != null) {
+ mFooterActionsView.setVisibility(footerVisible ? View.VISIBLE : View.INVISIBLE);
+ }
mFooter.setExpanded((keyguardShowing && !mHeaderAnimating && !mShowCollapsedOnKeyguard)
|| (mQsExpanded && !mStackScrollerOverscrolling));
mQSPanelController.setVisibility(qsPanelVisible ? View.VISIBLE : View.INVISIBLE);
@@ -622,8 +638,13 @@
@Override
public int getHeightDiff() {
- return mQSPanelScrollView.getBottom() - mHeader.getBottom()
- + mHeader.getPaddingBottom();
+ if (mSceneContainerFlags.isEnabled()) {
+ return mQSPanelController.getViewBottom() - mHeader.getBottom()
+ + mHeader.getPaddingBottom();
+ } else {
+ return mQSPanelScrollView.getBottom() - mHeader.getBottom()
+ + mHeader.getPaddingBottom();
+ }
}
@Override
@@ -678,25 +699,29 @@
mFooter.setExpansion(onKeyguardAndExpanded ? 1 : expansion);
float footerActionsExpansion =
onKeyguardAndExpanded ? 1 : mInSplitShade ? alphaProgress : expansion;
- mQSFooterActionsViewModel.onQuickSettingsExpansionChanged(footerActionsExpansion,
- mInSplitShade);
+ if (mQSFooterActionsViewModel != null) {
+ mQSFooterActionsViewModel.onQuickSettingsExpansionChanged(footerActionsExpansion,
+ mInSplitShade);
+ }
mQSPanelController.setRevealExpansion(expansion);
mQSPanelController.getTileLayout().setExpansion(expansion, proposedTranslation);
mQuickQSPanelController.getTileLayout().setExpansion(expansion, proposedTranslation);
- float qsScrollViewTranslation =
- onKeyguard && !mShowCollapsedOnKeyguard ? panelTranslationY : 0;
- mQSPanelScrollView.setTranslationY(qsScrollViewTranslation);
+ if (!mSceneContainerFlags.isEnabled()) {
+ float qsScrollViewTranslation =
+ onKeyguard && !mShowCollapsedOnKeyguard ? panelTranslationY : 0;
+ mQSPanelScrollView.setTranslationY(qsScrollViewTranslation);
- if (fullyCollapsed) {
- mQSPanelScrollView.setScrollY(0);
- }
+ if (fullyCollapsed) {
+ mQSPanelScrollView.setScrollY(0);
+ }
- if (!fullyExpanded) {
- // Set bounds on the QS panel so it doesn't run over the header when animating.
- mQsBounds.top = (int) -mQSPanelScrollView.getTranslationY();
- mQsBounds.right = mQSPanelScrollView.getWidth();
- mQsBounds.bottom = mQSPanelScrollView.getHeight();
+ if (!fullyExpanded) {
+ // Set bounds on the QS panel so it doesn't run over the header when animating.
+ mQsBounds.top = (int) -mQSPanelScrollView.getTranslationY();
+ mQsBounds.right = mQSPanelScrollView.getWidth();
+ mQsBounds.bottom = mQSPanelScrollView.getHeight();
+ }
}
updateQsBounds();
@@ -786,15 +811,17 @@
mQsBounds.set(-sideMargin, 0, mQSPanelScrollView.getWidth() + sideMargin,
mQSPanelScrollView.getHeight());
}
- mQSPanelScrollView.setClipBounds(mQsBounds);
+ if (!mSceneContainerFlags.isEnabled()) {
+ mQSPanelScrollView.setClipBounds(mQsBounds);
- mQSPanelScrollView.getLocationOnScreen(mLocationTemp);
- int left = mLocationTemp[0];
- int top = mLocationTemp[1];
- mQsMediaHost.getCurrentClipping().set(left, top,
- left + getView().getMeasuredWidth(),
- top + mQSPanelScrollView.getMeasuredHeight()
- - mQSPanelController.getPaddingBottom());
+ mQSPanelScrollView.getLocationOnScreen(mLocationTemp);
+ int left = mLocationTemp[0];
+ int top = mLocationTemp[1];
+ mQsMediaHost.getCurrentClipping().set(left, top,
+ left + getView().getMeasuredWidth(),
+ top + mQSPanelScrollView.getMeasuredHeight()
+ - mQSPanelController.getPaddingBottom());
+ }
}
private void updateMediaPositions() {
@@ -867,9 +894,15 @@
// The customize state changed, so our height changed.
mContainer.updateExpansion();
boolean customizing = isCustomizing();
- mQSPanelScrollView.setVisibility(!customizing ? View.VISIBLE : View.INVISIBLE);
+ if (mSceneContainerFlags.isEnabled()) {
+ mQSPanelController.setVisibility(!customizing ? View.VISIBLE : View.INVISIBLE);
+ } else {
+ mQSPanelScrollView.setVisibility(!customizing ? View.VISIBLE : View.INVISIBLE);
+ }
mFooter.setVisibility(!customizing ? View.VISIBLE : View.INVISIBLE);
- mFooterActionsView.setVisibility(!customizing ? View.VISIBLE : View.INVISIBLE);
+ if (mFooterActionsView != null) {
+ mFooterActionsView.setVisibility(!customizing ? View.VISIBLE : View.INVISIBLE);
+ }
mHeader.setVisibility(!customizing ? View.VISIBLE : View.INVISIBLE);
// Let the panel know the position changed and it needs to update where notifications
// and whatnot are.
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java
index 51b94dd..7a7ee59 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java
@@ -387,7 +387,7 @@
setPaddingRelative(getPaddingStart(),
mSceneContainerEnabled ? 0 : paddingTop,
getPaddingEnd(),
- paddingBottom);
+ mSceneContainerEnabled ? 0 : paddingBottom);
}
void addOnConfigurationChangedListener(OnConfigurationChangedListener listener) {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java
index ef58a60..c3f5086 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java
@@ -278,5 +278,9 @@
public int getPaddingBottom() {
return mView.getPaddingBottom();
}
+
+ int getViewBottom() {
+ return mView.getBottom();
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/dagger/BaseAutoAddableModule.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/dagger/BaseAutoAddableModule.kt
index adea26e..e1ec338 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/pipeline/dagger/BaseAutoAddableModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/dagger/BaseAutoAddableModule.kt
@@ -18,6 +18,8 @@
import android.content.res.Resources
import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.qs.pipeline.domain.autoaddable.A11yShortcutAutoAddable
+import com.android.systemui.qs.pipeline.domain.autoaddable.A11yShortcutAutoAddableList
import com.android.systemui.qs.pipeline.domain.autoaddable.AutoAddableSetting
import com.android.systemui.qs.pipeline.domain.autoaddable.AutoAddableSettingList
import com.android.systemui.qs.pipeline.domain.autoaddable.CastAutoAddable
@@ -51,6 +53,16 @@
)
.toSet()
}
+
+ @Provides
+ @ElementsIntoSet
+ fun providesA11yShortcutAutoAddable(
+ a11yShortcutAutoAddableFactory: A11yShortcutAutoAddable.Factory
+ ): Set<AutoAddable> {
+ return A11yShortcutAutoAddableList.getA11yShortcutAutoAddables(
+ a11yShortcutAutoAddableFactory
+ )
+ }
}
@Binds @IntoSet fun bindCastAutoAddable(impl: CastAutoAddable): AutoAddable
diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/InstalledTilesComponentRepository.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/InstalledTilesComponentRepository.kt
index bcd09bd..dc39c97 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/InstalledTilesComponentRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/InstalledTilesComponentRepository.kt
@@ -25,6 +25,7 @@
import android.content.pm.PackageManager.ResolveInfoFlags
import android.os.UserHandle
import android.service.quicksettings.TileService
+import androidx.annotation.GuardedBy
import com.android.systemui.common.data.repository.PackageChangeRepository
import com.android.systemui.common.data.shared.model.PackageChangeModel
import com.android.systemui.dagger.SysUISingleton
@@ -32,12 +33,13 @@
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.util.kotlin.isComponentActuallyEnabled
import javax.inject.Inject
-import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.distinctUntilChanged
-import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onStart
+import kotlinx.coroutines.flow.shareIn
interface InstalledTilesComponentRepository {
@@ -49,33 +51,39 @@
@Inject
constructor(
@Application private val applicationContext: Context,
- @Background private val backgroundDispatcher: CoroutineDispatcher,
+ @Background private val backgroundScope: CoroutineScope,
private val packageChangeRepository: PackageChangeRepository
) : InstalledTilesComponentRepository {
- override fun getInstalledTilesComponents(userId: Int): Flow<Set<ComponentName>> {
- /*
- * In order to query [PackageManager] for different users, this implementation will call
- * [Context.createContextAsUser] and retrieve the [PackageManager] from that context.
- */
- val packageManager =
- if (applicationContext.userId == userId) {
- applicationContext.packageManager
- } else {
- applicationContext
- .createContextAsUser(
- UserHandle.of(userId),
- /* flags */ 0,
- )
- .packageManager
+ @GuardedBy("userMap") private val userMap = mutableMapOf<Int, Flow<Set<ComponentName>>>()
+
+ override fun getInstalledTilesComponents(userId: Int): Flow<Set<ComponentName>> =
+ synchronized(userMap) {
+ userMap.getOrPut(userId) {
+ /*
+ * In order to query [PackageManager] for different users, this implementation will
+ * call [Context.createContextAsUser] and retrieve the [PackageManager] from that
+ * context.
+ */
+ val packageManager =
+ if (applicationContext.userId == userId) {
+ applicationContext.packageManager
+ } else {
+ applicationContext
+ .createContextAsUser(
+ UserHandle.of(userId),
+ /* flags */ 0,
+ )
+ .packageManager
+ }
+ packageChangeRepository
+ .packageChanged(UserHandle.of(userId))
+ .onStart { emit(PackageChangeModel.Empty) }
+ .map { reloadComponents(userId, packageManager) }
+ .distinctUntilChanged()
+ .shareIn(backgroundScope, SharingStarted.WhileSubscribed(), replay = 1)
}
- return packageChangeRepository
- .packageChanged(UserHandle.of(userId))
- .onStart { emit(PackageChangeModel.Empty) }
- .map { reloadComponents(userId, packageManager) }
- .distinctUntilChanged()
- .flowOn(backgroundDispatcher)
- }
+ }
@WorkerThread
private fun reloadComponents(userId: Int, packageManager: PackageManager): Set<ComponentName> {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/A11yShortcutAutoAddable.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/A11yShortcutAutoAddable.kt
new file mode 100644
index 0000000..2cebbe3
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/A11yShortcutAutoAddable.kt
@@ -0,0 +1,94 @@
+/*
+ * 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.qs.pipeline.domain.autoaddable
+
+import android.content.ComponentName
+import android.provider.Settings
+import com.android.systemui.accessibility.data.repository.AccessibilityQsShortcutsRepository
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.qs.pipeline.domain.model.AutoAddSignal
+import com.android.systemui.qs.pipeline.domain.model.AutoAddTracking
+import com.android.systemui.qs.pipeline.domain.model.AutoAddable
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import java.util.Objects
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.map
+
+/**
+ * [A11yShortcutAutoAddable] will auto add/remove qs tile of the accessibility framework feature
+ * based on the user's choices in the Settings app.
+ *
+ * The a11y feature component is added to [Settings.Secure.ACCESSIBILITY_QS_TARGETS] when the user
+ * selects to use qs tile as a shortcut for the a11 feature in the Settings app. The accessibility
+ * feature component is removed from [Settings.Secure.ACCESSIBILITY_QS_TARGETS] when the user
+ * doesn't want to use qs tile as a shortcut for the a11y feature in the Settings app.
+ *
+ * [A11yShortcutAutoAddable] tracks a [Settings.Secure.ACCESSIBILITY_QS_TARGETS] and when its value
+ * changes, it will emit a [AutoAddSignal.Add] for the [spec] if the [componentName] is a substring
+ * of the value; it will emit a [AutoAddSignal.Remove] for the [spec] if the [componentName] is not
+ * a substring of the value.
+ */
+class A11yShortcutAutoAddable
+@AssistedInject
+constructor(
+ private val a11yQsShortcutsRepository: AccessibilityQsShortcutsRepository,
+ @Background private val bgDispatcher: CoroutineDispatcher,
+ @Assisted private val spec: TileSpec,
+ @Assisted private val componentName: ComponentName
+) : AutoAddable {
+
+ override fun autoAddSignal(userId: Int): Flow<AutoAddSignal> {
+ return a11yQsShortcutsRepository
+ .a11yQsShortcutTargets(userId)
+ .map { it.contains(componentName.flattenToString()) }
+ .filterNotNull()
+ .distinctUntilChanged()
+ .map { if (it) AutoAddSignal.Add(spec) else AutoAddSignal.Remove(spec) }
+ .flowOn(bgDispatcher)
+ }
+
+ override val autoAddTracking = AutoAddTracking.Always
+
+ override val description =
+ "A11yShortcutAutoAddableSetting: $spec:$componentName ($autoAddTracking)"
+
+ override fun equals(other: Any?): Boolean {
+ return other is A11yShortcutAutoAddable &&
+ spec == other.spec &&
+ componentName == other.componentName
+ }
+
+ override fun hashCode(): Int {
+ return Objects.hash(spec, componentName)
+ }
+
+ override fun toString(): String {
+ return description
+ }
+
+ @AssistedFactory
+ interface Factory {
+ fun create(spec: TileSpec, componentName: ComponentName): A11yShortcutAutoAddable
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/A11yShortcutAutoAddableList.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/A11yShortcutAutoAddableList.kt
new file mode 100644
index 0000000..08e3920
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/A11yShortcutAutoAddableList.kt
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.pipeline.domain.autoaddable
+
+import android.view.accessibility.Flags
+import com.android.internal.accessibility.AccessibilityShortcutController
+import com.android.systemui.qs.pipeline.domain.model.AutoAddable
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.android.systemui.qs.tiles.ColorCorrectionTile
+import com.android.systemui.qs.tiles.ColorInversionTile
+import com.android.systemui.qs.tiles.OneHandedModeTile
+import com.android.systemui.qs.tiles.ReduceBrightColorsTile
+
+object A11yShortcutAutoAddableList {
+
+ /**
+ * Generate a collection of [A11yShortcutAutoAddable] for the framework tiles related to
+ * accessibility features with shortcut options
+ */
+ fun getA11yShortcutAutoAddables(factory: A11yShortcutAutoAddable.Factory): Set<AutoAddable> {
+ return if (Flags.a11yQsShortcut()) {
+ setOf(
+ factory.create(
+ TileSpec.create(ColorCorrectionTile.TILE_SPEC),
+ AccessibilityShortcutController.DALTONIZER_COMPONENT_NAME
+ ),
+ factory.create(
+ TileSpec.create(ColorInversionTile.TILE_SPEC),
+ AccessibilityShortcutController.COLOR_INVERSION_COMPONENT_NAME
+ ),
+ factory.create(
+ TileSpec.create(OneHandedModeTile.TILE_SPEC),
+ AccessibilityShortcutController.ONE_HANDED_COMPONENT_NAME
+ ),
+ factory.create(
+ TileSpec.create(ReduceBrightColorsTile.TILE_SPEC),
+ AccessibilityShortcutController.REDUCE_BRIGHT_COLORS_COMPONENT_NAME
+ ),
+ )
+ } else {
+ emptySet()
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSIconViewImpl.java b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSIconViewImpl.java
index d04e4f5..53f287b 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSIconViewImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSIconViewImpl.java
@@ -33,11 +33,13 @@
import android.widget.ImageView;
import android.widget.ImageView.ScaleType;
+import androidx.annotation.VisibleForTesting;
+
import com.android.settingslib.Utils;
-import com.android.systemui.res.R;
import com.android.systemui.plugins.qs.QSIconView;
import com.android.systemui.plugins.qs.QSTile;
import com.android.systemui.plugins.qs.QSTile.State;
+import com.android.systemui.res.R;
import java.util.Objects;
@@ -52,7 +54,10 @@
private boolean mDisabledByPolicy = false;
private int mTint;
@Nullable
- private QSTile.Icon mLastIcon;
+ @VisibleForTesting
+ QSTile.Icon mLastIcon;
+
+ private boolean mIconChangeScheduled;
private ValueAnimator mColorAnimator = new ValueAnimator();
@@ -112,6 +117,7 @@
}
protected void updateIcon(ImageView iv, State state, boolean allowAnimations) {
+ mIconChangeScheduled = false;
final QSTile.Icon icon = state.iconSupplier != null ? state.iconSupplier.get() : state.icon;
if (!Objects.equals(icon, iv.getTag(R.id.qs_icon_tag))) {
boolean shouldAnimate = allowAnimations && shouldAnimate(iv);
@@ -167,7 +173,12 @@
mState = state.state;
mDisabledByPolicy = state.disabledByPolicy;
if (mTint != 0 && allowAnimations && shouldAnimate(iv)) {
- animateGrayScale(mTint, color, iv, () -> updateIcon(iv, state, allowAnimations));
+ mIconChangeScheduled = true;
+ animateGrayScale(mTint, color, iv, () -> {
+ if (mIconChangeScheduled) {
+ updateIcon(iv, state, allowAnimations);
+ }
+ });
} else {
setTint(iv, color);
updateIcon(iv, state, allowAnimations);
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/RecordIssueTile.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/RecordIssueTile.kt
index 216d716..88863cb 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/RecordIssueTile.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/RecordIssueTile.kt
@@ -17,6 +17,8 @@
package com.android.systemui.qs.tiles
import android.app.AlertDialog
+import android.app.BroadcastOptions
+import android.app.PendingIntent
import android.content.Intent
import android.os.Handler
import android.os.Looper
@@ -42,6 +44,8 @@
import com.android.systemui.qs.tileimpl.QSTileImpl
import com.android.systemui.recordissue.RecordIssueDialogDelegate
import com.android.systemui.res.R
+import com.android.systemui.screenrecord.RecordingService
+import com.android.systemui.settings.UserContextProvider
import com.android.systemui.statusbar.phone.KeyguardDismissUtil
import com.android.systemui.statusbar.policy.KeyguardStateController
import javax.inject.Inject
@@ -61,6 +65,7 @@
private val keyguardDismissUtil: KeyguardDismissUtil,
private val keyguardStateController: KeyguardStateController,
private val dialogLaunchAnimator: DialogLaunchAnimator,
+ private val userContextProvider: UserContextProvider,
private val delegateFactory: RecordIssueDialogDelegate.Factory,
) :
QSTileImpl<QSTile.BooleanState>(
@@ -91,12 +96,22 @@
public override fun handleClick(view: View?) {
if (isRecording) {
isRecording = false
+ stopScreenRecord()
} else {
mUiHandler.post { showPrompt(view) }
}
refreshState()
}
+ private fun stopScreenRecord() =
+ PendingIntent.getService(
+ userContextProvider.userContext,
+ RecordingService.REQUEST_CODE,
+ RecordingService.getStopIntent(userContextProvider.userContext),
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
+ )
+ .send(BroadcastOptions.makeBasic().apply { isInteractive = true }.toBundle())
+
private fun showPrompt(view: View?) {
val dialog: AlertDialog =
delegateFactory
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogController.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogController.java
index 592cb3b..211b4594 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogController.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogController.java
@@ -192,6 +192,7 @@
private DialogLaunchAnimator mDialogLaunchAnimator;
private boolean mHasWifiEntries;
private WifiStateWorker mWifiStateWorker;
+ private boolean mHasActiveSubId;
@VisibleForTesting
static final float TOAST_PARAMS_HORIZONTAL_WEIGHT = 1.0f;
@@ -299,6 +300,7 @@
mExecutor);
// Listen the subscription changes
mOnSubscriptionsChangedListener = new InternetOnSubscriptionChangedListener();
+ refreshHasActiveSubId();
mSubscriptionManager.addOnSubscriptionsChangedListener(mExecutor,
mOnSubscriptionsChangedListener);
mDefaultDataSubId = getDefaultDataSubscriptionId();
@@ -901,18 +903,22 @@
* @return whether there is the carrier item in the slice.
*/
boolean hasActiveSubId() {
- if (mSubscriptionManager == null) {
- if (DEBUG) {
- Log.d(TAG, "SubscriptionManager is null, can not check carrier.");
- }
+ if (isAirplaneModeEnabled() || mTelephonyManager == null) {
return false;
}
- if (isAirplaneModeEnabled() || mTelephonyManager == null
- || mSubscriptionManager.getActiveSubscriptionIdList().length <= 0) {
- return false;
+ return mHasActiveSubId;
+ }
+
+ private void refreshHasActiveSubId() {
+ if (mSubscriptionManager == null) {
+ mHasActiveSubId = false;
+ Log.e(TAG, "SubscriptionManager is null, set mHasActiveSubId = false");
+ return;
}
- return true;
+
+ mHasActiveSubId = mSubscriptionManager.getActiveSubscriptionIdList().length > 0;
+ Log.i(TAG, "mHasActiveSubId:" + mHasActiveSubId);
}
/**
@@ -1204,6 +1210,7 @@
@Override
public void onSubscriptionsChanged() {
+ refreshHasActiveSubId();
updateListener();
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/ui/adapter/QSSceneAdapter.kt b/packages/SystemUI/src/com/android/systemui/qs/ui/adapter/QSSceneAdapter.kt
index ce840ee..0d43396 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/ui/adapter/QSSceneAdapter.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/ui/adapter/QSSceneAdapter.kt
@@ -17,10 +17,13 @@
package com.android.systemui.qs.ui.adapter
import android.content.Context
+import android.content.pm.ActivityInfo
import android.os.Bundle
import android.view.View
import androidx.annotation.VisibleForTesting
import androidx.asynclayoutinflater.view.AsyncLayoutInflater
+import com.android.settingslib.applications.InterestingConfigChanges
+import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Main
@@ -58,7 +61,7 @@
/**
* Inflate an instance of [QSImpl] for this context. Once inflated, it will be available in
- * [qsView]
+ * [qsView]. Re-inflations due to configuration changes will use the last used [context].
*/
suspend fun inflate(context: Context)
@@ -90,6 +93,7 @@
private val qsImplProvider: Provider<QSImpl>,
@Main private val mainDispatcher: CoroutineDispatcher,
@Application applicationScope: CoroutineScope,
+ private val configurationInteractor: ConfigurationInteractor,
private val asyncLayoutInflaterFactory: (Context) -> AsyncLayoutInflater,
) : QSContainerController, QSSceneAdapter {
@@ -99,7 +103,15 @@
qsImplProvider: Provider<QSImpl>,
@Main dispatcher: CoroutineDispatcher,
@Application scope: CoroutineScope,
- ) : this(qsSceneComponentFactory, qsImplProvider, dispatcher, scope, ::AsyncLayoutInflater)
+ configurationInteractor: ConfigurationInteractor,
+ ) : this(
+ qsSceneComponentFactory,
+ qsImplProvider,
+ dispatcher,
+ scope,
+ configurationInteractor,
+ ::AsyncLayoutInflater,
+ )
private val state = MutableStateFlow<QSSceneAdapter.State>(QSSceneAdapter.State.CLOSED)
private val _isCustomizing: MutableStateFlow<Boolean> = MutableStateFlow(false)
@@ -109,14 +121,36 @@
val qsImpl = _qsImpl.asStateFlow()
override val qsView: Flow<View> = _qsImpl.map { it?.view }.filterNotNull()
+ // Same config changes as in FragmentHostManager
+ private val interestingChanges =
+ InterestingConfigChanges(
+ ActivityInfo.CONFIG_FONT_SCALE or
+ ActivityInfo.CONFIG_LOCALE or
+ ActivityInfo.CONFIG_ASSETS_PATHS
+ )
+
init {
applicationScope.launch {
- state.sample(_isCustomizing, ::Pair).collect { (state, customizing) ->
- _qsImpl.value?.apply {
- if (state != QSSceneAdapter.State.QS && customizing) {
- this@apply.closeCustomizerImmediately()
+ launch {
+ state.sample(_isCustomizing, ::Pair).collect { (state, customizing) ->
+ _qsImpl.value?.apply {
+ if (state != QSSceneAdapter.State.QS && customizing) {
+ this@apply.closeCustomizerImmediately()
+ }
+ applyState(state)
}
- applyState(state)
+ }
+ }
+ launch {
+ configurationInteractor.configurationValues.collect { config ->
+ if (interestingChanges.applyNewConfig(config)) {
+ // Assumption: The context is always the same and with the same theme.
+ // If colors change they will be reflected as attributes in the theme.
+ qsImpl.value?.view?.let { inflate(it.context) }
+ } else {
+ qsImpl.value?.onConfigurationChanged(config)
+ qsImpl.value?.view?.dispatchConfigurationChanged(config)
+ }
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModel.kt
index e5e1e84..8a900ece 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModel.kt
@@ -16,7 +16,10 @@
package com.android.systemui.qs.ui.viewmodel
+import androidx.lifecycle.LifecycleOwner
import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.qs.FooterActionsController
+import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel
import com.android.systemui.qs.ui.adapter.QSSceneAdapter
import com.android.systemui.scene.shared.model.Direction
import com.android.systemui.scene.shared.model.SceneKey
@@ -24,6 +27,7 @@
import com.android.systemui.scene.shared.model.UserAction
import com.android.systemui.shade.ui.viewmodel.ShadeHeaderViewModel
import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationsPlaceholderViewModel
+import java.util.concurrent.atomic.AtomicBoolean
import javax.inject.Inject
import kotlinx.coroutines.flow.map
@@ -35,6 +39,8 @@
val shadeHeaderViewModel: ShadeHeaderViewModel,
val qsSceneAdapter: QSSceneAdapter,
val notifications: NotificationsPlaceholderViewModel,
+ private val footerActionsViewModelFactory: FooterActionsViewModel.Factory,
+ private val footerActionsController: FooterActionsController,
) {
val destinationScenes =
qsSceneAdapter.isCustomizing.map { customizing ->
@@ -47,4 +53,13 @@
)
}
}
+
+ private val footerActionsControllerInitialized = AtomicBoolean(false)
+
+ fun getFooterActionsViewModel(lifecycleOwner: LifecycleOwner): FooterActionsViewModel {
+ if (footerActionsControllerInitialized.compareAndSet(false, true)) {
+ footerActionsController.init()
+ }
+ return footerActionsViewModelFactory.create(lifecycleOwner)
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
index fd53423..cc53aab 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
@@ -169,7 +169,6 @@
private final DisplayTracker mDisplayTracker;
private Region mActiveNavBarRegion;
- private SurfaceControl mNavigationBarSurface;
private IOverviewProxy mOverviewProxy;
private int mConnectionBackoffAttempts;
@@ -488,7 +487,6 @@
Log.e(TAG_OPS, "Failed to call onInitialize()", e);
}
dispatchNavButtonBounds();
- dispatchNavigationBarSurface();
// Force-update the systemui state flags
updateSystemUiStateFlags();
@@ -679,28 +677,6 @@
.commitUpdate(mContext.getDisplayId());
}
- /**
- * Called when the navigation bar surface is created or changed
- */
- public void onNavigationBarSurfaceChanged(SurfaceControl navbarSurface) {
- mNavigationBarSurface = navbarSurface;
- dispatchNavigationBarSurface();
- }
-
- private void dispatchNavigationBarSurface() {
- try {
- if (mOverviewProxy != null) {
- // Catch all for cases where the surface is no longer valid
- if (mNavigationBarSurface != null && !mNavigationBarSurface.isValid()) {
- mNavigationBarSurface = null;
- }
- mOverviewProxy.onNavigationBarSurface(mNavigationBarSurface);
- }
- } catch (RemoteException e) {
- Log.e(TAG_OPS, "Failed to notify back action", e);
- }
- }
-
private void updateEnabledAndBinding() {
updateEnabledState();
startConnectionToCurrentUser();
@@ -1075,7 +1051,6 @@
pw.print(" mInputFocusTransferStartY="); pw.println(mInputFocusTransferStartY);
pw.print(" mInputFocusTransferStartMillis="); pw.println(mInputFocusTransferStartMillis);
pw.print(" mActiveNavBarRegion="); pw.println(mActiveNavBarRegion);
- pw.print(" mNavigationBarSurface="); pw.println(mNavigationBarSurface);
pw.print(" mNavBarMode="); pw.println(mNavBarMode);
mSysUiState.dump(pw, args);
}
diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/WindowRootViewVisibilityInteractor.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/WindowRootViewVisibilityInteractor.kt
index e2959fe..1c37908 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/WindowRootViewVisibilityInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/WindowRootViewVisibilityInteractor.kt
@@ -23,16 +23,22 @@
import com.android.systemui.keyguard.shared.model.StatusBarState
import com.android.systemui.power.domain.interactor.PowerInteractor
import com.android.systemui.scene.data.repository.WindowRootViewVisibilityRepository
+import com.android.systemui.scene.shared.flag.SceneContainerFlags
+import com.android.systemui.scene.shared.model.ObservableTransitionState
+import com.android.systemui.scene.shared.model.SceneKey
import com.android.systemui.statusbar.NotificationPresenter
import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor
import com.android.systemui.statusbar.notification.init.NotificationsController
import com.android.systemui.statusbar.notification.shared.NotificationsLiveDataStoreRefactor
import com.android.systemui.statusbar.policy.HeadsUpManager
import javax.inject.Inject
+import javax.inject.Provider
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
@@ -47,6 +53,8 @@
private val headsUpManager: HeadsUpManager,
private val powerInteractor: PowerInteractor,
private val activeNotificationsInteractor: ActiveNotificationsInteractor,
+ sceneContainerFlags: SceneContainerFlags,
+ sceneInteractorProvider: Provider<SceneInteractor>,
) : CoreStartable {
private var notificationPresenter: NotificationPresenter? = null
@@ -58,11 +66,28 @@
/**
* True if lockscreen (including AOD) or the shade is visible and false otherwise. Notably,
* false if the bouncer is visible.
- *
- * TODO(b/297080059): Use [SceneInteractor] as the source of truth if the scene flag is on.
*/
val isLockscreenOrShadeVisible: StateFlow<Boolean> =
- windowRootViewVisibilityRepository.isLockscreenOrShadeVisible
+ if (!sceneContainerFlags.isEnabled()) {
+ windowRootViewVisibilityRepository.isLockscreenOrShadeVisible
+ } else {
+ sceneInteractorProvider
+ .get()
+ .transitionState
+ .map { state ->
+ when (state) {
+ is ObservableTransitionState.Idle ->
+ state.scene == SceneKey.Shade || state.scene == SceneKey.Lockscreen
+ is ObservableTransitionState.Transition ->
+ state.toScene == SceneKey.Shade ||
+ state.toScene == SceneKey.Lockscreen ||
+ state.fromScene == SceneKey.Shade ||
+ state.fromScene == SceneKey.Lockscreen
+ }
+ }
+ .distinctUntilChanged()
+ .stateIn(scope, SharingStarted.Eagerly, false)
+ }
/**
* True if lockscreen (including AOD) or the shade is visible **and** the user is currently
diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt
index c96651c..995059d 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt
@@ -313,7 +313,7 @@
}
applicationScope.launch {
- keyguardInteractor.isDozing.distinctUntilChanged().collect { isDozing ->
+ keyguardInteractor.isDozing.collect { isDozing ->
falsingCollector.setShowingAod(isDozing)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/BaseShadeControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/BaseShadeControllerImpl.kt
new file mode 100644
index 0000000..f71a401d
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/shade/BaseShadeControllerImpl.kt
@@ -0,0 +1,109 @@
+/*
+ * 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.shade
+
+import com.android.systemui.assist.AssistManager
+import com.android.systemui.log.LogBuffer
+import com.android.systemui.shade.TouchLogger.Companion.logTouchesTo
+import com.android.systemui.statusbar.CommandQueue
+import com.android.systemui.statusbar.NotificationPresenter
+import com.android.systemui.statusbar.NotificationShadeWindowController
+import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager
+import dagger.Lazy
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+
+/** A base class for non-empty implementations of ShadeController. */
+@OptIn(ExperimentalCoroutinesApi::class)
+abstract class BaseShadeControllerImpl(
+ private val touchLog: LogBuffer,
+ protected val commandQueue: CommandQueue,
+ protected val statusBarKeyguardViewManager: StatusBarKeyguardViewManager,
+ protected val notificationShadeWindowController: NotificationShadeWindowController,
+ protected val assistManagerLazy: Lazy<AssistManager>
+) : ShadeController {
+ protected lateinit var notifPresenter: NotificationPresenter
+ /** Runnables to run after completing a collapse of the shade. */
+ private val postCollapseActions = ArrayList<Runnable>()
+
+ override fun start() {
+ logTouchesTo(touchLog)
+ }
+
+ final override fun animateExpandShade() {
+ if (isShadeEnabled) {
+ expandToNotifications()
+ }
+ }
+
+ /** Expand the shade with notifications visible. */
+ protected abstract fun expandToNotifications()
+
+ final override fun animateExpandQs() {
+ if (isShadeEnabled) {
+ expandToQs()
+ }
+ }
+
+ /** Expand the shade showing only quick settings. */
+ protected abstract fun expandToQs()
+
+ final override fun addPostCollapseAction(action: Runnable) {
+ postCollapseActions.add(action)
+ }
+
+ protected fun runPostCollapseActions() {
+ val clonedList: ArrayList<Runnable> = ArrayList(postCollapseActions)
+ postCollapseActions.clear()
+ for (r in clonedList) {
+ r.run()
+ }
+ statusBarKeyguardViewManager.readyForKeyguardDone()
+ }
+
+ final override fun onLaunchAnimationEnd(launchIsFullScreen: Boolean) {
+ if (!this.notifPresenter.isCollapsing()) {
+ onClosingFinished()
+ }
+ if (launchIsFullScreen) {
+ instantCollapseShade()
+ }
+ }
+ final override fun onLaunchAnimationCancelled(isLaunchForActivity: Boolean) {
+ if (
+ notifPresenter.isPresenterFullyCollapsed() &&
+ !notifPresenter.isCollapsing() &&
+ isLaunchForActivity
+ ) {
+ onClosingFinished()
+ } else {
+ collapseShade(true /* animate */)
+ }
+ }
+
+ protected fun onClosingFinished() {
+ runPostCollapseActions()
+ if (!this.notifPresenter.isPresenterFullyCollapsed()) {
+ // if we set it not to be focusable when collapsing, we have to undo it when we aborted
+ // the closing
+ notificationShadeWindowController.setNotificationShadeFocusable(true)
+ }
+ }
+
+ override fun setNotificationPresenter(presenter: NotificationPresenter) {
+ notifPresenter = presenter
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt b/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt
index 782d651..97ec3f9 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt
@@ -22,6 +22,7 @@
import android.view.GestureDetector
import android.view.MotionEvent
import android.view.View
+import android.view.ViewGroup
import com.android.internal.annotations.VisibleForTesting
import com.android.systemui.communal.domain.interactor.CommunalInteractor
import com.android.systemui.communal.ui.viewmodel.CommunalViewModel
@@ -33,6 +34,7 @@
import com.android.systemui.shade.domain.interactor.ShadeInteractor
import com.android.systemui.util.kotlin.collectFlow
import javax.inject.Inject
+import kotlinx.coroutines.flow.StateFlow
/**
* Controller that's responsible for the glanceable hub container view and its touch handling.
@@ -49,13 +51,28 @@
private val powerManager: PowerManager,
) {
/** The container view for the hub. This will not be initialized until [initView] is called. */
- private lateinit var communalContainerView: View
+ private var communalContainerView: View? = null
/**
* The width of the area in which a right edge swipe can open the hub, in pixels. Read from
* resources when [initView] is called.
*/
- private var edgeSwipeRegionWidth: Int = 0
+ // TODO(b/320786721): support RTL layouts
+ private var rightEdgeSwipeRegionWidth: Int = 0
+
+ /**
+ * The height of the area in which a top edge swipe while the hub is open will not intercept
+ * touches, in pixels. This allows the top edge swipe to instead open the notification shade.
+ * Read from resources when [initView] is called.
+ */
+ private var topEdgeSwipeRegionWidth: Int = 0
+
+ /**
+ * The height of the area in which a bottom edge swipe while the hub is open will not intercept
+ * touches, in pixels. This allows the bottom edge swipe to instead open the bouncer. Read from
+ * resources when [initView] is called.
+ */
+ private var bottomEdgeSwipeRegionWidth: Int = 0
/**
* True if we are currently tracking a gesture for opening the hub that started in the edge
@@ -63,6 +80,9 @@
*/
private var isTrackingOpenGesture = false
+ /** True if we are currently tracking a touch on the hub while it's open. */
+ private var isTrackingHubTouch = false
+
/**
* True if the hub UI is fully open, meaning it should receive touch input.
*
@@ -90,6 +110,11 @@
return communalInteractor.isCommunalEnabled && isComposeAvailable()
}
+ /** Returns a {@link StateFlow} that tracks whether communal hub is enabled. */
+ fun enabledState(): StateFlow<Boolean> {
+ return communalInteractor.communalEnabledState
+ }
+
/**
* Creates the container view containing the glanceable hub UI.
*
@@ -107,32 +132,44 @@
if (!isEnabled()) {
throw RuntimeException("Glanceable hub is not enabled")
}
- if (::communalContainerView.isInitialized) {
+ if (communalContainerView != null) {
throw RuntimeException("Communal view has already been initialized")
}
communalContainerView = containerView
- edgeSwipeRegionWidth =
- communalContainerView.resources.getDimensionPixelSize(R.dimen.communal_grid_gutter_size)
+ rightEdgeSwipeRegionWidth =
+ containerView.resources.getDimensionPixelSize(
+ R.dimen.communal_right_edge_swipe_region_width
+ )
+ topEdgeSwipeRegionWidth =
+ containerView.resources.getDimensionPixelSize(
+ R.dimen.communal_top_edge_swipe_region_height
+ )
+ bottomEdgeSwipeRegionWidth =
+ containerView.resources.getDimensionPixelSize(
+ R.dimen.communal_bottom_edge_swipe_region_height
+ )
collectFlow(
- communalContainerView,
+ containerView,
keyguardTransitionInteractor.isFinishedInStateWhere(KeyguardState::isBouncerState),
{ anyBouncerShowing = it }
)
- collectFlow(
- communalContainerView,
- communalInteractor.isCommunalShowing,
- { hubShowing = it }
- )
- collectFlow(
- communalContainerView,
- shadeInteractor.isAnyFullyExpanded,
- { shadeShowing = it }
- )
+ collectFlow(containerView, communalInteractor.isCommunalShowing, { hubShowing = it })
+ collectFlow(containerView, shadeInteractor.isAnyFullyExpanded, { shadeShowing = it })
- return communalContainerView
+ communalContainerView = containerView
+
+ return containerView
+ }
+
+ /** Removes the container view from its parent. */
+ fun disposeView() {
+ communalContainerView?.let {
+ (it.parent as ViewGroup).removeView(it)
+ communalContainerView = null
+ }
}
/**
@@ -145,10 +182,10 @@
* to be fully in control of its own touch handling.
*/
fun onTouchEvent(ev: MotionEvent): Boolean {
- if (!::communalContainerView.isInitialized) {
- return false
- }
+ return communalContainerView?.let { handleTouchEventOnCommunalView(it, ev) } ?: false
+ }
+ private fun handleTouchEventOnCommunalView(view: View, ev: MotionEvent): Boolean {
val isDown = ev.actionMasked == MotionEvent.ACTION_DOWN
val isUp = ev.actionMasked == MotionEvent.ACTION_UP
val isCancel = ev.actionMasked == MotionEvent.ACTION_CANCEL
@@ -157,28 +194,48 @@
// fully showing state
val hubOccluded = anyBouncerShowing || shadeShowing
- // If the hub is fully visible, send all touch events to it.
- val communalVisible = hubShowing && !hubOccluded
- if (communalVisible) {
- dispatchTouchEvent(ev)
+ // If the hub is fully visible, send all touch events to it, other than top and bottom edge
+ // swipes.
+ if (hubShowing && isDown) {
+ val y = ev.rawY
+ val topSwipe: Boolean = y <= topEdgeSwipeRegionWidth
+ val bottomSwipe = y >= view.height - bottomEdgeSwipeRegionWidth
+
+ if (topSwipe || bottomSwipe) {
+ // Don't intercept touches at the top/bottom edge so that swipes can open the
+ // notification shade and bouncer.
+ return false
+ }
+
+ if (!hubOccluded) {
+ isTrackingHubTouch = true
+ dispatchTouchEvent(view, ev)
+ // Return true regardless of dispatch result as some touches at the start of a
+ // gesture may return false from dispatchTouchEvent.
+ return true
+ }
+ } else if (isTrackingHubTouch) {
+ if (isUp || isCancel) {
+ isTrackingHubTouch = false
+ }
+ dispatchTouchEvent(view, ev)
// Return true regardless of dispatch result as some touches at the start of a gesture
// may return false from dispatchTouchEvent.
return true
}
- if (edgeSwipeRegionWidth == 0) {
- // If the edge region width has not been read yet or whatever reason, don't bother
+ if (rightEdgeSwipeRegionWidth == 0) {
+ // If the edge region width has not been read yet for whatever reason, don't bother
// intercepting touches to open the hub.
return false
}
if (!isTrackingOpenGesture && isDown) {
val x = ev.rawX
- val inOpeningSwipeRegion: Boolean =
- x >= communalContainerView.width - edgeSwipeRegionWidth
+ val inOpeningSwipeRegion: Boolean = x >= view.width - rightEdgeSwipeRegionWidth
if (inOpeningSwipeRegion && !hubOccluded) {
isTrackingOpenGesture = true
- dispatchTouchEvent(ev)
+ dispatchTouchEvent(view, ev)
// Return true regardless of dispatch result as some touches at the start of a
// gesture may return false from dispatchTouchEvent.
return true
@@ -187,7 +244,7 @@
if (isUp || isCancel) {
isTrackingOpenGesture = false
}
- dispatchTouchEvent(ev)
+ dispatchTouchEvent(view, ev)
// Return true regardless of dispatch result as some touches at the start of a gesture
// may return false from dispatchTouchEvent.
return true
@@ -200,8 +257,8 @@
* Dispatches the touch event to the communal container and sends a user activity event to reset
* the screen timeout.
*/
- private fun dispatchTouchEvent(ev: MotionEvent) {
- communalContainerView.dispatchTouchEvent(ev)
+ private fun dispatchTouchEvent(view: View, ev: MotionEvent) {
+ view.dispatchTouchEvent(ev)
powerManager.userActivity(
SystemClock.uptimeMillis(),
PowerManager.USER_ACTIVITY_EVENT_TOUCH,
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
index aeccf00..530c124 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
@@ -2883,7 +2883,9 @@
private void onTrackingStarted() {
endClosing();
mShadeRepository.setLegacyShadeTracking(true);
- mTrackingStartedListener.onTrackingStarted();
+ if (mTrackingStartedListener != null) {
+ mTrackingStartedListener.onTrackingStarted();
+ }
notifyExpandingStarted();
updateExpansionAndVisibility();
mScrimController.onTrackingStarted();
@@ -3574,11 +3576,6 @@
}
@Override
- public NotificationStackScrollLayoutController getNotificationStackScrollLayoutController() {
- return mNotificationStackScrollLayoutController;
- }
-
- @Override
public void disableHeader(int state1, int state2, boolean animated) {
mShadeHeaderController.disable(state1, state2, animated);
}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java
index 60feb82..0053474 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java
@@ -50,6 +50,7 @@
import com.android.systemui.Dumpable;
import com.android.systemui.biometrics.AuthController;
import com.android.systemui.colorextraction.SysuiColorExtractor;
+import com.android.systemui.communal.domain.interactor.CommunalInteractor;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.dagger.qualifiers.Background;
import com.android.systemui.dagger.qualifiers.Main;
@@ -118,6 +119,7 @@
private final Lazy<SelectedUserInteractor> mUserInteractor;
private final Lazy<ShadeInteractor> mShadeInteractorLazy;
private final SceneContainerFlags mSceneContainerFlags;
+ private final Lazy<CommunalInteractor> mCommunalInteractor;
private ViewGroup mWindowRootView;
private LayoutParams mLp;
private boolean mHasTopUi;
@@ -165,7 +167,8 @@
ShadeWindowLogger logger,
Lazy<SelectedUserInteractor> userInteractor,
UserTracker userTracker,
- SceneContainerFlags sceneContainerFlags) {
+ SceneContainerFlags sceneContainerFlags,
+ Lazy<CommunalInteractor> communalInteractor) {
mContext = context;
mWindowRootViewComponentFactory = windowRootViewComponentFactory;
mWindowManager = windowManager;
@@ -184,6 +187,7 @@
mAuthController = authController;
mUserInteractor = userInteractor;
mSceneContainerFlags = sceneContainerFlags;
+ mCommunalInteractor = communalInteractor;
mLastKeyguardRotationAllowed = mKeyguardStateController.isKeyguardScreenRotationAllowed();
mLockScreenDisplayTimeout = context.getResources()
.getInteger(R.integer.config_lockScreenDisplayTimeout);
@@ -325,6 +329,11 @@
mShadeInteractorLazy.get().isQsExpanded(),
this::onQsExpansionChanged
);
+ collectFlow(
+ mWindowRootView,
+ mCommunalInteractor.get().isCommunalShowing(),
+ this::onCommunalShowingChanged
+ );
}
@Override
@@ -501,14 +510,21 @@
}
private void applyUserActivityTimeout(NotificationShadeWindowState state) {
- if (state.isKeyguardShowingAndNotOccluded()
+ final Boolean communalShowing = state.isCommunalShowingAndNotOccluded();
+ final Boolean keyguardShowing = state.isKeyguardShowingAndNotOccluded();
+ long timeout = -1;
+ if ((communalShowing || keyguardShowing)
&& state.statusBarState == StatusBarState.KEYGUARD
&& !state.qsExpanded) {
- mLpChanged.userActivityTimeout = state.bouncerShowing
- ? KeyguardViewMediator.AWAKE_INTERVAL_BOUNCER_MS : mLockScreenDisplayTimeout;
- } else {
- mLpChanged.userActivityTimeout = -1;
+ if (state.bouncerShowing) {
+ timeout = KeyguardViewMediator.AWAKE_INTERVAL_BOUNCER_MS;
+ } else if (communalShowing) {
+ timeout = CommunalInteractor.AWAKE_INTERVAL_MS;
+ } else if (keyguardShowing) {
+ timeout = mLockScreenDisplayTimeout;
+ }
}
+ mLpChanged.userActivityTimeout = timeout;
}
private void applyInputFeatures(NotificationShadeWindowState state) {
@@ -607,7 +623,8 @@
state.forcePluginOpen,
state.dozing,
state.scrimsVisibility,
- state.backgroundBlurRadius
+ state.backgroundBlurRadius,
+ state.communalShowing
);
}
@@ -731,6 +748,12 @@
apply(mCurrentState);
}
+ @VisibleForTesting
+ void onCommunalShowingChanged(Boolean showing) {
+ mCurrentState.communalShowing = showing;
+ apply(mCurrentState);
+ }
+
@Override
public void setForceUserActivity(boolean forceUserActivity) {
mCurrentState.forceUserActivity = forceUserActivity;
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowState.kt b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowState.kt
index 0b20170..f9c9d83 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowState.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowState.kt
@@ -58,12 +58,17 @@
@JvmField var dreaming: Boolean = false,
@JvmField var scrimsVisibility: Int = 0,
@JvmField var backgroundBlurRadius: Int = 0,
+ @JvmField var communalShowing: Boolean = false,
) {
fun isKeyguardShowingAndNotOccluded(): Boolean {
return keyguardShowing && !keyguardOccluded
}
+ fun isCommunalShowingAndNotOccluded(): Boolean {
+ return communalShowing && !keyguardOccluded
+ }
+
/** List of [String] to be used as a [Row] with [DumpsysTableLogger]. */
val asStringList: List<String> by lazy {
listOf(
@@ -93,7 +98,8 @@
forcePluginOpen.toString(),
dozing.toString(),
scrimsVisibility.toString(),
- backgroundBlurRadius.toString()
+ backgroundBlurRadius.toString(),
+ communalShowing.toString(),
)
}
@@ -134,6 +140,7 @@
dozing: Boolean,
scrimsVisibility: Int,
backgroundBlurRadius: Int,
+ communalShowing: Boolean,
) {
buffer.advance().apply {
this.keyguardShowing = keyguardShowing
@@ -165,6 +172,7 @@
this.dozing = dozing
this.scrimsVisibility = scrimsVisibility
this.backgroundBlurRadius = backgroundBlurRadius
+ this.communalShowing = communalShowing
}
}
@@ -209,7 +217,8 @@
"forcePluginOpen",
"dozing",
"scrimsVisibility",
- "backgroundBlurRadius"
+ "backgroundBlurRadius",
+ "communalShowing"
)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java
index 8c852cd..5ecc54b 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java
@@ -272,6 +272,14 @@
return result;
}
+ /**
+ * Handle a touch event while dreaming by forwarding the event to the content view.
+ * @param event The event to forward.
+ */
+ public void handleDreamTouch(MotionEvent event) {
+ mView.dispatchTouchEvent(event);
+ }
+
/** Inflates the {@link R.layout#status_bar_expanded} layout and sets it up. */
public void setupExpandedStatusBar() {
mStackScrollLayout = mView.findViewById(R.id.notification_stack_scroller);
@@ -597,16 +605,21 @@
* The layout lives in {@link R.id.communal_ui_stub}.
*/
public void setupCommunalHubLayout() {
- if (!mGlanceableHubContainerController.isEnabled()) {
- return;
- }
-
- // Replace the placeholder view with the communal UI.
- View communalPlaceholder = mView.findViewById(R.id.communal_ui_stub);
- int index = mView.indexOfChild(communalPlaceholder);
- mView.removeView(communalPlaceholder);
-
- mView.addView(mGlanceableHubContainerController.initView(mView.getContext()), index);
+ collectFlow(
+ mView,
+ mGlanceableHubContainerController.enabledState(),
+ isEnabled -> {
+ if (isEnabled) {
+ View communalPlaceholder = mView.findViewById(R.id.communal_ui_stub);
+ int index = mView.indexOfChild(communalPlaceholder);
+ mView.addView(
+ mGlanceableHubContainerController.initView(mView.getContext()),
+ index);
+ } else {
+ mGlanceableHubContainerController.disposeView();
+ }
+ }
+ );
}
private boolean didNotificationPanelInterceptEvent(MotionEvent ev) {
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeController.java b/packages/SystemUI/src/com/android/systemui/shade/ShadeController.java
index 2c4b0b9..ec4b23a 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeController.java
@@ -33,10 +33,20 @@
* {@link com.android.systemui.keyguard.KeyguardViewMediator} and others.
*/
public interface ShadeController extends CoreStartable {
- /** True if the shade UI is enabled on this particular Android variant and false otherwise. */
+ /**
+ * True if the shade UI is enabled on this particular Android variant and false otherwise.
+ *
+ * @deprecated use ShadeInteractor instead
+ */
+ @Deprecated
boolean isShadeEnabled();
- /** Make our window larger and the shade expanded */
+ /**
+ * Make our window larger and the shade expanded
+ *
+ * @deprecated will no longer be needed when keyguard is a sibling view to the shade
+ */
+ @Deprecated
void instantExpandShade();
/** Collapse the shade instantly with no animation. */
@@ -74,13 +84,28 @@
/** Expand the shade with quick settings expanded with an animation. */
void animateExpandQs();
- /** Posts a request to collapse the shade. */
+ /**
+ * Posts a request to collapse the shade.
+ *
+ * @deprecated use #animateCollapseShade
+ */
+ @Deprecated
void postAnimateCollapseShade();
- /** Posts a request to force collapse the shade. */
+ /**
+ * Posts a request to force collapse the shade.
+ *
+ * @deprecated use #animateForceCollapseShade
+ */
+ @Deprecated
void postAnimateForceCollapseShade();
- /** Posts a request to expand the shade to quick settings. */
+ /**
+ * Posts a request to expand the shade to quick settings.
+ *
+ * @deprecated use #animateExpandQs
+ */
+ @Deprecated
void postAnimateExpandQs();
/** Cancels any ongoing expansion touch handling and collapses the shade. */
@@ -90,26 +115,29 @@
* If the shade is not fully expanded, collapse it animated.
*
* @return Seems to always return false
+ * @deprecated use {@link #collapseShade()} instead
*/
+ @Deprecated
boolean closeShadeIfOpen();
/**
- * Returns whether the shade state is the keyguard or not.
- */
- boolean isKeyguard();
-
- /**
* Returns whether the shade is currently open.
* Even though in the current implementation shade is in expanded state on keyguard, this
* method makes distinction between shade being truly open and plain keyguard state:
* - if QS and notifications are visible on the screen, return true
* - for any other state, including keyguard, return false
+ *
+ * @deprecated will be replaced by ShadeInteractor once scene container launches
*/
+ @Deprecated
boolean isShadeFullyOpen();
/**
* Returns whether shade or QS are currently opening or collapsing.
+ *
+ * @deprecated will be replaced by ShadeInteractor once scene container launches
*/
+ @Deprecated
boolean isExpandingOrCollapsing();
/**
@@ -127,37 +155,67 @@
*/
void addPostCollapseAction(Runnable action);
- /** Run all of the runnables added by {@link #addPostCollapseAction}. */
- void runPostCollapseRunnables();
-
/**
* Close the shade if it was open
*
* @return true if the shade was open, else false
*/
- boolean collapseShade();
+ void collapseShade();
/**
* If animate is true, does the same as {@link #collapseShade()}. Otherwise, instantly collapse
* the shade. Post collapse runnables will be executed
*
* @param animate true to animate the collapse, false for instantaneous collapse
+ * @deprecated call either #animateCollapseShade or #instantCollapseShade
*/
+ @Deprecated
void collapseShade(boolean animate);
- /** Calls #collapseShade if already on the main thread. If not, posts a call to it. */
+ /**
+ * Calls #collapseShade if already on the main thread. If not, posts a call to it.
+ * @deprecated call #collapseShade
+ */
+ @Deprecated
void collapseOnMainThread();
- /** Makes shade expanded but not visible. */
+ /**
+ * If necessary, instantly collapses the shade for an activity start, otherwise runs the
+ * post-collapse runnables. Instant collapse is ok here, because the purpose is to have the
+ * shade collapsed when the user returns to SysUI from the launched activity.
+ */
+ void collapseShadeForActivityStart();
+
+ /**
+ * Makes shade expanded but not visible.
+ *
+ * @deprecated no longer needed once keyguard is a sibling view to the shade
+ */
+ @Deprecated
void makeExpandedInvisible();
- /** Makes shade expanded and visible. */
+ /**
+ * Makes shade expanded and visible.
+ *
+ * @deprecated no longer needed once keyguard is a sibling view to the shade
+ */
+ @Deprecated
void makeExpandedVisible(boolean force);
- /** Returns whether the shade is expanded and visible. */
+ /**
+ * Returns whether the shade is expanded and visible.
+ *
+ * @deprecated no longer needed once keyguard is a sibling view to the shade
+ */
+ @Deprecated
boolean isExpandedVisible();
- /** Handle status bar touch event. */
+ /**
+ * Handle status bar touch event.
+ *
+ * @deprecated only called by CentralSurfaces, which is being deleted
+ */
+ @Deprecated
void onStatusBarTouch(MotionEvent event);
/** Called when a launch animation was cancelled. */
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerEmptyImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerEmptyImpl.kt
index 82959ee..08a0c93 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerEmptyImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerEmptyImpl.kt
@@ -42,9 +42,6 @@
override fun closeShadeIfOpen(): Boolean {
return false
}
- override fun isKeyguard(): Boolean {
- return false
- }
override fun isShadeFullyOpen(): Boolean {
return false
}
@@ -53,12 +50,10 @@
}
override fun postOnShadeExpanded(action: Runnable?) {}
override fun addPostCollapseAction(action: Runnable?) {}
- override fun runPostCollapseRunnables() {}
- override fun collapseShade(): Boolean {
- return false
- }
+ override fun collapseShade() {}
override fun collapseShade(animate: Boolean) {}
override fun collapseOnMainThread() {}
+ override fun collapseShadeForActivityStart() {}
override fun makeExpandedInvisible() {}
override fun makeExpandedVisible(force: Boolean) {}
override fun isExpandedVisible(): Boolean {
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerImpl.java b/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerImpl.java
index fdc7eec..e8d9c35 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerImpl.java
@@ -33,7 +33,6 @@
import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.scene.domain.interactor.WindowRootViewVisibilityInteractor;
import com.android.systemui.statusbar.CommandQueue;
-import com.android.systemui.statusbar.NotificationPresenter;
import com.android.systemui.statusbar.NotificationShadeWindowController;
import com.android.systemui.statusbar.StatusBarState;
import com.android.systemui.statusbar.notification.row.NotificationGutsManager;
@@ -44,14 +43,13 @@
import dagger.Lazy;
-import java.util.ArrayList;
import java.util.concurrent.Executor;
import javax.inject.Inject;
/** An implementation of {@link ShadeController}. */
@SysUISingleton
-public final class ShadeControllerImpl implements ShadeController {
+public final class ShadeControllerImpl extends BaseShadeControllerImpl {
private static final String TAG = "ShadeControllerImpl";
private static final boolean SPEW = false;
@@ -60,7 +58,6 @@
private final CommandQueue mCommandQueue;
private final Executor mMainExecutor;
- private final LogBuffer mTouchLog;
private final WindowRootViewVisibilityInteractor mWindowRootViewVisibilityInteractor;
private final KeyguardStateController mKeyguardStateController;
private final NotificationShadeWindowController mNotificationShadeWindowController;
@@ -73,12 +70,9 @@
private final Lazy<AssistManager> mAssistManagerLazy;
private final Lazy<NotificationGutsManager> mGutsManager;
- private final ArrayList<Runnable> mPostCollapseRunnables = new ArrayList<>();
-
private boolean mExpandedVisible;
private boolean mLockscreenOrShadeVisible;
- private NotificationPresenter mPresenter;
private NotificationShadeWindowViewController mNotificationShadeWindowViewController;
private ShadeVisibilityListener mShadeVisibilityListener;
@@ -99,9 +93,13 @@
Lazy<AssistManager> assistManagerLazy,
Lazy<NotificationGutsManager> gutsManager
) {
+ super(touchLog,
+ commandQueue,
+ statusBarKeyguardViewManager,
+ notificationShadeWindowController,
+ assistManagerLazy);
mCommandQueue = commandQueue;
mMainExecutor = mainExecutor;
- mTouchLog = touchLog;
mWindowRootViewVisibilityInteractor = windowRootViewVisibilityInteractor;
mShadeViewControllerLazy = shadeViewControllerLazy;
mStatusBarStateController = statusBarStateController;
@@ -117,7 +115,7 @@
@Override
public boolean isShadeEnabled() {
- return true;
+ return mCommandQueue.panelsEnabled() && mDeviceProvisionedController.isCurrentUserSetup();
}
@Override
@@ -125,20 +123,16 @@
// Make our window larger and the panel expanded.
makeExpandedVisible(true /* force */);
getShadeViewController().expand(false /* animate */);
- mCommandQueue.recomputeDisableFlags(mDisplayId, false /* animate */);
+ getCommandQueue().recomputeDisableFlags(mDisplayId, false /* animate */);
}
@Override
public void animateCollapseShade(int flags, boolean force, boolean delayed,
float speedUpFactor) {
if (!force && mStatusBarStateController.getState() != StatusBarState.SHADE) {
- runPostCollapseRunnables();
+ runPostCollapseActions();
return;
}
- if (SPEW) {
- Log.d(TAG,
- "animateCollapse(): mExpandedVisible=" + mExpandedVisible + "flags=" + flags);
- }
if (getNotificationShadeWindowView() != null
&& getShadeViewController().canBeCollapsed()
&& (flags & CommandQueue.FLAG_EXCLUDE_NOTIFICATION_PANEL) == 0) {
@@ -151,28 +145,19 @@
}
@Override
- public void animateExpandShade() {
- if (!mCommandQueue.panelsEnabled()) {
- return;
- }
+ protected void expandToNotifications() {
getShadeViewController().expandToNotifications();
}
@Override
- public void animateExpandQs() {
- if (!mCommandQueue.panelsEnabled()) {
- return;
- }
- // Settings are not available in setup
- if (!mDeviceProvisionedController.isCurrentUserSetup()) return;
-
+ protected void expandToQs() {
getShadeViewController().expandToQs();
}
@Override
public boolean closeShadeIfOpen() {
if (!getShadeViewController().isFullyCollapsed()) {
- mCommandQueue.animateCollapsePanels(
+ getCommandQueue().animateCollapsePanels(
CommandQueue.FLAG_EXCLUDE_RECENTS_PANEL, true /* force */);
notifyVisibilityChanged(false);
mAssistManagerLazy.get().hideAssist();
@@ -181,11 +166,6 @@
}
@Override
- public boolean isKeyguard() {
- return mStatusBarStateController.getState() == StatusBarState.KEYGUARD;
- }
-
- @Override
public boolean isShadeFullyOpen() {
return getShadeViewController().isShadeFullyExpanded();
}
@@ -224,46 +204,34 @@
}
@Override
- public void addPostCollapseAction(Runnable action) {
- mPostCollapseRunnables.add(action);
+ public void collapseShade() {
+ collapseShadeInternal();
}
- @Override
- public void runPostCollapseRunnables() {
- ArrayList<Runnable> clonedList = new ArrayList<>(mPostCollapseRunnables);
- mPostCollapseRunnables.clear();
- int size = clonedList.size();
- for (int i = 0; i < size; i++) {
- clonedList.get(i).run();
- }
- mStatusBarKeyguardViewManager.readyForKeyguardDone();
- }
-
- @Override
- public boolean collapseShade() {
+ private boolean collapseShadeInternal() {
if (!getShadeViewController().isFullyCollapsed()) {
// close the shade if it was open
animateCollapseShadeForcedDelayed();
notifyVisibilityChanged(false);
-
return true;
} else {
return false;
}
}
+
@Override
public void collapseShade(boolean animate) {
if (animate) {
- boolean willCollapse = collapseShade();
+ boolean willCollapse = collapseShadeInternal();
if (!willCollapse) {
- runPostCollapseRunnables();
+ runPostCollapseActions();
}
- } else if (!mPresenter.isPresenterFullyCollapsed()) {
+ } else if (!getNotifPresenter().isPresenterFullyCollapsed()) {
instantCollapseShade();
notifyVisibilityChanged(false);
} else {
- runPostCollapseRunnables();
+ runPostCollapseActions();
}
}
@@ -296,46 +264,16 @@
}
}
- private void onClosingFinished() {
- runPostCollapseRunnables();
- if (!mPresenter.isPresenterFullyCollapsed()) {
- // if we set it not to be focusable when collapsing, we have to undo it when we aborted
- // the closing
- mNotificationShadeWindowController.setNotificationShadeFocusable(true);
- }
- }
-
- @Override
- public void onLaunchAnimationCancelled(boolean isLaunchForActivity) {
- if (mPresenter.isPresenterFullyCollapsed()
- && !mPresenter.isCollapsing()
- && isLaunchForActivity) {
- onClosingFinished();
- } else {
- collapseShade(true /* animate */);
- }
- }
-
- @Override
- public void onLaunchAnimationEnd(boolean launchIsFullScreen) {
- if (!mPresenter.isCollapsing()) {
- onClosingFinished();
- }
- if (launchIsFullScreen) {
- instantCollapseShade();
- }
- }
-
@Override
public void instantCollapseShade() {
getShadeViewController().instantCollapse();
- runPostCollapseRunnables();
+ runPostCollapseActions();
}
@Override
public void makeExpandedVisible(boolean force) {
if (SPEW) Log.d(TAG, "Make expanded visible: expanded visible=" + mExpandedVisible);
- if (!force && (mExpandedVisible || !mCommandQueue.panelsEnabled())) {
+ if (!force && (mExpandedVisible || !getCommandQueue().panelsEnabled())) {
return;
}
@@ -346,7 +284,7 @@
mNotificationShadeWindowController.setPanelVisible(true);
notifyVisibilityChanged(true);
- mCommandQueue.recomputeDisableFlags(mDisplayId, !force /* animate */);
+ getCommandQueue().recomputeDisableFlags(mDisplayId, !force /* animate */);
notifyExpandedVisibleChanged(true);
}
@@ -377,9 +315,9 @@
-1 /* y */,
true /* resetMenu */);
- runPostCollapseRunnables();
+ runPostCollapseActions();
notifyExpandedVisibleChanged(false);
- mCommandQueue.recomputeDisableFlags(
+ getCommandQueue().recomputeDisableFlags(
mDisplayId,
getShadeViewController().shouldHideStatusBarIconsWhenExpanded());
@@ -421,11 +359,6 @@
}
@Override
- public void setNotificationPresenter(NotificationPresenter presenter) {
- mPresenter = presenter;
- }
-
- @Override
public void setNotificationShadeWindowViewController(
NotificationShadeWindowViewController controller) {
mNotificationShadeWindowViewController = controller;
@@ -441,8 +374,8 @@
@Override
public void start() {
- TouchLogger.logTouchesTo(mTouchLog);
- getShadeViewController().setTrackingStartedListener(this::runPostCollapseRunnables);
+ super.start();
+ getShadeViewController().setTrackingStartedListener(this::runPostCollapseActions);
getShadeViewController().setOpenCloseListener(
new OpenCloseListener() {
@Override
@@ -456,4 +389,16 @@
}
});
}
+
+ @Override
+ public void collapseShadeForActivityStart() {
+ if (isExpandedVisible() && !mStatusBarKeyguardViewManager.isBouncerShowing()) {
+ animateCollapseShadeForcedDelayed();
+ } else {
+ // Do it after DismissAction has been processed to conserve the
+ // needed ordering.
+ mMainExecutor.execute(this::runPostCollapseActions);
+ }
+ }
+
}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt
new file mode 100644
index 0000000..10b9db0
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt
@@ -0,0 +1,245 @@
+/*
+ * 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.shade
+
+import android.view.MotionEvent
+import com.android.systemui.assist.AssistManager
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor
+import com.android.systemui.log.LogBuffer
+import com.android.systemui.log.dagger.ShadeTouchLog
+import com.android.systemui.scene.domain.interactor.SceneInteractor
+import com.android.systemui.scene.shared.model.SceneKey
+import com.android.systemui.scene.shared.model.SceneModel
+import com.android.systemui.shade.ShadeController.ShadeVisibilityListener
+import com.android.systemui.shade.domain.interactor.ShadeInteractor
+import com.android.systemui.statusbar.CommandQueue
+import com.android.systemui.statusbar.NotificationShadeWindowController
+import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout
+import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager
+import dagger.Lazy
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.launch
+
+/**
+ * Implementation of ShadeController backed by scenes instead of NPVC.
+ *
+ * TODO(b/300258424) rename to ShadeControllerImpl and inline/delete all the deprecated methods
+ */
+@OptIn(ExperimentalCoroutinesApi::class)
+@SysUISingleton
+class ShadeControllerSceneImpl
+@Inject
+constructor(
+ @Background private val scope: CoroutineScope,
+ private val shadeInteractor: ShadeInteractor,
+ private val sceneInteractor: SceneInteractor,
+ private val deviceEntryInteractor: DeviceEntryInteractor,
+ private val notificationStackScrollLayout: NotificationStackScrollLayout,
+ @ShadeTouchLog private val touchLog: LogBuffer,
+ commandQueue: CommandQueue,
+ statusBarKeyguardViewManager: StatusBarKeyguardViewManager,
+ notificationShadeWindowController: NotificationShadeWindowController,
+ assistManagerLazy: Lazy<AssistManager>,
+) :
+ BaseShadeControllerImpl(
+ touchLog,
+ commandQueue,
+ statusBarKeyguardViewManager,
+ notificationShadeWindowController,
+ assistManagerLazy,
+ ) {
+
+ init {
+ scope.launch {
+ shadeInteractor.isAnyExpanded.collect {
+ if (!it) {
+ runPostCollapseActions()
+ }
+ }
+ }
+ }
+
+ override fun isShadeEnabled() = shadeInteractor.isShadeEnabled.value
+
+ override fun isShadeFullyOpen(): Boolean = shadeInteractor.isAnyFullyExpanded.value
+
+ override fun isExpandingOrCollapsing(): Boolean =
+ shadeInteractor.anyExpansion.value > 0f && shadeInteractor.anyExpansion.value < 1f
+
+ override fun instantExpandShade() {
+ // Do nothing
+ }
+
+ override fun instantCollapseShade() {
+ // TODO(b/315921512) add support for instant transition
+ sceneInteractor.changeScene(
+ SceneModel(getCollapseDestinationScene(), "instant"),
+ "hide shade"
+ )
+ }
+
+ override fun animateCollapseShade(
+ flags: Int,
+ force: Boolean,
+ delayed: Boolean,
+ speedUpFactor: Float
+ ) {
+ if (!force && !shadeInteractor.isAnyExpanded.value) {
+ runPostCollapseActions()
+ return
+ }
+ if (
+ shadeInteractor.isAnyExpanded.value &&
+ flags and CommandQueue.FLAG_EXCLUDE_NOTIFICATION_PANEL == 0
+ ) {
+ // release focus immediately to kick off focus change transition
+ notificationShadeWindowController.setNotificationShadeFocusable(false)
+ notificationStackScrollLayout.cancelExpandHelper()
+ sceneInteractor.changeScene(
+ SceneModel(SceneKey.Shade, null),
+ "ShadeController.animateExpandShade"
+ )
+ if (delayed) {
+ scope.launch {
+ delay(125)
+ animateCollapseShadeInternal()
+ }
+ } else {
+ animateCollapseShadeInternal()
+ }
+ }
+ }
+
+ private fun animateCollapseShadeInternal() {
+ sceneInteractor.changeScene(
+ SceneModel(getCollapseDestinationScene(), "ShadeController.animateCollapseShade"),
+ "ShadeController.animateCollapseShade"
+ )
+ }
+
+ private fun getCollapseDestinationScene(): SceneKey {
+ return if (deviceEntryInteractor.isDeviceEntered.value) {
+ SceneKey.Gone
+ } else {
+ SceneKey.Lockscreen
+ }
+ }
+
+ override fun cancelExpansionAndCollapseShade() {
+ // TODO do we need to actually cancel the touch session?
+ animateCollapseShade()
+ }
+
+ override fun closeShadeIfOpen(): Boolean {
+ if (shadeInteractor.isAnyExpanded.value) {
+ commandQueue.animateCollapsePanels(
+ CommandQueue.FLAG_EXCLUDE_RECENTS_PANEL,
+ true /* force */
+ )
+ assistManagerLazy.get().hideAssist()
+ }
+ return false
+ }
+
+ override fun collapseShade() {
+ animateCollapseShadeForcedDelayed()
+ }
+
+ override fun collapseShade(animate: Boolean) {
+ if (animate) {
+ animateCollapseShade()
+ } else {
+ instantCollapseShade()
+ }
+ }
+
+ override fun collapseOnMainThread() {
+ // TODO if this works with delegation alone, we can deprecate and delete
+ collapseShade()
+ }
+
+ override fun expandToNotifications() {
+ sceneInteractor.changeScene(
+ SceneModel(SceneKey.Shade, null),
+ "ShadeController.animateExpandShade"
+ )
+ }
+
+ override fun expandToQs() {
+ sceneInteractor.changeScene(
+ SceneModel(SceneKey.QuickSettings, null),
+ "ShadeController.animateExpandQs"
+ )
+ }
+
+ override fun setVisibilityListener(listener: ShadeVisibilityListener) {
+ scope.launch { sceneInteractor.isVisible.collect { listener.expandedVisibleChanged(it) } }
+ }
+
+ @ExperimentalCoroutinesApi
+ override fun collapseShadeForActivityStart() {
+ if (shadeInteractor.isAnyExpanded.value) {
+ animateCollapseShadeForcedDelayed()
+ } else {
+ runPostCollapseActions()
+ }
+ }
+
+ override fun postAnimateCollapseShade() {
+ animateCollapseShade()
+ }
+
+ override fun postAnimateForceCollapseShade() {
+ animateCollapseShadeForced()
+ }
+
+ override fun postAnimateExpandQs() {
+ expandToQs()
+ }
+
+ override fun postOnShadeExpanded(action: Runnable) {
+ // TODO verify that clicking "reply" in a work profile notification launches the app
+ // TODO verify that there's not a way to replace and deprecate this method
+ scope.launch {
+ shadeInteractor.isAnyFullyExpanded.first { it }
+ action.run()
+ }
+ }
+
+ override fun makeExpandedInvisible() {
+ // Do nothing
+ }
+
+ override fun makeExpandedVisible(force: Boolean) {
+ // Do nothing
+ }
+
+ override fun isExpandedVisible(): Boolean {
+ return sceneInteractor.desiredScene.value.key != SceneKey.Gone
+ }
+
+ override fun onStatusBarTouch(event: MotionEvent) {
+ // The only call to this doesn't happen with KeyguardShadeMigrationNssl enabled
+ throw UnsupportedOperationException()
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt
index c057147..fc2c3ee 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt
@@ -53,6 +53,20 @@
@Provides
@SysUISingleton
+ fun provideShadeController(
+ sceneContainerFlags: SceneContainerFlags,
+ sceneContainerOn: Provider<ShadeControllerSceneImpl>,
+ sceneContainerOff: Provider<ShadeControllerImpl>
+ ): ShadeController {
+ return if (sceneContainerFlags.isEnabled()) {
+ sceneContainerOn.get()
+ } else {
+ sceneContainerOff.get()
+ }
+ }
+
+ @Provides
+ @SysUISingleton
fun provideShadeAnimationInteractor(
sceneContainerFlags: SceneContainerFlags,
sceneContainerOn: Provider<ShadeAnimationInteractorSceneContainerImpl>,
@@ -79,8 +93,4 @@
abstract fun bindsShadeViewController(
notificationPanelViewController: NotificationPanelViewController
): ShadeViewController
-
- @Binds
- @SysUISingleton
- abstract fun bindsShadeController(shadeControllerImpl: ShadeControllerImpl): ShadeController
}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeSurface.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeSurface.kt
index e54286f..4f970b3 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeSurface.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeSurface.kt
@@ -17,7 +17,6 @@
import android.view.ViewPropertyAnimator
import com.android.systemui.statusbar.GestureRecorder
-import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController
import com.android.systemui.statusbar.phone.CentralSurfaces
import com.android.systemui.statusbar.policy.HeadsUpManager
@@ -44,9 +43,6 @@
/** Animates the view from its current alpha to zero then runs the runnable. */
fun fadeOut(startDelayMs: Long, durationMs: Long, endAction: Runnable): ViewPropertyAnimator
- /** Returns the NSSL controller. */
- val notificationStackScrollLayoutController: NotificationStackScrollLayoutController
-
/** Set whether the bouncer is showing. */
fun setBouncerShowing(bouncerShowing: Boolean)
diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt
index 31a4de4..43ede2a 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt
@@ -29,7 +29,7 @@
val isShadeEnabled: StateFlow<Boolean>
/** Whether either the shade or QS is fully expanded. */
- val isAnyFullyExpanded: Flow<Boolean>
+ val isAnyFullyExpanded: StateFlow<Boolean>
/** Whether the Shade is fully expanded. */
val isShadeFullyExpanded: Flow<Boolean>
diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorEmptyImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorEmptyImpl.kt
index 6defbcf..55dd674 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorEmptyImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorEmptyImpl.kt
@@ -34,7 +34,7 @@
override val isQsBypassingShade: Flow<Boolean> = inactiveFlowBoolean
override val isQsFullscreen: Flow<Boolean> = inactiveFlowBoolean
override val anyExpansion: StateFlow<Float> = inactiveFlowFloat
- override val isAnyFullyExpanded: Flow<Boolean> = inactiveFlowBoolean
+ override val isAnyFullyExpanded: StateFlow<Boolean> = inactiveFlowBoolean
override val isShadeFullyExpanded: Flow<Boolean> = inactiveFlowBoolean
override val isAnyExpanded: StateFlow<Boolean> = inactiveFlowBoolean
override val isUserInteractingWithShade: Flow<Boolean> = inactiveFlowBoolean
diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImpl.kt
index 6407b5a..a71cf95 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImpl.kt
@@ -55,12 +55,20 @@
private val baseShadeInteractor: BaseShadeInteractor,
) : ShadeInteractor, BaseShadeInteractor by baseShadeInteractor {
override val isShadeEnabled: StateFlow<Boolean> =
- disableFlagsRepository.disableFlags
- .map { it.isShadeEnabled() }
+ combine(
+ deviceProvisioningRepository.isFactoryResetProtectionActive,
+ disableFlagsRepository.disableFlags,
+ ) { isFrpActive, isDisabledByFlags ->
+ isDisabledByFlags.isShadeEnabled() && !isFrpActive
+ }
+ .distinctUntilChanged()
.stateIn(scope, SharingStarted.Eagerly, initialValue = false)
- override val isAnyFullyExpanded: Flow<Boolean> =
- anyExpansion.map { it >= 1f }.distinctUntilChanged()
+ override val isAnyFullyExpanded: StateFlow<Boolean> =
+ anyExpansion
+ .map { it >= 1f }
+ .distinctUntilChanged()
+ .stateIn(scope, SharingStarted.Eagerly, initialValue = false)
override val isShadeFullyExpanded: Flow<Boolean> =
baseShadeInteractor.shadeExpansion.map { it >= 1f }.distinctUntilChanged()
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutListSearch.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutListSearch.java
index 909cff37..e598242 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutListSearch.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutListSearch.java
@@ -542,7 +542,19 @@
new ShortcutKeyGroupMultiMappingInfo(
context.getString(R.string.group_system_access_google_assistant),
Arrays.asList(
- Pair.create(KeyEvent.KEYCODE_A, KeyEvent.META_META_ON)))
+ Pair.create(KeyEvent.KEYCODE_A, KeyEvent.META_META_ON))),
+ /* Lock screen: Meta + L */
+ new ShortcutKeyGroupMultiMappingInfo(
+ context.getString(R.string.group_system_lock_screen),
+ Arrays.asList(
+ Pair.create(KeyEvent.KEYCODE_L, KeyEvent.META_META_ON))),
+ /* Pull up Notes app for quick memo: Meta + Ctrl + N */
+ new ShortcutKeyGroupMultiMappingInfo(
+ context.getString(R.string.group_system_quick_memo),
+ Arrays.asList(
+ Pair.create(
+ KeyEvent.KEYCODE_N,
+ KeyEvent.META_META_ON | KeyEvent.META_CTRL_ON)))
);
for (ShortcutKeyGroupMultiMappingInfo info : infoList) {
systemGroup.addItem(info.getShortcutMultiMappingInfo());
@@ -584,11 +596,17 @@
new ArrayList<>());
// System multitasking shortcuts:
+ // Enter Split screen with current app to RHS: Meta + Ctrl + Right arrow
+ // Enter Split screen with current app to LHS: Meta + Ctrl + Left arrow
// Switch from Split screen to full screen: Meta + Ctrl + Up arrow
String[] shortcutLabels = {
+ context.getString(R.string.system_multitasking_rhs),
+ context.getString(R.string.system_multitasking_lhs),
context.getString(R.string.system_multitasking_full_screen),
};
int[] keyCodes = {
+ KeyEvent.KEYCODE_DPAD_RIGHT,
+ KeyEvent.KEYCODE_DPAD_LEFT,
KeyEvent.KEYCODE_DPAD_UP,
};
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/NetworkControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/NetworkControllerImpl.java
index fc84973..28d4457 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/NetworkControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/NetworkControllerImpl.java
@@ -35,7 +35,6 @@
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Handler;
-import android.os.HandlerExecutor;
import android.os.Looper;
import android.provider.Settings;
import android.telephony.CarrierConfigManager;
@@ -61,7 +60,6 @@
import com.android.settingslib.mobile.TelephonyIcons;
import com.android.settingslib.net.DataUsageController;
import com.android.systemui.Dumpable;
-import com.android.systemui.res.R;
import com.android.systemui.broadcast.BroadcastDispatcher;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.dagger.qualifiers.Background;
@@ -73,6 +71,7 @@
import com.android.systemui.log.core.LogLevel;
import com.android.systemui.log.dagger.StatusBarNetworkControllerLog;
import com.android.systemui.qs.tiles.dialog.InternetDialogFactory;
+import com.android.systemui.res.R;
import com.android.systemui.settings.UserTracker;
import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags;
import com.android.systemui.statusbar.policy.ConfigurationController;
@@ -85,6 +84,8 @@
import dalvik.annotation.optimization.NeverCompile;
+import kotlin.Unit;
+
import java.io.PrintWriter;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
@@ -99,8 +100,6 @@
import javax.inject.Inject;
-import kotlin.Unit;
-
/** Platform implementation of the network controller. **/
@SysUISingleton
public class NetworkControllerImpl extends BroadcastReceiver
@@ -350,7 +349,7 @@
// AIRPLANE_MODE_CHANGED is sent at boot; we've probably already missed it
updateAirplaneMode(true /* force callback */);
mUserTracker = userTracker;
- mUserTracker.addCallback(mUserChangedCallback, new HandlerExecutor(mMainHandler));
+ mUserTracker.addCallback(mUserChangedCallback, mBgExecutor);
deviceProvisionedController.addCallback(new DeviceProvisionedListener() {
@Override
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/ConversationNotifications.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/ConversationNotifications.kt
index 6e3b15d..c643238 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/ConversationNotifications.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/ConversationNotifications.kt
@@ -52,8 +52,8 @@
entry: NotificationEntry,
recoveredBuilder: Notification.Builder,
logger: NotificationContentInflaterLogger
- ) {
- val messagingStyle = recoveredBuilder.style as? Notification.MessagingStyle ?: return
+ ): Notification.MessagingStyle? {
+ val messagingStyle = recoveredBuilder.style as? Notification.MessagingStyle ?: return null
messagingStyle.conversationType =
if (entry.ranking.channel.isImportantConversation)
Notification.MessagingStyle.CONVERSATION_TYPE_IMPORTANT
@@ -68,6 +68,7 @@
}
messagingStyle.unreadMessageCount =
conversationNotificationManager.getUnreadCount(entry, recoveredBuilder)
+ return messagingStyle
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt
index 46806e6..54b6ad7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/HeadsUpCoordinator.kt
@@ -790,7 +790,7 @@
/** Mutates the HeadsUp state of notifications. */
private interface HunMutator {
- fun updateNotification(key: String, alert: Boolean)
+ fun updateNotification(key: String, shouldHeadsUpAgain: Boolean)
fun removeNotification(key: String, releaseImmediately: Boolean)
}
@@ -801,8 +801,8 @@
private class HunMutatorImpl(private val headsUpManager: HeadsUpManager) : HunMutator {
private val deferred = mutableListOf<Pair<String, Boolean>>()
- override fun updateNotification(key: String, alert: Boolean) {
- headsUpManager.updateNotification(key, alert)
+ override fun updateNotification(key: String, shouldHeadsUpAgain: Boolean) {
+ headsUpManager.updateNotification(key, shouldHeadsUpAgain)
}
override fun removeNotification(key: String, releaseImmediately: Boolean) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinator.java
index 73decfc..639e23a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinator.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinator.java
@@ -362,8 +362,12 @@
}
NotifInflater.Params getInflaterParams(NotifUiAdjustment adjustment, String reason) {
- return new NotifInflater.Params(adjustment.isMinimized(), reason,
- adjustment.isSnoozeEnabled());
+ return new NotifInflater.Params(
+ adjustment.isMinimized(),
+ reason,
+ adjustment.isSnoozeEnabled(),
+ adjustment.isChildInGroup()
+ );
}
private void abortInflation(NotificationEntry entry, String reason) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/SensitiveContentCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/SensitiveContentCoordinator.kt
index 380cdad..ae4ba27 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/SensitiveContentCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/SensitiveContentCoordinator.kt
@@ -18,6 +18,7 @@
import android.os.UserHandle
import com.android.keyguard.KeyguardUpdateMonitor
+import com.android.systemui.Flags.screenshareNotificationHiding
import com.android.systemui.plugins.statusbar.StatusBarStateController
import com.android.systemui.statusbar.NotificationLockscreenUserManager
import com.android.systemui.statusbar.StatusBarState
@@ -30,6 +31,7 @@
import com.android.systemui.statusbar.notification.collection.listbuilder.OnBeforeRenderListListener
import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.Invalidator
import com.android.systemui.statusbar.policy.KeyguardStateController
+import com.android.systemui.statusbar.policy.SensitiveNotificationProtectionController
import com.android.systemui.user.domain.interactor.SelectedUserInteractor
import dagger.Binds
import dagger.Module
@@ -55,6 +57,8 @@
private val statusBarStateController: StatusBarStateController,
private val keyguardStateController: KeyguardStateController,
private val selectedUserInteractor: SelectedUserInteractor,
+ private val sensitiveNotificationProtectionController:
+ SensitiveNotificationProtectionController,
) : Invalidator("SensitiveContentInvalidator"),
SensitiveContentCoordinator,
DynamicPrivacyController.Listener,
@@ -82,10 +86,13 @@
return
}
+ val isSensitiveContentProtectionActive = screenshareNotificationHiding() &&
+ sensitiveNotificationProtectionController.isSensitiveStateActive
val currentUserId = lockscreenUserManager.currentUserId
val devicePublic = lockscreenUserManager.isLockscreenPublicMode(currentUserId)
- val deviceSensitive = devicePublic &&
- !lockscreenUserManager.userAllowsPrivateNotificationsInPublic(currentUserId)
+ val deviceSensitive = (devicePublic &&
+ !lockscreenUserManager.userAllowsPrivateNotificationsInPublic(currentUserId)) ||
+ isSensitiveContentProtectionActive
val dynamicallyUnlocked = dynamicPrivacyController.isDynamicallyUnlocked
for (entry in extractAllRepresentativeEntries(entries).filter { it.rowExists() }) {
val notifUserId = entry.sbn.user.identifier
@@ -105,9 +112,13 @@
else -> lockscreenUserManager.needsSeparateWorkChallenge(notifUserId)
}
}
+
+ val shouldProtectNotification = screenshareNotificationHiding() &&
+ sensitiveNotificationProtectionController.shouldProtectNotification(entry)
+
val needsRedaction = lockscreenUserManager.needsRedaction(entry)
val isSensitive = userPublic && needsRedaction
- entry.setSensitive(isSensitive, deviceSensitive)
+ entry.setSensitive(isSensitive || shouldProtectNotification, deviceSensitive)
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotifInflater.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotifInflater.kt
index 4483599..c0b187b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotifInflater.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotifInflater.kt
@@ -20,9 +20,9 @@
import com.android.systemui.statusbar.notification.collection.render.NotifViewController
/**
- * Used by the [PreparationCoordinator]. When notifications are added or updated, the
- * NotifInflater is asked to (re)inflated and prepare their views. This inflation occurs off the
- * main thread. When the inflation is finished, NotifInflater will trigger its InflationCallback.
+ * Used by the [PreparationCoordinator]. When notifications are added or updated, the NotifInflater
+ * is asked to (re)inflated and prepare their views. This inflation occurs off the main thread. When
+ * the inflation is finished, NotifInflater will trigger its InflationCallback.
*/
interface NotifInflater {
/**
@@ -33,7 +33,7 @@
fun rebindViews(entry: NotificationEntry, params: Params, callback: InflationCallback)
/**
- * Called to inflate the views of an entry. Views are not considered inflated until all of its
+ * Called to inflate the views of an entry. Views are not considered inflated until all of its
* views are bound. Once all views are inflated, the InflationCallback is triggered.
*
* @param callback callback called after inflation finishes
@@ -41,25 +41,24 @@
fun inflateViews(entry: NotificationEntry, params: Params, callback: InflationCallback)
/**
- * Request to stop the inflation of an entry. For example, called when a notification is
- * removed and no longer needs to be inflated. Returns whether anything may have been aborted.
+ * Request to stop the inflation of an entry. For example, called when a notification is removed
+ * and no longer needs to be inflated. Returns whether anything may have been aborted.
*/
fun abortInflation(entry: NotificationEntry): Boolean
- /**
- * Called to let the system remove the content views from the notification row.
- */
+ /** Called to let the system remove the content views from the notification row. */
fun releaseViews(entry: NotificationEntry)
- /**
- * Callback once all the views are inflated and bound for a given NotificationEntry.
- */
+ /** Callback once all the views are inflated and bound for a given NotificationEntry. */
interface InflationCallback {
fun onInflationFinished(entry: NotificationEntry, controller: NotifViewController)
}
- /**
- * A class holding parameters used when inflating the notification row
- */
- class Params(val isLowPriority: Boolean, val reason: String, val showSnooze: Boolean)
+ /** A class holding parameters used when inflating the notification row */
+ class Params(
+ val isLowPriority: Boolean,
+ val reason: String,
+ val showSnooze: Boolean,
+ val isChildInGroup: Boolean = false,
+ )
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotifUiAdjustment.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotifUiAdjustment.kt
index ee0b008..e1d2cdc 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotifUiAdjustment.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotifUiAdjustment.kt
@@ -20,6 +20,7 @@
import android.app.RemoteInput
import android.graphics.drawable.Icon
import android.text.TextUtils
+import com.android.systemui.statusbar.notification.row.shared.AsyncHybridViewInflation
/**
* An immutable object which contains minimal state extracted from an entry that represents state
@@ -34,6 +35,7 @@
val isSnoozeEnabled: Boolean,
val isMinimized: Boolean,
val needsRedaction: Boolean,
+ val isChildInGroup: Boolean,
) {
companion object {
@JvmStatic
@@ -48,6 +50,11 @@
oldAdjustment.needsRedaction != newAdjustment.needsRedaction -> true
areDifferent(oldAdjustment.smartActions, newAdjustment.smartActions) -> true
newAdjustment.smartReplies != oldAdjustment.smartReplies -> true
+ // TODO(b/217799515): Here we decide whether to re-inflate the row on every group-status
+ // change if we want to keep the single-line view, the following line should be:
+ // !oldAdjustment.isChildInGroup && newAdjustment.isChildInGroup -> true
+ AsyncHybridViewInflation.isEnabled &&
+ oldAdjustment.isChildInGroup != newAdjustment.isChildInGroup -> true
else -> false
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotifUiAdjustmentProvider.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotifUiAdjustmentProvider.kt
index 0585456..6f44c13 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotifUiAdjustmentProvider.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotifUiAdjustmentProvider.kt
@@ -29,6 +29,7 @@
import com.android.systemui.statusbar.notification.collection.GroupEntry
import com.android.systemui.statusbar.notification.collection.NotificationEntry
import com.android.systemui.statusbar.notification.collection.provider.SectionStyleProvider
+import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManager
import com.android.systemui.util.ListenerSet
import com.android.systemui.util.settings.SecureSettings
import javax.inject.Inject
@@ -43,7 +44,8 @@
private val secureSettings: SecureSettings,
private val lockscreenUserManager: NotificationLockscreenUserManager,
private val sectionStyleProvider: SectionStyleProvider,
- private val userTracker: UserTracker
+ private val userTracker: UserTracker,
+ private val groupMembershipManager: GroupMembershipManager,
) {
private val dirtyListeners = ListenerSet<Runnable>()
private var isSnoozeSettingsEnabled = false
@@ -121,5 +123,6 @@
isSnoozeEnabled = isSnoozeSettingsEnabled && !entry.isCanceled,
isMinimized = isEntryMinimized(entry),
needsRedaction = lockscreenUserManager.needsRedaction(entry),
+ isChildInGroup = groupMembershipManager.isChildInGroup(entry),
)
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinderImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinderImpl.java
index 80ef14b..cd816ae 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinderImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinderImpl.java
@@ -20,6 +20,7 @@
import static com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_CONTRACTED;
import static com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_EXPANDED;
import static com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_PUBLIC;
+import static com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_SINGLE_LINE;
import static java.util.Objects.requireNonNull;
@@ -49,6 +50,7 @@
import com.android.systemui.statusbar.notification.row.RowContentBindStage;
import com.android.systemui.statusbar.notification.row.RowInflaterTask;
import com.android.systemui.statusbar.notification.row.dagger.ExpandableNotificationRowComponent;
+import com.android.systemui.statusbar.notification.row.shared.AsyncHybridViewInflation;
import com.android.systemui.statusbar.notification.stack.NotificationListContainer;
import javax.inject.Inject;
@@ -127,6 +129,8 @@
@NonNull NotifInflater.Params params,
NotificationRowContentBinder.InflationCallback callback)
throws InflationException {
+ //TODO(b/217799515): Remove the entry parameter from getViewParentForNotification(), this
+ // function returns the NotificationStackScrollLayout regardless of the entry.
ViewGroup parent = mListContainer.getViewParentForNotification(entry);
if (entry.rowExists()) {
@@ -174,6 +178,9 @@
params.markContentViewsFreeable(FLAG_CONTENT_VIEW_CONTRACTED);
params.markContentViewsFreeable(FLAG_CONTENT_VIEW_EXPANDED);
params.markContentViewsFreeable(FLAG_CONTENT_VIEW_PUBLIC);
+ if (AsyncHybridViewInflation.isEnabled()) {
+ params.markContentViewsFreeable(FLAG_CONTENT_VIEW_SINGLE_LINE);
+ }
mRowContentBindStage.requestRebind(entry, null);
}
@@ -254,6 +261,16 @@
params.markContentViewsFreeable(FLAG_CONTENT_VIEW_PUBLIC);
}
+ if (AsyncHybridViewInflation.isEnabled()) {
+ if (inflaterParams.isChildInGroup()) {
+ params.requireContentViews(FLAG_CONTENT_VIEW_SINGLE_LINE);
+ } else {
+ // TODO(b/217799515): here we decide whether to free the single-line view
+ // when the group status changes
+ params.markContentViewsFreeable(FLAG_CONTENT_VIEW_SINGLE_LINE);
+ }
+ }
+
params.rebindAllContentViews();
mLogger.logRequestingRebind(entry, inflaterParams);
mRowContentBindStage.requestRebind(entry, en -> {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerImpl.java
index 2d5afd5..3cdb2cd 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerImpl.java
@@ -21,8 +21,6 @@
import com.android.systemui.Dumpable;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.dump.DumpManager;
-import com.android.systemui.flags.FeatureFlags;
-import com.android.systemui.flags.Flags;
import com.android.systemui.statusbar.notification.collection.GroupEntry;
import com.android.systemui.statusbar.notification.collection.ListEntry;
import com.android.systemui.statusbar.notification.collection.NotifPipeline;
@@ -53,14 +51,11 @@
*/
private final Set<NotificationEntry> mExpandedGroups = new HashSet<>();
- private final FeatureFlags mFeatureFlags;
-
@Inject
public GroupExpansionManagerImpl(DumpManager dumpManager,
- GroupMembershipManager groupMembershipManager, FeatureFlags featureFlags) {
+ GroupMembershipManager groupMembershipManager) {
mDumpManager = dumpManager;
mGroupMembershipManager = groupMembershipManager;
- mFeatureFlags = featureFlags;
}
/**
@@ -86,10 +81,8 @@
};
public void attach(NotifPipeline pipeline) {
- if (mFeatureFlags.isEnabled(Flags.NOTIFICATION_GROUP_EXPANSION_CHANGE)) {
- mDumpManager.registerDumpable(this);
- pipeline.addOnBeforeRenderListListener(mNotifTracker);
- }
+ mDumpManager.registerDumpable(this);
+ pipeline.addOnBeforeRenderListListener(mNotifTracker);
}
@Override
@@ -105,8 +98,7 @@
@Override
public void setGroupExpanded(NotificationEntry entry, boolean expanded) {
NotificationEntry groupSummary = mGroupMembershipManager.getGroupSummary(entry);
- if (mFeatureFlags.isEnabled(Flags.NOTIFICATION_GROUP_EXPANSION_CHANGE)
- && entry.getParent() == null) {
+ if (entry.getParent() == null) {
if (expanded) {
throw new IllegalArgumentException("Cannot expand group that is not attached");
} else {
@@ -124,7 +116,7 @@
}
// Only notify listeners if something changed.
- if (!mFeatureFlags.isEnabled(Flags.NOTIFICATION_GROUP_EXPANSION_CHANGE) || changed) {
+ if (changed) {
sendOnGroupExpandedChange(entry, expanded);
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupMembershipManagerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupMembershipManagerImpl.java
index cb79353..da12479 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupMembershipManagerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupMembershipManagerImpl.java
@@ -22,8 +22,6 @@
import androidx.annotation.Nullable;
import com.android.systemui.dagger.SysUISingleton;
-import com.android.systemui.flags.FeatureFlagsClassic;
-import com.android.systemui.flags.Flags;
import com.android.systemui.statusbar.notification.collection.GroupEntry;
import com.android.systemui.statusbar.notification.collection.ListEntry;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
@@ -38,25 +36,17 @@
*/
@SysUISingleton
public class GroupMembershipManagerImpl implements GroupMembershipManager {
- FeatureFlagsClassic mFeatureFlags;
-
@Inject
- public GroupMembershipManagerImpl(FeatureFlagsClassic featureFlags) {
- mFeatureFlags = featureFlags;
- }
+ public GroupMembershipManagerImpl() {}
@Override
public boolean isGroupSummary(@NonNull NotificationEntry entry) {
- if (mFeatureFlags.isEnabled(Flags.NOTIFICATION_GROUP_EXPANSION_CHANGE)) {
- if (entry.getParent() == null) {
- // The entry is not attached, so it doesn't count.
- return false;
- }
- // If entry is a summary, its parent is a GroupEntry with summary = entry.
- return entry.getParent().getSummary() == entry;
- } else {
- return getGroupSummary(entry) == entry;
+ if (entry.getParent() == null) {
+ // The entry is not attached, so it doesn't count.
+ return false;
}
+ // If entry is a summary, its parent is a GroupEntry with summary = entry.
+ return entry.getParent().getSummary() == entry;
}
@Nullable
@@ -70,12 +60,8 @@
@Override
public boolean isChildInGroup(@NonNull NotificationEntry entry) {
- if (mFeatureFlags.isEnabled(Flags.NOTIFICATION_GROUP_EXPANSION_CHANGE)) {
- // An entry is a child if it's not a summary or top level entry, but it is attached.
- return !isGroupSummary(entry) && !isTopLevelEntry(entry) && entry.getParent() != null;
- } else {
- return !isTopLevelEntry(entry);
- }
+ // An entry is a child if it's not a summary or top level entry, but it is attached.
+ return !isGroupSummary(entry) && !isTopLevelEntry(entry) && entry.getParent() != null;
}
@Override
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/ShadeViewDiffer.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/ShadeViewDiffer.kt
index 61e6f65..8021d8f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/ShadeViewDiffer.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/ShadeViewDiffer.kt
@@ -127,6 +127,9 @@
}
}
+ /**
+ * Attach the Child Nodes to the parentNode using the structure from specMap
+ */
private fun attachChildren(
parentNode: ShadeNode,
specMap: Map<NodeController, NodeSpec>
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 aca8b64..342828c 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
@@ -6,6 +6,7 @@
import android.net.Uri
import android.os.Handler
import android.os.HandlerExecutor
+import android.os.HandlerThread
import android.os.UserHandle
import android.provider.Settings
import com.android.keyguard.KeyguardUpdateMonitor
@@ -87,6 +88,7 @@
secureSettings.getUriFor(Settings.Secure.LOCK_SCREEN_SHOW_SILENT_NOTIFICATIONS)
private val onStateChangedListeners = ListenerSet<Consumer<String>>()
private var hideSilentNotificationsOnLockscreen: Boolean = false
+ private val handlerThread: HandlerThread = HandlerThread("KeyguardNotificationVis")
private val userTrackerCallback = object : UserTracker.Callback {
override fun onUserChanged(newUser: Int, userContext: Context) {
@@ -154,7 +156,9 @@
notifyStateChanged("onStatusBarUpcomingStateChanged")
}
})
- userTracker.addCallback(userTrackerCallback, HandlerExecutor(handler))
+ handlerThread.start()
+ userTracker.addCallback(userTrackerCallback,
+ HandlerExecutor(handlerThread.getThreadHandler()))
}
override fun addOnStateChangedListener(listener: Consumer<String>) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationLogger.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationLogger.java
index 4349b3b..c6832bc 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationLogger.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationLogger.java
@@ -247,6 +247,9 @@
public void setUpWithContainer(NotificationListContainer listContainer) {
mListContainer = listContainer;
+ if (mLogging) {
+ mListContainer.setChildLocationsChangedListener(this::onChildLocationsChanged);
+ }
}
@Override
@@ -294,7 +297,9 @@
lockscreen = mLockscreen != null && mLockscreen;
}
mNotificationPanelLogger.logPanelShown(lockscreen, getVisibleNotifications());
- mListContainer.setChildLocationsChangedListener(this::onChildLocationsChanged);
+ if (mListContainer != null) {
+ mListContainer.setChildLocationsChangedListener(this::onChildLocationsChanged);
+ }
// Sometimes, the transition from lockscreenOrShadeVisible=false ->
// lockscreenOrShadeVisible=true doesn't cause the scroller to emit child location
// events. Hence generate one ourselves to guarantee that we're reporting visible
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/BindStage.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/BindStage.java
index d626c18..8ae324f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/BindStage.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/BindStage.java
@@ -44,6 +44,7 @@
/**
* Execute the stage asynchronously.
*
+ * @param entry the NotificationEntry to bind
* @param row notification top-level view to bind views to
* @param callback callback after stage finishes
*/
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/HybridConversationNotificationView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/HybridConversationNotificationView.java
index 43d99a0..6bc2b2f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/HybridConversationNotificationView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/HybridConversationNotificationView.java
@@ -16,19 +16,27 @@
package com.android.systemui.statusbar.notification.row;
+import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
+import android.content.res.ColorStateList;
import android.graphics.drawable.Icon;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.view.View;
+import android.view.ViewStub;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.TextView;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.widget.ConversationLayout;
import com.android.systemui.res.R;
import com.android.systemui.statusbar.notification.NotificationFadeAware;
+import com.android.systemui.statusbar.notification.row.shared.AsyncHybridViewInflation;
+import com.android.systemui.statusbar.notification.row.ui.viewmodel.ConversationAvatar;
+import com.android.systemui.statusbar.notification.row.ui.viewmodel.FacePile;
+import com.android.systemui.statusbar.notification.row.ui.viewmodel.SingleIcon;
/**
* A hybrid view which may contain information about one ore more conversations.
@@ -37,6 +45,7 @@
private ImageView mConversationIconView;
private TextView mConversationSenderName;
+ private ViewStub mConversationFacePileStub;
private View mConversationFacePile;
private int mSingleAvatarSize;
private int mFacePileSize;
@@ -65,7 +74,16 @@
protected void onFinishInflate() {
super.onFinishInflate();
mConversationIconView = requireViewById(com.android.internal.R.id.conversation_icon);
- mConversationFacePile = requireViewById(com.android.internal.R.id.conversation_face_pile);
+ if (AsyncHybridViewInflation.isEnabled()) {
+ mConversationFacePileStub =
+ requireViewById(com.android.internal.R.id.conversation_face_pile);
+ } else {
+ // TODO(b/217799515): This usage is vague because mConversationFacePile represents both
+ // View and ViewStub at different stages of View inflation, should be removed when
+ // AsyncHybridViewInflation flag is removed
+ mConversationFacePile =
+ requireViewById(com.android.internal.R.id.conversation_face_pile);
+ }
mConversationSenderName = requireViewById(R.id.conversation_notification_sender);
applyTextColor(mConversationSenderName, mSecondaryTextColor);
mFacePileSize = getResources()
@@ -85,7 +103,8 @@
@Override
public void bind(@Nullable CharSequence title, @Nullable CharSequence text,
- @Nullable View contentView) {
+ @Nullable View contentView) {
+ AsyncHybridViewInflation.assertInLegacyMode();
if (!(contentView instanceof ConversationLayout)) {
super.bind(title, text, contentView);
return;
@@ -137,6 +156,77 @@
super.bind(conversationTitle, conversationText, conversationLayout);
}
+ /**
+ * Set the avatar using ConversationAvatar from SingleLineViewModel
+ *
+ * @param conversationAvatar the icon needed for a single-line conversation view, it should be
+ * either an instance of SingleIcon or FacePile
+ */
+ public void setAvatar(@NonNull ConversationAvatar conversationAvatar) {
+ if (AsyncHybridViewInflation.isUnexpectedlyInLegacyMode()) return;
+ if (conversationAvatar instanceof SingleIcon) {
+ SingleIcon avatar = (SingleIcon) conversationAvatar;
+ if (mConversationFacePile != null) mConversationFacePile.setVisibility(GONE);
+ mConversationIconView.setVisibility(VISIBLE);
+ mConversationIconView.setImageDrawable(avatar.getIconDrawable());
+ setSize(mConversationIconView, mSingleAvatarSize);
+ return;
+ }
+
+ // If conversationAvatar is not a SingleIcon, it should be a FacePile.
+ // Bind the face pile with it.
+ FacePile facePileModel = (FacePile) conversationAvatar;
+ mConversationIconView.setVisibility(GONE);
+ // Inflate mConversationFacePile from ViewStub
+ if (mConversationFacePile == null) {
+ mConversationFacePile = mConversationFacePileStub.inflate();
+ }
+ mConversationFacePile.setVisibility(VISIBLE);
+
+ ImageView facePileBottomBg = mConversationFacePile.requireViewById(
+ com.android.internal.R.id.conversation_face_pile_bottom_background);
+ ImageView facePileBottom = mConversationFacePile.requireViewById(
+ com.android.internal.R.id.conversation_face_pile_bottom);
+ ImageView facePileTop = mConversationFacePile.requireViewById(
+ com.android.internal.R.id.conversation_face_pile_top);
+
+ int bottomBackgroundColor = facePileModel.getBottomBackgroundColor();
+ facePileBottomBg.setImageTintList(ColorStateList.valueOf(bottomBackgroundColor));
+
+ facePileBottom.setImageDrawable(facePileModel.getBottomIconDrawable());
+ facePileTop.setImageDrawable(facePileModel.getTopIconDrawable());
+
+ setSize(mConversationFacePile, mFacePileSize);
+ setSize(facePileBottom, mFacePileAvatarSize);
+ setSize(facePileTop, mFacePileAvatarSize);
+ setSize(facePileBottomBg, mFacePileAvatarSize + 2 * mFacePileProtectionWidth);
+
+ mTransformationHelper.addViewTransformingToSimilar(facePileTop);
+ mTransformationHelper.addViewTransformingToSimilar(facePileBottom);
+ mTransformationHelper.addViewTransformingToSimilar(facePileBottomBg);
+
+ }
+
+ /**
+ * bind the text views
+ */
+ public void setText(
+ CharSequence titleText,
+ CharSequence contentText,
+ CharSequence conversationSenderName
+ ) {
+ if (AsyncHybridViewInflation.isUnexpectedlyInLegacyMode()) return;
+ if (conversationSenderName == null) {
+ mConversationSenderName.setVisibility(GONE);
+ } else {
+ mConversationSenderName.setVisibility(VISIBLE);
+ mConversationSenderName.setText(conversationSenderName);
+ }
+ // TODO (b/217799515): super.bind() doesn't use contentView, remove the contentView
+ // argument when the flag is removed
+ super.bind(/* title = */ titleText, /* text = */ contentText, /* contentView = */ null);
+ }
+
private static void setSize(View view, int size) {
FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) view.getLayoutParams();
lp.width = size;
@@ -153,4 +243,9 @@
super.setNotificationFaded(faded);
NotificationFadeAware.setLayerTypeForFaded(mConversationFacePile, faded);
}
+
+ @VisibleForTesting
+ TextView getConversationSenderNameView() {
+ return mConversationSenderName;
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/HybridGroupManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/HybridGroupManager.java
index ddd9bdd..09c0349 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/HybridGroupManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/HybridGroupManager.java
@@ -32,6 +32,7 @@
import com.android.internal.widget.ConversationLayout;
import com.android.systemui.res.R;
+import com.android.systemui.statusbar.notification.row.shared.AsyncHybridViewInflation;
/**
* A class managing hybrid groups that include {@link HybridNotificationView} and the notification
@@ -41,6 +42,8 @@
private final Context mContext;
+ private static final String TAG = "HybridGroupManager";
+
private float mOverflowNumberSize;
private int mOverflowNumberPadding;
@@ -93,21 +96,34 @@
public HybridNotificationView bindFromNotification(HybridNotificationView reusableView,
View contentView, StatusBarNotification notification,
ViewGroup parent) {
+ AsyncHybridViewInflation.assertInLegacyMode();
boolean isNewView = false;
if (reusableView == null) {
Trace.beginSection("HybridGroupManager#bindFromNotification");
reusableView = inflateHybridView(contentView, parent);
isNewView = true;
}
- CharSequence titleText = resolveTitle(notification.getNotification());
- CharSequence contentText = resolveText(notification.getNotification());
- reusableView.bind(titleText, contentText, contentView);
+
+ updateReusableView(reusableView, notification, contentView);
if (isNewView) {
Trace.endSection();
}
return reusableView;
}
+ /**
+ * Update the HybridNotificationView (single-line view)'s appearance
+ */
+ public void updateReusableView(HybridNotificationView reusableView,
+ StatusBarNotification notification, View contentView) {
+ AsyncHybridViewInflation.assertInLegacyMode();
+ final CharSequence titleText = resolveTitle(notification.getNotification());
+ final CharSequence contentText = resolveText(notification.getNotification());
+ if (reusableView != null) {
+ reusableView.bind(titleText, contentText, contentView);
+ }
+ }
+
@Nullable
public static CharSequence resolveText(Notification notification) {
CharSequence contentText = notification.extras.getCharSequence(Notification.EXTRA_TEXT);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotifLayoutInflaterFactory.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotifLayoutInflaterFactory.kt
index d10b556..8bc8e8c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotifLayoutInflaterFactory.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotifLayoutInflaterFactory.kt
@@ -22,11 +22,9 @@
import android.view.LayoutInflater
import android.view.View
import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.InflationFlag
-import com.android.systemui.statusbar.notification.row.NotificationRowModule.NOTIF_REMOTEVIEWS_FACTORIES
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
-import javax.inject.Named
/**
* Implementation of [NotifLayoutInflaterFactory]. This class uses a set of
@@ -37,8 +35,7 @@
constructor(
@Assisted private val row: ExpandableNotificationRow,
@Assisted @InflationFlag val layoutType: Int,
- @Named(NOTIF_REMOTEVIEWS_FACTORIES)
- private val remoteViewsFactories: Set<@JvmSuppressWildcards NotifRemoteViewsFactory>
+ private val notifRemoteViewsFactoryContainer: NotifRemoteViewsFactoryContainer
) : LayoutInflater.Factory2 {
override fun onCreateView(
@@ -49,7 +46,7 @@
): View? {
var handledFactory: NotifRemoteViewsFactory? = null
var result: View? = null
- for (layoutFactory in remoteViewsFactories) {
+ for (layoutFactory in notifRemoteViewsFactoryContainer.factories) {
layoutFactory.instantiate(row, layoutType, parent, name, context, attrs)?.run {
check(handledFactory == null) {
"$layoutFactory tries to produce name:$name with type:$layoutType. " +
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotifRemoteViewsFactoryContainer.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotifRemoteViewsFactoryContainer.kt
new file mode 100644
index 0000000..99177c2
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotifRemoteViewsFactoryContainer.kt
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.row
+
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags
+import javax.inject.Inject
+
+interface NotifRemoteViewsFactoryContainer {
+ val factories: Set<NotifRemoteViewsFactory>
+}
+
+class NotifRemoteViewsFactoryContainerImpl
+@Inject
+constructor(
+ featureFlags: FeatureFlags,
+ precomputedTextViewFactory: PrecomputedTextViewFactory,
+ bigPictureLayoutInflaterFactory: BigPictureLayoutInflaterFactory,
+ callLayoutSetDataAsyncFactory: CallLayoutSetDataAsyncFactory,
+) : NotifRemoteViewsFactoryContainer {
+ override val factories: Set<NotifRemoteViewsFactory> = buildSet {
+ add(precomputedTextViewFactory)
+ if (featureFlags.isEnabled(Flags.BIGPICTURE_NOTIFICATION_LAZY_LOADING)) {
+ add(bigPictureLayoutInflaterFactory)
+ }
+ if (featureFlags.isEnabled(Flags.CALL_LAYOUT_ASYNC_SET_DATA)) {
+ add(callLayoutSetDataAsyncFactory)
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java
index f186e66..913d5f6 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java
@@ -20,6 +20,7 @@
import static com.android.systemui.statusbar.notification.row.NotificationContentView.VISIBLE_TYPE_CONTRACTED;
import static com.android.systemui.statusbar.notification.row.NotificationContentView.VISIBLE_TYPE_EXPANDED;
import static com.android.systemui.statusbar.notification.row.NotificationContentView.VISIBLE_TYPE_HEADSUP;
+import static com.android.systemui.statusbar.notification.row.NotificationContentView.VISIBLE_TYPE_SINGLELINE;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -41,15 +42,19 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.widget.ImageMessageConsumer;
-import com.android.systemui.res.R;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.dagger.qualifiers.Background;
import com.android.systemui.media.controls.util.MediaFeatureFlag;
+import com.android.systemui.res.R;
import com.android.systemui.statusbar.InflationTask;
import com.android.systemui.statusbar.NotificationRemoteInputManager;
import com.android.systemui.statusbar.notification.ConversationNotificationProcessor;
import com.android.systemui.statusbar.notification.InflationException;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
+import com.android.systemui.statusbar.notification.row.shared.AsyncHybridViewInflation;
+import com.android.systemui.statusbar.notification.row.ui.viewbinder.SingleLineConversationViewBinder;
+import com.android.systemui.statusbar.notification.row.ui.viewbinder.SingleLineViewBinder;
+import com.android.systemui.statusbar.notification.row.ui.viewmodel.SingleLineViewModel;
import com.android.systemui.statusbar.notification.row.wrapper.NotificationViewWrapper;
import com.android.systemui.statusbar.phone.CentralSurfaces;
import com.android.systemui.statusbar.policy.InflatedSmartReplyState;
@@ -135,7 +140,7 @@
AsyncInflationTask task = new AsyncInflationTask(
mBgExecutor,
mInflateSynchronously,
- contentToBind,
+ /* reInflateFlags = */ contentToBind,
mRemoteViewCache,
entry,
mConversationProcessor,
@@ -145,7 +150,7 @@
bindParams.usesIncreasedHeadsUpHeight,
callback,
mRemoteInputManager.getRemoteViewsOnClickHandler(),
- mIsMediaInQS,
+ /* isMediaFlagEnabled = */ mIsMediaInQS,
mSmartReplyStateInflater,
mNotifLayoutInflaterFactoryProvider,
mLogger);
@@ -178,6 +183,29 @@
result = inflateSmartReplyViews(result, reInflateFlags, entry, row.getContext(),
packageContext, row.getExistingSmartReplyState(), smartRepliesInflater, mLogger);
+ if (AsyncHybridViewInflation.isEnabled()) {
+ boolean isConversation = entry.getRanking().isConversation();
+ Notification.MessagingStyle messagingStyle = null;
+ if (isConversation) {
+ messagingStyle = mConversationProcessor
+ .processNotification(entry, builder, mLogger);
+ }
+ result.mInflatedSingleLineViewModel = SingleLineViewInflater
+ .inflateSingleLineViewModel(
+ entry.getSbn().getNotification(),
+ messagingStyle,
+ builder,
+ row.getContext()
+ );
+ result.mInflatedSingleLineViewHolder =
+ SingleLineViewInflater.inflateSingleLineViewHolder(
+ isConversation,
+ reInflateFlags,
+ entry,
+ row.getContext(),
+ mLogger
+ );
+ }
apply(
mBgExecutor,
@@ -255,6 +283,15 @@
mRemoteViewCache.removeCachedView(entry, FLAG_CONTENT_VIEW_PUBLIC);
});
break;
+ case FLAG_CONTENT_VIEW_SINGLE_LINE: {
+ if (AsyncHybridViewInflation.isEnabled()) {
+ row.getPrivateLayout().performWhenContentInactive(
+ VISIBLE_TYPE_SINGLELINE,
+ () -> row.getPrivateLayout().setSingleLineView(null)
+ );
+ }
+ break;
+ }
default:
break;
}
@@ -282,6 +319,10 @@
if ((contentViews & FLAG_CONTENT_VIEW_PUBLIC) != 0) {
row.getPublicLayout().removeContentInactiveRunnable(VISIBLE_TYPE_CONTRACTED);
}
+ if (AsyncHybridViewInflation.isEnabled()
+ && (contentViews & FLAG_CONTENT_VIEW_SINGLE_LINE) != 0) {
+ row.getPrivateLayout().removeContentInactiveRunnable(VISIBLE_TYPE_SINGLELINE);
+ }
}
private static InflationProgress inflateSmartReplyViews(
@@ -772,6 +813,25 @@
}
setRepliesAndActions = true;
}
+
+ if (AsyncHybridViewInflation.isEnabled()
+ && (reInflateFlags & FLAG_CONTENT_VIEW_SINGLE_LINE) != 0) {
+ HybridNotificationView viewHolder = result.mInflatedSingleLineViewHolder;
+ SingleLineViewModel viewModel = result.mInflatedSingleLineViewModel;
+ if (viewHolder != null && viewModel != null) {
+ if (viewModel.isConversation()) {
+ SingleLineConversationViewBinder.bind(
+ result.mInflatedSingleLineViewModel,
+ result.mInflatedSingleLineViewHolder
+ );
+ } else {
+ SingleLineViewBinder.bind(result.mInflatedSingleLineViewModel,
+ result.mInflatedSingleLineViewHolder);
+ }
+ privateLayout.setSingleLineView(result.mInflatedSingleLineViewHolder);
+ }
+ }
+
if (setRepliesAndActions) {
privateLayout.setInflatedSmartReplyState(result.inflatedSmartReplyState);
}
@@ -941,19 +1001,23 @@
// For all of our templates, we want it to be RTL
packageContext = new RtlEnabledContext(packageContext);
}
- if (mEntry.getRanking().isConversation()) {
- mConversationProcessor.processNotification(mEntry, recoveredBuilder, mLogger);
+ boolean isConversation = mEntry.getRanking().isConversation();
+ Notification.MessagingStyle messagingStyle = null;
+ if (isConversation) {
+ messagingStyle = mConversationProcessor.processNotification(
+ mEntry, recoveredBuilder, mLogger);
}
InflationProgress inflationProgress = createRemoteViews(mReInflateFlags,
recoveredBuilder, mIsLowPriority, mUsesIncreasedHeight,
mUsesIncreasedHeadsUpHeight, packageContext, mRow,
mNotifLayoutInflaterFactoryProvider, mLogger);
+
mLogger.logAsyncTaskProgress(mEntry,
"getting existing smart reply state (on wrong thread!)");
InflatedSmartReplyState previousSmartReplyState = mRow.getExistingSmartReplyState();
mLogger.logAsyncTaskProgress(mEntry, "inflating smart reply views");
InflationProgress result = inflateSmartReplyViews(
- inflationProgress,
+ /* result = */ inflationProgress,
mReInflateFlags,
mEntry,
mContext,
@@ -962,6 +1026,27 @@
mSmartRepliesInflater,
mLogger);
+ if (AsyncHybridViewInflation.isEnabled()) {
+ // Inflate the single-line content view's ViewModel and ViewHolder from the
+ // background thread, the ViewHolder needs to be bind with ViewModel later from
+ // the main thread.
+ result.mInflatedSingleLineViewModel = SingleLineViewInflater
+ .inflateSingleLineViewModel(
+ mEntry.getSbn().getNotification(),
+ messagingStyle,
+ recoveredBuilder,
+ mContext
+ );
+ result.mInflatedSingleLineViewHolder =
+ SingleLineViewInflater.inflateSingleLineViewHolder(
+ isConversation,
+ mReInflateFlags,
+ mEntry,
+ mContext,
+ mLogger
+ );
+ }
+
mLogger.logAsyncTaskProgress(mEntry,
"getting row image resolver (on wrong thread!)");
final NotificationInlineImageResolver imageResolver = mRow.getImageResolver();
@@ -1078,6 +1163,11 @@
private InflatedSmartReplyState inflatedSmartReplyState;
private InflatedSmartReplyViewHolder expandedInflatedSmartReplies;
private InflatedSmartReplyViewHolder headsUpInflatedSmartReplies;
+
+ // ViewModel for SingleLineView, holds the UI State
+ SingleLineViewModel mInflatedSingleLineViewModel;
+ // Inflated SingleLineViewHolder, SingleLineView that lacks the UI State
+ HybridNotificationView mInflatedSingleLineViewHolder;
}
@VisibleForTesting
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterLogger.kt
index 4f5455d..ee9462c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterLogger.kt
@@ -26,6 +26,7 @@
import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_EXPANDED
import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_HEADS_UP
import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_PUBLIC
+import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_SINGLE_LINE
import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.InflationFlag
import javax.inject.Inject
@@ -99,6 +100,26 @@
)
}
+ fun logInflateSingleLine(
+ entry: NotificationEntry,
+ @InflationFlag inflationFlags: Int,
+ isConversation: Boolean
+ ) {
+ buffer.log(
+ TAG,
+ LogLevel.DEBUG,
+ {
+ str1 = entry.logKey
+ int1 = inflationFlags
+ bool1 = isConversation
+ },
+ {
+ "inflateSingleLineView, inflationFlags: ${flagToString(int1)} for $str1, " +
+ "isConversation: $bool1"
+ }
+ )
+ }
+
companion object {
fun flagToString(@InflationFlag flag: Int): String {
if (flag == 0) {
@@ -121,6 +142,9 @@
if (flag and FLAG_CONTENT_VIEW_PUBLIC != 0) {
l.add("PUBLIC")
}
+ if (flag and FLAG_CONTENT_VIEW_SINGLE_LINE != 0) {
+ l.add("SINGLE_LINE")
+ }
return l.joinToString("|")
}
}
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 a1718b9..402ea51 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
@@ -57,6 +57,7 @@
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManager;
import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier;
+import com.android.systemui.statusbar.notification.row.shared.AsyncHybridViewInflation;
import com.android.systemui.statusbar.notification.row.wrapper.NotificationCustomViewWrapper;
import com.android.systemui.statusbar.notification.row.wrapper.NotificationViewWrapper;
import com.android.systemui.statusbar.policy.InflatedSmartReplyState;
@@ -86,7 +87,7 @@
public static final int VISIBLE_TYPE_CONTRACTED = 0;
public static final int VISIBLE_TYPE_EXPANDED = 1;
public static final int VISIBLE_TYPE_HEADSUP = 2;
- private static final int VISIBLE_TYPE_SINGLELINE = 3;
+ public static final int VISIBLE_TYPE_SINGLELINE = 3;
/**
* Used when there is no content on the view such as when we're a public layout but don't
* need to show.
@@ -98,6 +99,7 @@
private final Rect mClipBounds = new Rect();
private int mMinContractedHeight;
+ private int mMinSingleLineHeight;
private View mContractedChild;
private View mExpandedChild;
private View mHeadsUpChild;
@@ -234,6 +236,11 @@
public void reinflate() {
mMinContractedHeight = getResources().getDimensionPixelSize(
R.dimen.min_notification_layout_height);
+ if (AsyncHybridViewInflation.isEnabled()) {
+ //TODO: set the height with a more reasonable min single-line height
+ mMinSingleLineHeight = getResources().getDimensionPixelSize(
+ R.dimen.conversation_single_line_face_pile_size);
+ }
}
public void setHeights(int smallHeight, int headsUpMaxHeight, int maxHeight) {
@@ -540,6 +547,28 @@
updateShownWrapper(mVisibleType);
}
+ /**
+ * Sets the single-line view. Child may be null to remove the view.
+ * @param child single-line content view to set
+ */
+ public void setSingleLineView(@Nullable HybridNotificationView child) {
+ if (AsyncHybridViewInflation.isUnexpectedlyInLegacyMode()) return;
+ if (mSingleLineView != null) {
+ mOnContentViewInactiveListeners.remove(mSingleLineView);
+ mSingleLineView.animate().cancel();
+ removeView(mSingleLineView);
+ }
+ if (child == null) {
+ mSingleLineView = null;
+ if (mTransformationStartVisibleType == VISIBLE_TYPE_SINGLELINE) {
+ mTransformationStartVisibleType = VISIBLE_TYPE_NONE;
+ }
+ return;
+ }
+ addView(child);
+ mSingleLineView = child;
+ }
+
@Override
public void onViewAdded(View child) {
super.onViewAdded(child);
@@ -809,7 +838,17 @@
return mContractedChild != null
? getViewHeight(VISIBLE_TYPE_CONTRACTED) : mMinContractedHeight;
} else {
- return mSingleLineView.getHeight();
+ if (AsyncHybridViewInflation.isEnabled()) {
+ if (mSingleLineView != null) {
+ return getViewHeight(VISIBLE_TYPE_SINGLELINE);
+ } else {
+ Log.wtf(TAG, "getMinHeight: mSingleLineView == null");
+ return mMinSingleLineHeight;
+ }
+ } else {
+ AsyncHybridViewInflation.assertInLegacyMode();
+ return mSingleLineView.getHeight();
+ }
}
}
@@ -1264,19 +1303,30 @@
}
private void updateSingleLineView() {
- if (mIsChildInGroup) {
+ try {
Trace.beginSection("NotifContentView#updateSingleLineView");
- boolean isNewView = mSingleLineView == null;
- mSingleLineView = mHybridGroupManager.bindFromNotification(
- mSingleLineView, mContractedChild, mNotificationEntry.getSbn(), this);
- if (isNewView) {
- updateViewVisibility(mVisibleType, VISIBLE_TYPE_SINGLELINE,
- mSingleLineView, mSingleLineView);
+ if (AsyncHybridViewInflation.isEnabled()) {
+ return;
}
+ AsyncHybridViewInflation.assertInLegacyMode();
+ if (mIsChildInGroup) {
+ boolean isNewView = mSingleLineView == null;
+ mSingleLineView = mHybridGroupManager.bindFromNotification(
+ /* reusableView = */ mSingleLineView,
+ /* contentView = */ mContractedChild,
+ /* notification = */ mNotificationEntry.getSbn(),
+ /* parent = */ this
+ );
+ if (isNewView && mSingleLineView != null) {
+ updateViewVisibility(mVisibleType, VISIBLE_TYPE_SINGLELINE,
+ mSingleLineView, mSingleLineView);
+ }
+ } else if (mSingleLineView != null) {
+ removeView(mSingleLineView);
+ mSingleLineView = null;
+ }
+ } finally {
Trace.endSection();
- } else if (mSingleLineView != null) {
- removeView(mSingleLineView);
- mSingleLineView = null;
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinder.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinder.java
index d7b7aa2..736140c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinder.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinder.java
@@ -80,6 +80,7 @@
FLAG_CONTENT_VIEW_EXPANDED,
FLAG_CONTENT_VIEW_HEADS_UP,
FLAG_CONTENT_VIEW_PUBLIC,
+ FLAG_CONTENT_VIEW_SINGLE_LINE,
FLAG_CONTENT_VIEW_ALL})
@interface InflationFlag {}
/**
@@ -102,7 +103,12 @@
*/
int FLAG_CONTENT_VIEW_PUBLIC = 1 << 3;
- int FLAG_CONTENT_VIEW_ALL = (1 << 4) - 1;
+ /**
+ * The single line notification view. Show when the notification is shown as a child in group.
+ */
+ int FLAG_CONTENT_VIEW_SINGLE_LINE = 1 << 4;
+
+ int FLAG_CONTENT_VIEW_ALL = (1 << 5) - 1;
/**
* Parameters for content view binding
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowModule.java
index 3a59978..200a08a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowModule.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowModule.java
@@ -17,26 +17,15 @@
package com.android.systemui.statusbar.notification.row;
import com.android.systemui.dagger.SysUISingleton;
-import com.android.systemui.flags.FeatureFlags;
-import com.android.systemui.flags.Flags;
import dagger.Binds;
import dagger.Module;
-import dagger.Provides;
-import dagger.multibindings.ElementsIntoSet;
-
-import java.util.HashSet;
-import java.util.Set;
-
-import javax.inject.Named;
/**
* Dagger Module containing notification row and view inflation implementations.
*/
@Module
public abstract class NotificationRowModule {
- public static final String NOTIF_REMOTEVIEWS_FACTORIES =
- "notif_remoteviews_factories";
/**
* Provides notification row content binder instance.
@@ -54,26 +43,11 @@
public abstract NotifRemoteViewCache provideNotifRemoteViewCache(
NotifRemoteViewCacheImpl cacheImpl);
- /** Provides view factories to be inflated in notification content. */
- @Provides
- @ElementsIntoSet
- @Named(NOTIF_REMOTEVIEWS_FACTORIES)
- static Set<NotifRemoteViewsFactory> provideNotifRemoteViewsFactories(
- FeatureFlags featureFlags,
- PrecomputedTextViewFactory precomputedTextViewFactory,
- BigPictureLayoutInflaterFactory bigPictureLayoutInflaterFactory,
- CallLayoutSetDataAsyncFactory callLayoutSetDataAsyncFactory
- ) {
- final Set<NotifRemoteViewsFactory> replacementFactories = new HashSet<>();
- if (featureFlags.isEnabled(Flags.PRECOMPUTED_TEXT)) {
- replacementFactories.add(precomputedTextViewFactory);
- }
- if (featureFlags.isEnabled(Flags.BIGPICTURE_NOTIFICATION_LAZY_LOADING)) {
- replacementFactories.add(bigPictureLayoutInflaterFactory);
- }
- if (featureFlags.isEnabled(Flags.CALL_LAYOUT_ASYNC_SET_DATA)) {
- replacementFactories.add(callLayoutSetDataAsyncFactory);
- }
- return replacementFactories;
- }
+ /**
+ * Provides notification remote view factory container
+ */
+ @Binds
+ @SysUISingleton
+ public abstract NotifRemoteViewsFactoryContainer provideNotifRemoteViewsFactoryContainer(
+ NotifRemoteViewsFactoryContainerImpl containerImpl);
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindParams.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindParams.java
index a52f638..1494c27 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindParams.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindParams.java
@@ -102,9 +102,9 @@
* @see InflationFlag
*/
public void markContentViewsFreeable(@InflationFlag int contentViews) {
- @InflationFlag int existingContentViews = contentViews &= mContentViews;
+ @InflationFlag int existingFreeableContentViews = contentViews &= mContentViews;
mContentViews &= ~contentViews;
- mDirtyContentViews |= existingContentViews;
+ mDirtyContentViews |= existingFreeableContentViews;
}
public @InflationFlag int getContentViews() {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindStage.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindStage.java
index b70da00..f4f8374 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindStage.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindStage.java
@@ -63,7 +63,10 @@
@InflationFlag int inflationFlags = params.getContentViews();
@InflationFlag int invalidatedFlags = params.getDirtyContentViews();
+ // Rebind the content views which are needed now, and the corresponding old views are
+ // invalidated
@InflationFlag int contentToBind = invalidatedFlags & inflationFlags;
+ // Unbind the content views that are not needed
@InflationFlag int contentToUnbind = inflationFlags ^ FLAG_CONTENT_VIEW_ALL;
// Bind/unbind with parameters
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/SingleLineViewInflater.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/SingleLineViewInflater.kt
new file mode 100644
index 0000000..d6118a0
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/SingleLineViewInflater.kt
@@ -0,0 +1,390 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.row
+
+import android.app.Notification
+import android.app.Notification.MessagingStyle
+import android.app.Person
+import android.content.Context
+import android.graphics.drawable.Icon
+import android.util.Log
+import android.view.LayoutInflater
+import com.android.app.tracing.traceSection
+import com.android.internal.R
+import com.android.internal.widget.MessagingMessage
+import com.android.internal.widget.PeopleHelper
+import com.android.systemui.statusbar.notification.collection.NotificationEntry
+import com.android.systemui.statusbar.notification.logKey
+import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_SINGLE_LINE
+import com.android.systemui.statusbar.notification.row.shared.AsyncHybridViewInflation
+import com.android.systemui.statusbar.notification.row.ui.viewmodel.ConversationAvatar
+import com.android.systemui.statusbar.notification.row.ui.viewmodel.ConversationData
+import com.android.systemui.statusbar.notification.row.ui.viewmodel.FacePile
+import com.android.systemui.statusbar.notification.row.ui.viewmodel.SingleIcon
+import com.android.systemui.statusbar.notification.row.ui.viewmodel.SingleLineViewModel
+
+/** The inflater of SingleLineViewModel and SingleLineViewHolder */
+internal object SingleLineViewInflater {
+ const val TAG = "SingleLineViewInflater"
+
+ /**
+ * Inflate an instance of SingleLineViewModel.
+ *
+ * @param notification the notification to show
+ * @param messagingStyle the MessagingStyle information is only provided for conversation
+ * notification, not for legacy messaging notifications
+ * @param builder the recovered Notification Builder
+ * @param systemUiContext the context of Android System UI
+ * @return the inflated SingleLineViewModel
+ */
+ @JvmStatic
+ fun inflateSingleLineViewModel(
+ notification: Notification,
+ messagingStyle: MessagingStyle?,
+ builder: Notification.Builder,
+ systemUiContext: Context,
+ ): SingleLineViewModel {
+ if (AsyncHybridViewInflation.isUnexpectedlyInLegacyMode()) {
+ return SingleLineViewModel(null, null, null)
+ }
+ peopleHelper.init(systemUiContext)
+ var titleText = HybridGroupManager.resolveTitle(notification)
+ var contentText = HybridGroupManager.resolveText(notification)
+
+ if (messagingStyle == null) {
+ return SingleLineViewModel(
+ titleText = titleText,
+ contentText = contentText,
+ conversationData = null,
+ )
+ }
+
+ val isGroupConversation = messagingStyle.isGroupConversation
+
+ val conversationTextData = messagingStyle.loadConversationTextData(systemUiContext)
+ if (conversationTextData?.conversationTitle?.isNotEmpty() == true) {
+ titleText = conversationTextData.conversationTitle
+ }
+ if (conversationTextData?.conversationText?.isNotEmpty() == true) {
+ contentText = conversationTextData.conversationText
+ }
+
+ val conversationAvatar =
+ messagingStyle.loadConversationAvatar(
+ notification = notification,
+ isGroupConversation = isGroupConversation,
+ builder = builder,
+ systemUiContext = systemUiContext
+ )
+
+ val conversationData =
+ ConversationData(
+ // We don't show the sender's name for one-to-one conversation
+ conversationSenderName =
+ if (isGroupConversation) conversationTextData?.senderName else null,
+ avatar = conversationAvatar
+ )
+
+ return SingleLineViewModel(
+ titleText = titleText,
+ contentText = contentText,
+ conversationData = conversationData,
+ )
+ }
+
+ /** load conversation text data from the MessagingStyle of conversation notifications */
+ private fun MessagingStyle.loadConversationTextData(
+ systemUiContext: Context
+ ): ConversationTextData? {
+ if (AsyncHybridViewInflation.isUnexpectedlyInLegacyMode()) {
+ return null
+ }
+ var conversationText: CharSequence?
+
+ if (messages.isEmpty()) {
+ return null
+ }
+
+ // load the conversation text
+ val lastMessage = messages[messages.lastIndex]
+ conversationText = lastMessage.text
+ if (conversationText == null && lastMessage.isImageMessage()) {
+ conversationText = findBackUpConversationText(lastMessage, systemUiContext)
+ }
+
+ // load the sender's name to display
+ val name = lastMessage.senderPerson?.name
+ val senderName =
+ systemUiContext.resources.getString(
+ R.string.conversation_single_line_name_display,
+ name
+ )
+
+ // We need to find back-up values for those texts if they are needed and empty
+ return ConversationTextData(
+ conversationTitle = conversationTitle
+ ?: findBackUpConversationTitle(senderName, systemUiContext),
+ conversationText = conversationText,
+ senderName = senderName,
+ )
+ }
+
+ private fun MessagingStyle.Message.isImageMessage(): Boolean = MessagingMessage.hasImage(this)
+
+ /** find a back-up conversation title when the conversation title is null. */
+ private fun MessagingStyle.findBackUpConversationTitle(
+ senderName: CharSequence?,
+ systemUiContext: Context,
+ ): CharSequence {
+ if (AsyncHybridViewInflation.isUnexpectedlyInLegacyMode()) {
+ return ""
+ }
+ return if (isGroupConversation) {
+ systemUiContext.resources.getString(R.string.conversation_title_fallback_group_chat)
+ } else {
+ // Is one-to-one, let's try to use the last sender's name
+ // The last back-up is the value of resource: conversation_title_fallback_one_to_one
+ senderName
+ ?: systemUiContext.resources.getString(
+ R.string.conversation_title_fallback_one_to_one
+ )
+ }
+ }
+
+ /**
+ * find a back-up conversation text when the conversation has null text and is image message.
+ */
+ private fun findBackUpConversationText(
+ message: MessagingStyle.Message,
+ context: Context,
+ ): CharSequence? {
+ if (AsyncHybridViewInflation.isUnexpectedlyInLegacyMode()) {
+ return null
+ }
+ // If the message is not an image message, just return empty, the back-up text for showing
+ // will be SingleLineViewModel.contentText
+ if (!message.isImageMessage()) return null
+ // If is image message, return a placeholder
+ return context.resources.getString(R.string.conversation_single_line_image_placeholder)
+ }
+
+ /**
+ * The text data that we load from a conversation notification to show in the single-line views.
+ *
+ * Group conversation single-line view should be formatted as:
+ * [conversationTitle, senderName, conversationText]
+ *
+ * One-to-one single-line view should be formatted as:
+ * [conversationTitle (which is equal to the senderName), conversationText]
+ *
+ * @property conversationTitle the title of the conversation, not necessarily the title of the
+ * notification row. conversationTitle is non-null, though may be empty, in which case we need
+ * to show the notification title instead.
+ * @property conversationText the text content of the conversation, single-line will use the
+ * notification's text when conversationText is null
+ * @property senderName the sender's name to be shown in the row when needed. senderName can be
+ * null
+ */
+ data class ConversationTextData(
+ val conversationTitle: CharSequence,
+ val conversationText: CharSequence?,
+ val senderName: CharSequence?,
+ )
+
+ private fun groupMessages(
+ messages: List<MessagingStyle.Message>,
+ historicMessages: List<MessagingStyle.Message>,
+ ): List<MutableList<MessagingStyle.Message>> {
+ if (AsyncHybridViewInflation.isUnexpectedlyInLegacyMode()) {
+ return listOf()
+ }
+ if (messages.isEmpty() && historicMessages.isEmpty()) return listOf()
+ var currentGroup: MutableList<MessagingStyle.Message>? = null
+ var currentSenderKey: CharSequence? = null
+ val groups = mutableListOf<MutableList<MessagingStyle.Message>>()
+ for (i in 0 until (historicMessages.size + messages.size)) {
+ val message = if (i < historicMessages.size) historicMessages[i] else messages[i]
+
+ val sender = message.senderPerson
+ val senderKey = sender?.getKeyOrName()
+ val isNewGroup = (currentGroup == null) || senderKey != currentSenderKey
+ if (isNewGroup) {
+ currentGroup = mutableListOf()
+ groups.add(currentGroup)
+ currentSenderKey = senderKey
+ }
+ currentGroup?.add(message)
+ }
+ return groups
+ }
+
+ private fun MessagingStyle.loadConversationAvatar(
+ builder: Notification.Builder,
+ notification: Notification,
+ isGroupConversation: Boolean,
+ systemUiContext: Context,
+ ): ConversationAvatar {
+ if (AsyncHybridViewInflation.isUnexpectedlyInLegacyMode()) {
+ return SingleIcon(null)
+ }
+ val userKey = user.getKeyOrName()
+ var conversationIcon: Icon? = null
+ var conversationText: CharSequence? = conversationTitle
+
+ val groups = groupMessages(messages, historicMessages)
+ val uniqueNames = peopleHelper.mapUniqueNamesToPrefixWithGroupList(groups)
+
+ if (!isGroupConversation) {
+ // Conversation is one-to-one, load the single icon
+ // Let's resolve the icon / text from the last sender
+ if (shortcutIcon != null) {
+ conversationIcon = shortcutIcon
+ }
+
+ for (i in messages.lastIndex downTo 0) {
+ val message = messages[i]
+ val sender = message.senderPerson
+ val senderKey = sender?.getKeyOrName()
+ if ((sender != null && senderKey != userKey) || i == 0) {
+ if (conversationText.isNullOrEmpty()) {
+ // We use the senderName as header text if no conversation title is provided
+ // (This usually happens for most 1:1 conversations)
+ conversationText = sender?.name ?: ""
+ }
+ if (conversationIcon == null) {
+ var avatarIcon = sender?.icon
+ if (avatarIcon == null) {
+ avatarIcon = builder.getDefaultAvatar(name = conversationText)
+ }
+ conversationIcon = avatarIcon
+ }
+ break
+ }
+ }
+ }
+
+ if (conversationIcon == null) {
+ conversationIcon = notification.getLargeIcon()
+ }
+
+ // If is one-to-one or the conversation has an icon, return a single icon
+ if (!isGroupConversation || conversationIcon != null) {
+ return SingleIcon(conversationIcon?.loadDrawable(systemUiContext))
+ }
+
+ // Otherwise, let's find the two last conversations to build a face pile:
+ var secondLastIcon: Icon? = null
+ var lastIcon: Icon? = null
+ var lastKey: CharSequence? = null
+
+ for (i in groups.lastIndex downTo 0) {
+ val message = groups[i][0]
+ val sender = message.senderPerson ?: user
+ val senderKey = sender.getKeyOrName()
+ val notUser = senderKey != userKey
+ val notIncluded = senderKey != lastKey
+
+ if ((notUser && notIncluded) || (i == 0 && lastKey == null)) {
+ if (lastIcon == null) {
+ lastIcon =
+ sender.icon
+ ?: builder.getDefaultAvatar(
+ name = sender.name,
+ uniqueNames = uniqueNames
+ )
+ lastKey = senderKey
+ } else {
+ secondLastIcon =
+ sender.icon
+ ?: builder.getDefaultAvatar(
+ name = sender.name,
+ uniqueNames = uniqueNames
+ )
+ break
+ }
+ }
+ }
+
+ if (lastIcon == null) {
+ lastIcon = builder.getDefaultAvatar(name = "")
+ }
+
+ if (secondLastIcon == null) {
+ secondLastIcon = builder.getDefaultAvatar(name = "")
+ }
+
+ return FacePile(
+ topIconDrawable = secondLastIcon.loadDrawable(systemUiContext),
+ bottomIconDrawable = lastIcon.loadDrawable(systemUiContext),
+ bottomBackgroundColor = builder.getBackgroundColor(/* isHeader = */ false),
+ )
+ }
+
+ @JvmStatic
+ fun inflateSingleLineViewHolder(
+ isConversation: Boolean,
+ reinflateFlags: Int,
+ entry: NotificationEntry,
+ context: Context,
+ logger: NotificationContentInflaterLogger,
+ ): HybridNotificationView? {
+ if (AsyncHybridViewInflation.isUnexpectedlyInLegacyMode()) return null
+ if (reinflateFlags and FLAG_CONTENT_VIEW_SINGLE_LINE == 0) {
+ return null
+ }
+
+ logger.logInflateSingleLine(entry, reinflateFlags, isConversation)
+ logger.logAsyncTaskProgress(entry, "inflating single-line content view")
+
+ var view: HybridNotificationView? = null
+
+ traceSection("NotificationContentInflater#inflateSingleLineView") {
+ val inflater = LayoutInflater.from(context)
+ val layoutRes: Int =
+ if (isConversation)
+ com.android.systemui.res.R.layout.hybrid_conversation_notification
+ else com.android.systemui.res.R.layout.hybrid_notification
+ view = inflater.inflate(layoutRes, /* root = */ null) as HybridNotificationView
+ if (view == null) {
+ Log.wtf(TAG, "Single-line view inflation result is null for entry: ${entry.logKey}")
+ }
+ }
+ return view
+ }
+
+ private fun Notification.Builder.getDefaultAvatar(
+ name: CharSequence?,
+ uniqueNames: PeopleHelper.NameToPrefixMap? = null
+ ): Icon {
+ val layoutColor = getSmallIconColor(/* isHeader = */ false)
+ if (!name.isNullOrEmpty()) {
+ val symbol = uniqueNames?.getPrefix(name) ?: ""
+ return peopleHelper.createAvatarSymbol(
+ /* name = */ name,
+ /* symbol = */ symbol,
+ /* layoutColor = */ layoutColor
+ )
+ }
+ // If name is null, create default avatar with background color
+ // TODO(b/319829062): Investigate caching default icon for color
+ return peopleHelper.createAvatarSymbol(/* name = */ "", /* symbol = */ "", layoutColor)
+ }
+
+ private fun Person.getKeyOrName(): CharSequence? = if (key == null) name else key
+
+ private val peopleHelper = PeopleHelper()
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewbinder/SingleLineConversationViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewbinder/SingleLineConversationViewBinder.kt
new file mode 100644
index 0000000..69284bd
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewbinder/SingleLineConversationViewBinder.kt
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.row.ui.viewbinder
+
+import com.android.systemui.statusbar.notification.row.HybridConversationNotificationView
+import com.android.systemui.statusbar.notification.row.HybridNotificationView
+import com.android.systemui.statusbar.notification.row.shared.AsyncHybridViewInflation
+import com.android.systemui.statusbar.notification.row.ui.viewmodel.SingleLineViewModel
+
+object SingleLineConversationViewBinder {
+ @JvmStatic
+ fun bind(viewModel: SingleLineViewModel, view: HybridNotificationView?) {
+ if (AsyncHybridViewInflation.isUnexpectedlyInLegacyMode()) return
+ if (view !is HybridConversationNotificationView || !viewModel.isConversation()) {
+ SingleLineViewBinder.bind(viewModel, view)
+ return
+ }
+
+ viewModel.conversationData?.avatar?.let { view.setAvatar(it) }
+ view.setText(
+ viewModel.titleText,
+ viewModel.contentText,
+ viewModel.conversationData?.conversationSenderName
+ )
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewbinder/SingleLineViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewbinder/SingleLineViewBinder.kt
new file mode 100644
index 0000000..22e10c1
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewbinder/SingleLineViewBinder.kt
@@ -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 com.android.systemui.statusbar.notification.row.ui.viewbinder
+
+import com.android.systemui.statusbar.notification.row.HybridNotificationView
+import com.android.systemui.statusbar.notification.row.ui.viewmodel.SingleLineViewModel
+
+object SingleLineViewBinder {
+ @JvmStatic
+ fun bind(viewModel: SingleLineViewModel?, view: HybridNotificationView?) {
+ // bind the title and content text views
+ view?.apply {
+ bind(
+ /* title = */ viewModel?.titleText,
+ /* text = */ viewModel?.contentText,
+ /* contentView = */ null
+ )
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/SingleLineViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/SingleLineViewModel.kt
new file mode 100644
index 0000000..d583fa5
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/SingleLineViewModel.kt
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.row.ui.viewmodel
+
+import android.annotation.ColorInt
+import android.graphics.drawable.Drawable
+
+/**
+ * ViewModel for SingleLine Notification View.
+ *
+ * @property titleText the text of notification view title
+ * @property contentText the text of view content
+ * @property conversationData the data that is needed specifically for conversation single-line
+ * views. Null conversationData shows that the notification is not conversation. Legacy
+ * MessagingStyle Notifications doesn't have this member.
+ */
+data class SingleLineViewModel(
+ var titleText: CharSequence?,
+ var contentText: CharSequence?,
+ var conversationData: ConversationData?,
+) {
+ fun isConversation(): Boolean {
+ return conversationData != null
+ }
+}
+
+/**
+ * @property conversationSenderName the name of sender to show in the single-line view. Only group
+ * conversation single-line views show the sender name.
+ * @property avatar the avatar to show for the conversation
+ */
+data class ConversationData(
+ val conversationSenderName: CharSequence?,
+ val avatar: ConversationAvatar,
+)
+
+/**
+ * An avatar to show for a single-line conversation notification, it can be either a single icon or
+ * a face pile.
+ */
+sealed class ConversationAvatar
+
+data class SingleIcon(val iconDrawable: Drawable?) : ConversationAvatar()
+
+/**
+ * A kind of avatar to show for a group conversation notification view. It consists of two avatars
+ * of the last two senders.
+ */
+data class FacePile(
+ val topIconDrawable: Drawable?,
+ val bottomIconDrawable: Drawable?,
+ @ColorInt val bottomBackgroundColor: Int
+) : ConversationAvatar()
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationAvalancheSuppression.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationAvalancheSuppression.kt
new file mode 100644
index 0000000..a21dd9b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationAvalancheSuppression.kt
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.shared
+
+import com.android.systemui.Flags
+import com.android.systemui.flags.FlagToken
+import com.android.systemui.flags.RefactorFlagUtils
+
+/** Helper for reading or using the notification avalanche suppression flag state. */
+@Suppress("NOTHING_TO_INLINE")
+object NotificationAvalancheSuppression {
+ /** The aconfig flag name */
+ const val FLAG_NAME = Flags.FLAG_NOTIFICATION_AVALANCHE_SUPPRESSION
+
+ /** A token used for dependency declaration */
+ val token: FlagToken
+ get() = FlagToken(FLAG_NAME, isEnabled)
+
+ /** Is the refactor enabled */
+ @JvmStatic
+ inline val isEnabled
+ get() = Flags.notificationAvalancheSuppression()
+
+ /**
+ * Called to ensure code is only run when the flag is enabled. This protects users from the
+ * unintended behaviors caused by accidentally running new logic, while also crashing on an eng
+ * build to ensure that the refactor author catches issues in testing.
+ */
+ @JvmStatic
+ inline fun isUnexpectedlyInLegacyMode() =
+ RefactorFlagUtils.isUnexpectedlyInLegacyMode(isEnabled, FLAG_NAME)
+
+ /**
+ * Called to ensure code is only run when the flag is disabled. This will throw an exception if
+ * the flag is enabled to ensure that the refactor author catches issues in testing.
+ */
+ @JvmStatic
+ inline fun assertInLegacyMode() = RefactorFlagUtils.assertInLegacyMode(isEnabled, FLAG_NAME)
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java
index 45b9c26..abf6c27 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java
@@ -1295,8 +1295,8 @@
if (singleLineView != null) {
minExpandHeight += singleLineView.getHeight();
} else {
- Log.e(TAG, "getMinHeight: child " + child + " single line view is null",
- new Exception());
+ Log.e(TAG, "getMinHeight: child " + child.getEntry().getKey()
+ + " single line view is null", new Exception());
}
visibleChildren++;
}
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 04db653..dd04531 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
@@ -4917,30 +4917,12 @@
public void removeContainerView(View v) {
Assert.isMainThread();
removeView(v);
- if (!FooterViewRefactor.isEnabled()) {
- // A notification was removed, and we're not currently showing the empty shade view.
- if (v instanceof ExpandableNotificationRow && !mController.isShowingEmptyShadeView()) {
- mController.updateShowEmptyShadeView();
- updateFooter();
- mController.updateImportantForAccessibility();
- }
- }
-
updateSpeedBumpIndex();
}
public void addContainerView(View v) {
Assert.isMainThread();
addView(v);
- if (!FooterViewRefactor.isEnabled()) {
- // A notification was added, and we're currently showing the empty shade view.
- if (v instanceof ExpandableNotificationRow && mController.isShowingEmptyShadeView()) {
- mController.updateShowEmptyShadeView();
- updateFooter();
- mController.updateImportantForAccessibility();
- }
- }
-
updateSpeedBumpIndex();
}
@@ -4948,14 +4930,6 @@
Assert.isMainThread();
ensureRemovedFromTransientContainer(v);
addView(v, index);
- // A notification was added, and we're currently showing the empty shade view.
- if (!FooterViewRefactor.isEnabled() && v instanceof ExpandableNotificationRow
- && mController.isShowingEmptyShadeView()) {
- mController.updateShowEmptyShadeView();
- updateFooter();
- mController.updateImportantForAccessibility();
- }
-
updateSpeedBumpIndex();
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
index 6a66bb7..49fde39 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
@@ -22,6 +22,7 @@
import static com.android.app.animation.Interpolators.STANDARD;
import static com.android.internal.jank.InteractionJankMonitor.CUJ_NOTIFICATION_SHADE_SCROLL_FLING;
import static com.android.systemui.Dependency.ALLOW_NOTIFICATION_LONG_PRESS_NAME;
+import static com.android.systemui.Flags.screenshareNotificationHiding;
import static com.android.systemui.statusbar.StatusBarState.KEYGUARD;
import static com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.OnEmptySpaceClickListener;
import static com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.OnOverscrollTopChangedListener;
@@ -135,6 +136,7 @@
import com.android.systemui.statusbar.policy.DeviceProvisionedController.DeviceProvisionedListener;
import com.android.systemui.statusbar.policy.HeadsUpManager;
import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener;
+import com.android.systemui.statusbar.policy.SensitiveNotificationProtectionController;
import com.android.systemui.statusbar.policy.SplitShadeStateController;
import com.android.systemui.statusbar.policy.ZenModeController;
import com.android.systemui.tuner.TunerService;
@@ -218,6 +220,8 @@
private final SecureSettings mSecureSettings;
private final NotificationDismissibilityProvider mDismissibilityProvider;
private final ActivityStarter mActivityStarter;
+ private final SensitiveNotificationProtectionController
+ mSensitiveNotificationProtectionController;
private View mLongPressedView;
@@ -295,6 +299,15 @@
}
};
+ private final Runnable mSensitiveStateChangedListener = new Runnable() {
+ @Override
+ public void run() {
+ // Animate false to protect against screen recording capturing content
+ // during the animation
+ updateSensitivenessWithAnimation(false);
+ }
+ };
+
private final DynamicPrivacyController.Listener mDynamicPrivacyControllerListener = () -> {
if (mView.isExpanded()) {
// The bottom might change because we're using the final actual height of the view
@@ -342,6 +355,12 @@
private float mMaxAlphaForExpansion = 1.0f;
private float mMaxAlphaForUnhide = 1.0f;
+ /**
+ * Maximum alpha when to and from or sitting idle on the glanceable hub. Will be 1.0f when the
+ * hub is not visible or transitioning.
+ */
+ private float mMaxAlphaForGlanceableHub = 1.0f;
+
private final NotificationListViewBinder mViewBinder;
private void updateResources() {
@@ -399,7 +418,20 @@
}
private void updateSensitivenessWithAnimation(boolean animate) {
- mView.updateSensitiveness(animate, mLockscreenUserManager.isAnyProfilePublicMode());
+ Trace.beginSection("NSSLC.updateSensitivenessWithAnimation");
+ if (screenshareNotificationHiding()) {
+ boolean isAnyProfilePublic = mLockscreenUserManager.isAnyProfilePublicMode();
+ boolean isSensitiveContentProtectionActive =
+ mSensitiveNotificationProtectionController.isSensitiveStateActive();
+ boolean isSensitive = isAnyProfilePublic || isSensitiveContentProtectionActive;
+
+ // Only animate if in a non-sensitive state (not screen sharing)
+ boolean shouldAnimate = animate && !isSensitiveContentProtectionActive;
+ mView.updateSensitiveness(shouldAnimate, isSensitive);
+ } else {
+ mView.updateSensitiveness(animate, mLockscreenUserManager.isAnyProfilePublicMode());
+ }
+ Trace.endSection();
}
/**
@@ -708,7 +740,8 @@
SecureSettings secureSettings,
NotificationDismissibilityProvider dismissibilityProvider,
ActivityStarter activityStarter,
- SplitShadeStateController splitShadeStateController) {
+ SplitShadeStateController splitShadeStateController,
+ SensitiveNotificationProtectionController sensitiveNotificationProtectionController) {
mView = view;
mKeyguardTransitionRepo = keyguardTransitionRepo;
mViewBinder = viewBinder;
@@ -756,6 +789,7 @@
mSecureSettings = secureSettings;
mDismissibilityProvider = dismissibilityProvider;
mActivityStarter = activityStarter;
+ mSensitiveNotificationProtectionController = sensitiveNotificationProtectionController;
mView.passSplitShadeStateController(splitShadeStateController);
mDumpManager.registerDumpable(this);
updateResources();
@@ -860,6 +894,11 @@
mDeviceProvisionedController.addCallback(mDeviceProvisionedListener);
mDeviceProvisionedListener.onDeviceProvisionedChanged();
+ if (screenshareNotificationHiding()) {
+ mSensitiveNotificationProtectionController
+ .registerSensitiveStateListener(mSensitiveStateChangedListener);
+ }
+
if (mView.isAttachedToWindow()) {
mOnAttachStateChangeListener.onViewAttachedToWindow(mView);
}
@@ -1252,9 +1291,19 @@
updateAlpha();
}
+ /**
+ * Sets the max alpha value for notifications when idle on the glanceable hub or when
+ * transitioning to/from the glanceable hub.
+ */
+ public void setMaxAlphaForGlanceableHub(float alpha) {
+ mMaxAlphaForGlanceableHub = alpha;
+ updateAlpha();
+ }
+
private void updateAlpha() {
if (mView != null) {
- mView.setAlpha(Math.min(mMaxAlphaForExpansion, mMaxAlphaForUnhide));
+ mView.setAlpha(Math.min(mMaxAlphaForExpansion,
+ Math.min(mMaxAlphaForUnhide, mMaxAlphaForGlanceableHub)));
}
}
@@ -1746,6 +1795,7 @@
public void dump(@NonNull PrintWriter pw, @NonNull String[] args) {
pw.println("mMaxAlphaForExpansion=" + mMaxAlphaForExpansion);
pw.println("mMaxAlphaForUnhide=" + mMaxAlphaForUnhide);
+ pw.println("mMaxAlphaForGlanceableHub=" + mMaxAlphaForGlanceableHub);
}
/**
@@ -2087,6 +2137,7 @@
if (!FooterViewRefactor.isEnabled()) {
updateShowEmptyShadeView();
+ updateImportantForAccessibility();
}
}
}
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 12927b8..fe5bdd4 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
@@ -18,10 +18,12 @@
import android.animation.Animator
import android.animation.AnimatorListenerAdapter
-import android.animation.ValueAnimator
+import android.view.View
+import android.view.WindowInsets
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.repeatOnLifecycle
import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.keyguard.ui.viewmodel.BurnInParameters
import com.android.systemui.lifecycle.repeatWhenAttached
import com.android.systemui.scene.shared.flag.SceneContainerFlags
import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController
@@ -31,6 +33,9 @@
import com.android.systemui.statusbar.notification.stack.ui.viewmodel.SharedNotificationContainerViewModel
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.DisposableHandle
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
/** Binds the shared notification container to its view-model. */
@@ -66,6 +71,8 @@
}
}
+ val burnInParams = MutableStateFlow(BurnInParameters())
+
/*
* For animation sensitive coroutines, immediately run just like applicationScope does
* instead of doing a post() to the main thread. This extra delay can cause visible jitter.
@@ -123,19 +130,39 @@
}
}
- launch { viewModel.translationY.collect { controller.setTranslationY(it) } }
+ launch {
+ burnInParams
+ .flatMapLatest { params -> viewModel.translationY(params) }
+ .collect { y -> controller.setTranslationY(y) }
+ }
- launch { viewModel.alpha.collect { controller.setMaxAlphaForExpansion(it) } }
+ launch {
+ viewModel.expansionAlpha.collect { controller.setMaxAlphaForExpansion(it) }
+ }
+ launch {
+ viewModel.glanceableHubAlpha.collect {
+ controller.setMaxAlphaForGlanceableHub(it)
+ }
+ }
}
}
controller.setOnHeightChangedRunnable(Runnable { viewModel.notificationStackChanged() })
+ view.setOnApplyWindowInsetsListener { v: View, insets: WindowInsets ->
+ val insetTypes = WindowInsets.Type.systemBars() or WindowInsets.Type.displayCutout()
+ burnInParams.update { current ->
+ current.copy(topInset = insets.getInsetsIgnoringVisibility(insetTypes).top)
+ }
+ insets
+ }
+
return object : DisposableHandle {
override fun dispose() {
disposableHandle.dispose()
disposableHandleMainImmediate.dispose()
controller.setOnHeightChangedRunnable(null)
+ view.setOnApplyWindowInsetsListener(null)
}
}
}
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 a48fb45..4617ce4 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
@@ -20,6 +20,7 @@
package com.android.systemui.statusbar.notification.stack.ui.viewmodel
import com.android.systemui.common.shared.model.NotificationContainerBounds
+import com.android.systemui.communal.domain.interactor.CommunalInteractor
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
@@ -27,6 +28,10 @@
import com.android.systemui.keyguard.shared.model.StatusBarState.SHADE_LOCKED
import com.android.systemui.keyguard.shared.model.TransitionState.RUNNING
import com.android.systemui.keyguard.shared.model.TransitionState.STARTED
+import com.android.systemui.keyguard.ui.viewmodel.AodBurnInViewModel
+import com.android.systemui.keyguard.ui.viewmodel.BurnInParameters
+import com.android.systemui.keyguard.ui.viewmodel.GlanceableHubToLockscreenTransitionViewModel
+import com.android.systemui.keyguard.ui.viewmodel.LockscreenToGlanceableHubTransitionViewModel
import com.android.systemui.keyguard.ui.viewmodel.LockscreenToOccludedTransitionViewModel
import com.android.systemui.keyguard.ui.viewmodel.OccludedToLockscreenTransitionViewModel
import com.android.systemui.shade.domain.interactor.ShadeInteractor
@@ -61,8 +66,12 @@
private val keyguardInteractor: KeyguardInteractor,
keyguardTransitionInteractor: KeyguardTransitionInteractor,
private val shadeInteractor: ShadeInteractor,
- occludedToLockscreenTransitionViewModel: OccludedToLockscreenTransitionViewModel,
+ communalInteractor: CommunalInteractor,
+ private val occludedToLockscreenTransitionViewModel: OccludedToLockscreenTransitionViewModel,
lockscreenToOccludedTransitionViewModel: LockscreenToOccludedTransitionViewModel,
+ glanceableHubToLockscreenTransitionViewModel: GlanceableHubToLockscreenTransitionViewModel,
+ lockscreenToGlanceableHubTransitionViewModel: LockscreenToGlanceableHubTransitionViewModel,
+ private val aodBurnInViewModel: AodBurnInViewModel,
) {
private val statesForConstrainedNotifications =
setOf(
@@ -87,6 +96,20 @@
.distinctUntilChanged()
.onStart { emit(false) }
+ private val lockscreenToGlanceableHubRunning =
+ keyguardTransitionInteractor
+ .transition(KeyguardState.LOCKSCREEN, KeyguardState.GLANCEABLE_HUB)
+ .map { it.transitionState == STARTED || it.transitionState == RUNNING }
+ .distinctUntilChanged()
+ .onStart { emit(false) }
+
+ private val glanceableHubToLockscreenRunning =
+ keyguardTransitionInteractor
+ .transition(KeyguardState.GLANCEABLE_HUB, KeyguardState.LOCKSCREEN)
+ .map { it.transitionState == STARTED || it.transitionState == RUNNING }
+ .distinctUntilChanged()
+ .onStart { emit(false) }
+
val shadeCollapseFadeInComplete = MutableStateFlow(false)
val configurationBasedDimensions: Flow<ConfigurationBasedDimensions> =
@@ -144,6 +167,24 @@
initialValue = false,
)
+ /** Are we purely on the glanceable hub without the shade/qs? */
+ internal val isOnGlanceableHubWithoutShade: Flow<Boolean> =
+ combine(
+ communalInteractor.isIdleOnCommunal,
+ // Shade with notifications
+ shadeInteractor.shadeExpansion.map { it > 0f },
+ // Shade without notifications, quick settings only (pull down from very top on
+ // lockscreen)
+ shadeInteractor.qsExpansion.map { it > 0f },
+ ) { isIdleOnCommunal, isShadeVisible, qsExpansion ->
+ isIdleOnCommunal && !(isShadeVisible || qsExpansion)
+ }
+ .stateIn(
+ scope = applicationScope,
+ started = SharingStarted.WhileSubscribed(),
+ initialValue = false,
+ )
+
/** Fade in only for use after the shade collapses */
val shadeCollpaseFadeIn: Flow<Boolean> =
flow {
@@ -201,7 +242,7 @@
initialValue = NotificationContainerBounds(),
)
- val alpha: Flow<Float> =
+ val expansionAlpha: Flow<Float> =
// Due to issues with the legacy shade, some shade expansion events are sent incorrectly,
// such as when the shade resets. This can happen while the LOCKSCREEN<->OCCLUDED transition
// is running. Therefore use a series of flatmaps to prevent unwanted interruptions while
@@ -235,23 +276,62 @@
}
/**
+ * Returns a flow of the expected alpha while running a LOCKSCREEN<->GLANCEABLE_HUB transition
+ * or idle on the glanceable hub.
+ *
+ * Must return 1.0f when not controlling the alpha since notifications does a min of all the
+ * alpha sources.
+ */
+ val glanceableHubAlpha: Flow<Float> =
+ isOnGlanceableHubWithoutShade.flatMapLatest { isOnGlanceableHubWithoutShade ->
+ combineTransform(
+ lockscreenToGlanceableHubRunning,
+ glanceableHubToLockscreenRunning,
+ merge(
+ lockscreenToGlanceableHubTransitionViewModel.notificationAlpha,
+ glanceableHubToLockscreenTransitionViewModel.notificationAlpha,
+ )
+ .onStart {
+ // Transition flows don't emit a value on start, kick things off so the
+ // combine starts.
+ emit(1f)
+ }
+ ) { lockscreenToGlanceableHubRunning, glanceableHubToLockscreenRunning, alpha ->
+ if (isOnGlanceableHubWithoutShade) {
+ // Notifications should not be visible on the glanceable hub.
+ // TODO(b/321075734): implement a way to actually set the notifications to gone
+ // while on the hub instead of just adjusting alpha
+ emit(0f)
+ } else if (lockscreenToGlanceableHubRunning || glanceableHubToLockscreenRunning) {
+ emit(alpha)
+ } else {
+ // Not on the hub and no transitions running, return full visibility so we don't
+ // block the notifications from showing.
+ emit(1f)
+ }
+ }
+ }
+
+ /**
* Under certain scenarios, such as swiping up on the lockscreen, the container will need to be
* translated as the keyguard fades out.
*/
- val translationY: Flow<Float> =
- combine(
+ fun translationY(params: BurnInParameters): Flow<Float> {
+ return combine(
+ aodBurnInViewModel.translationY(params).onStart { emit(0f) },
isOnLockscreenWithoutShade,
merge(
keyguardInteractor.keyguardTranslationY,
occludedToLockscreenTransitionViewModel.lockscreenTranslationY,
)
- ) { isOnLockscreenWithoutShade, translationY ->
+ ) { burnInY, isOnLockscreenWithoutShade, translationY ->
if (isOnLockscreenWithoutShade) {
- translationY
+ burnInY + translationY
} else {
0f
}
}
+ }
/**
* When on keyguard, there is limited space to display notifications so calculate how many could
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterImpl.kt
index 63194c3..8a56da3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterImpl.kt
@@ -797,18 +797,7 @@
}
}
if (dismissShade) {
- if (
- shadeControllerLazy.get().isExpandedVisible &&
- !statusBarKeyguardViewManagerLazy.get().isBouncerShowing
- ) {
- shadeControllerLazy.get().animateCollapseShadeForcedDelayed()
- } else {
- // Do it after DismissAction has been processed to conserve the
- // needed ordering.
- postOnUiThread {
- shadeControllerLazy.get().runPostCollapseRunnables()
- }
- }
+ shadeControllerLazy.get().collapseShadeForActivityStart()
}
return deferred
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java
index 90cba40..4019436 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java
@@ -25,6 +25,7 @@
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.os.UserHandle;
+import android.view.MotionEvent;
import android.view.RemoteAnimationAdapter;
import android.view.View;
import android.window.RemoteTransition;
@@ -277,6 +278,13 @@
void awakenDreams();
+ /**
+ * Handle a touch event while dreaming when the touch was initiated within a prescribed
+ * swipeable area. This method is provided for cases where swiping in certain areas of a dream
+ * should be handled by CentralSurfaces instead (e.g. swiping communal hub open).
+ */
+ void handleDreamTouch(MotionEvent event);
+
boolean isBouncerShowing();
boolean isBouncerShowingScrimmed();
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesEmptyImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesEmptyImpl.kt
index 7dc4b96..60dfaa7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesEmptyImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesEmptyImpl.kt
@@ -17,6 +17,7 @@
package com.android.systemui.statusbar.phone
import android.content.Intent
+import android.view.MotionEvent
import androidx.lifecycle.LifecycleRegistry
import com.android.keyguard.AuthKeyguardMessageArea
import com.android.systemui.animation.ActivityLaunchAnimator
@@ -78,6 +79,7 @@
override fun updateScrimController() {}
override fun shouldIgnoreTouch() = false
override fun isDeviceInteractive() = false
+ override fun handleDreamTouch(event: MotionEvent?) {}
override fun awakenDreams() {}
override fun isBouncerShowing() = false
override fun isBouncerShowingScrimmed() = false
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
index 6e3aabf..7952511 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
@@ -80,6 +80,7 @@
import android.view.Display;
import android.view.IRemoteAnimationRunner;
import android.view.IWindowManager;
+import android.view.MotionEvent;
import android.view.ThreadedRenderer;
import android.view.View;
import android.view.WindowInsets;
@@ -2586,8 +2587,7 @@
// So if AOD is off or unsupported we need to trigger these updates at screen on
// when the keyguard is occluded.
mLockscreenUserManager.updatePublicMode();
- mShadeSurface.getNotificationStackScrollLayoutController()
- .updateSensitivenessForOccludedWakeup();
+ mStackScrollerController.updateSensitivenessForOccludedWakeup();
}
if (mLaunchCameraWhenFinishedWaking) {
mCameraLauncherLazy.get().launchCamera(mLastCameraLaunchSource,
@@ -2903,6 +2903,11 @@
};
@Override
+ public void handleDreamTouch(MotionEvent event) {
+ getNotificationShadeWindowViewController().handleDreamTouch(event);
+ }
+
+ @Override
public void awakenDreams() {
mUiBgExecutor.execute(() -> {
try {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java
index ae04eaf..aabe4a0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java
@@ -60,6 +60,7 @@
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.dock.DockManager;
import com.android.systemui.keyguard.KeyguardUnlockAnimationController;
+import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor;
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor;
import com.android.systemui.keyguard.shared.model.ScrimAlpha;
import com.android.systemui.keyguard.shared.model.TransitionState;
@@ -217,6 +218,7 @@
private final ScreenOffAnimationController mScreenOffAnimationController;
private final KeyguardUnlockAnimationController mKeyguardUnlockAnimationController;
private final StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
+ private final KeyguardInteractor mKeyguardInteractor;
private GradientColors mColors;
private boolean mNeedsDrawableColorUpdate;
@@ -298,7 +300,7 @@
DozeParameters dozeParameters,
AlarmManager alarmManager,
KeyguardStateController keyguardStateController,
- DelayedWakeLock.Builder delayedWakeLockBuilder,
+ DelayedWakeLock.Factory delayedWakeLockFactory,
Handler handler,
KeyguardUpdateMonitor keyguardUpdateMonitor,
DockManager dockManager,
@@ -311,6 +313,7 @@
PrimaryBouncerToGoneTransitionViewModel primaryBouncerToGoneTransitionViewModel,
AlternateBouncerToGoneTransitionViewModel alternateBouncerToGoneTransitionViewModel,
KeyguardTransitionInteractor keyguardTransitionInteractor,
+ KeyguardInteractor keyguardInteractor,
WallpaperRepository wallpaperRepository,
@Main CoroutineDispatcher mainDispatcher,
LargeScreenShadeInterpolator largeScreenShadeInterpolator) {
@@ -328,7 +331,7 @@
mScreenOffAnimationController = screenOffAnimationController;
mTimeTicker = new AlarmTimeout(alarmManager, this::onHideWallpaperTimeout,
"hide_aod_wallpaper", mHandler);
- mWakeLock = delayedWakeLockBuilder.setHandler(mHandler).setTag("Scrims").build();
+ mWakeLock = delayedWakeLockFactory.create("Scrims");
// Scrim alpha is initially set to the value on the resource but might be changed
// to make sure that text on top of it is legible.
mDozeParameters = dozeParameters;
@@ -357,6 +360,7 @@
mPrimaryBouncerToGoneTransitionViewModel = primaryBouncerToGoneTransitionViewModel;
mAlternateBouncerToGoneTransitionViewModel = alternateBouncerToGoneTransitionViewModel;
mKeyguardTransitionInteractor = keyguardTransitionInteractor;
+ mKeyguardInteractor = keyguardInteractor;
mWallpaperRepository = wallpaperRepository;
mMainDispatcher = mainDispatcher;
}
@@ -759,7 +763,9 @@
// see: b/186644628
mNotificationsScrim.setDrawableBounds(left - 1, top, right + 1, bottom);
mScrimBehind.setBottomEdgePosition((int) top);
+ mKeyguardInteractor.setTopClippingBounds((int) top);
} else {
+ mKeyguardInteractor.setTopClippingBounds(null);
mNotificationsScrim.setDrawableBounds(left, top, right, bottom);
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt
index fe49c07..6b30326 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt
@@ -17,6 +17,7 @@
package com.android.systemui.statusbar.pipeline.mobile.domain.interactor
import android.content.Context
+import com.android.internal.telephony.flags.Flags
import com.android.settingslib.SignalIcon.MobileIconGroup
import com.android.settingslib.graph.SignalDrawable
import com.android.settingslib.mobile.MobileIconCarrierIdOverrides
@@ -32,14 +33,18 @@
import com.android.systemui.statusbar.pipeline.mobile.domain.model.NetworkTypeIconModel.DefaultIcon
import com.android.systemui.statusbar.pipeline.mobile.domain.model.NetworkTypeIconModel.OverriddenIcon
import com.android.systemui.statusbar.pipeline.mobile.domain.model.SignalIconModel
+import com.android.systemui.statusbar.pipeline.satellite.ui.model.SatelliteIconModel
import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
@@ -79,6 +84,9 @@
/** Whether or not to show the slice attribution */
val showSliceAttribution: StateFlow<Boolean>
+ /** True if this connection is satellite-based */
+ val isNonTerrestrial: StateFlow<Boolean>
+
/**
* Provider name for this network connection. The name can be one of 3 values:
* 1. The default network name, if one is configured
@@ -244,6 +252,13 @@
override val showSliceAttribution: StateFlow<Boolean> =
connectionRepository.hasPrioritizedNetworkCapabilities
+ override val isNonTerrestrial: StateFlow<Boolean> =
+ if (Flags.carrierEnabledSatelliteFlag()) {
+ connectionRepository.isNonTerrestrial
+ } else {
+ MutableStateFlow(false).asStateFlow()
+ }
+
override val isRoaming: StateFlow<Boolean> =
combine(
connectionRepository.carrierNetworkChangeActive,
@@ -313,26 +328,45 @@
}
.stateIn(scope, SharingStarted.WhileSubscribed(), 0)
- override val signalLevelIcon: StateFlow<SignalIconModel> = run {
- val initial =
- SignalIconModel(
- level = shownLevel.value,
- numberOfLevels = numberOfLevels.value,
- showExclamationMark = showExclamationMark.value,
- carrierNetworkChange = carrierNetworkChangeActive.value,
- )
+ private val cellularIcon: Flow<SignalIconModel.Cellular> =
combine(
+ shownLevel,
+ numberOfLevels,
+ showExclamationMark,
+ carrierNetworkChangeActive,
+ ) { shownLevel, numberOfLevels, showExclamationMark, carrierNetworkChange ->
+ SignalIconModel.Cellular(
shownLevel,
numberOfLevels,
showExclamationMark,
- carrierNetworkChangeActive,
- ) { shownLevel, numberOfLevels, showExclamationMark, carrierNetworkChange ->
- SignalIconModel(
- shownLevel,
- numberOfLevels,
- showExclamationMark,
- carrierNetworkChange,
- )
+ carrierNetworkChange,
+ )
+ }
+
+ private val satelliteIcon: Flow<SignalIconModel.Satellite> =
+ shownLevel.map {
+ SignalIconModel.Satellite(
+ level = it,
+ icon = SatelliteIconModel.fromSignalStrength(it)
+ ?: SatelliteIconModel.fromSignalStrength(0)!!
+ )
+ }
+
+ override val signalLevelIcon: StateFlow<SignalIconModel> = run {
+ val initial =
+ SignalIconModel.Cellular(
+ shownLevel.value,
+ numberOfLevels.value,
+ showExclamationMark.value,
+ carrierNetworkChangeActive.value,
+ )
+ isNonTerrestrial
+ .flatMapLatest { ntn ->
+ if (ntn) {
+ satelliteIcon
+ } else {
+ cellularIcon
+ }
}
.distinctUntilChanged()
.logDiffsForTable(
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/model/SignalIconModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/model/SignalIconModel.kt
index e58f081..d6b8fd4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/model/SignalIconModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/model/SignalIconModel.kt
@@ -17,51 +17,94 @@
package com.android.systemui.statusbar.pipeline.mobile.domain.model
import com.android.settingslib.graph.SignalDrawable
+import com.android.systemui.common.shared.model.Icon
import com.android.systemui.log.table.Diffable
import com.android.systemui.log.table.TableRowLogger
-/** A model that will be consumed by [SignalDrawable] to show the mobile triangle icon. */
-data class SignalIconModel(
- val level: Int,
- val numberOfLevels: Int,
- val showExclamationMark: Boolean,
- val carrierNetworkChange: Boolean,
-) : Diffable<SignalIconModel> {
- // TODO(b/267767715): Can we implement [logDiffs] and [logFull] generically for data classes?
+sealed interface SignalIconModel : Diffable<SignalIconModel> {
+ val level: Int
+
override fun logDiffs(prevVal: SignalIconModel, row: TableRowLogger) {
- if (prevVal.level != level) {
+ logPartial(prevVal, row)
+ }
+
+ override fun logFull(row: TableRowLogger) = logFully(row)
+
+ fun logFully(row: TableRowLogger)
+
+ fun logPartial(prevVal: SignalIconModel, row: TableRowLogger)
+
+ /** A model that will be consumed by [SignalDrawable] to show the mobile triangle icon. */
+ data class Cellular(
+ override val level: Int,
+ val numberOfLevels: Int,
+ val showExclamationMark: Boolean,
+ val carrierNetworkChange: Boolean,
+ ) : SignalIconModel {
+ override fun logPartial(prevVal: SignalIconModel, row: TableRowLogger) {
+ if (prevVal !is Cellular) {
+ logFull(row)
+ } else {
+ if (prevVal.level != level) {
+ row.logChange(COL_LEVEL, level)
+ }
+ if (prevVal.numberOfLevels != numberOfLevels) {
+ row.logChange(COL_NUM_LEVELS, numberOfLevels)
+ }
+ if (prevVal.showExclamationMark != showExclamationMark) {
+ row.logChange(COL_SHOW_EXCLAMATION, showExclamationMark)
+ }
+ if (prevVal.carrierNetworkChange != carrierNetworkChange) {
+ row.logChange(COL_CARRIER_NETWORK_CHANGE, carrierNetworkChange)
+ }
+ }
+ }
+
+ override fun logFully(row: TableRowLogger) {
+ row.logChange(COL_TYPE, "c")
row.logChange(COL_LEVEL, level)
- }
- if (prevVal.numberOfLevels != numberOfLevels) {
row.logChange(COL_NUM_LEVELS, numberOfLevels)
- }
- if (prevVal.showExclamationMark != showExclamationMark) {
row.logChange(COL_SHOW_EXCLAMATION, showExclamationMark)
- }
- if (prevVal.carrierNetworkChange != carrierNetworkChange) {
row.logChange(COL_CARRIER_NETWORK_CHANGE, carrierNetworkChange)
}
+
+ /** Convert this model to an [Int] consumable by [SignalDrawable]. */
+ fun toSignalDrawableState(): Int =
+ if (carrierNetworkChange) {
+ SignalDrawable.getCarrierChangeState(numberOfLevels)
+ } else {
+ SignalDrawable.getState(level, numberOfLevels, showExclamationMark)
+ }
}
- override fun logFull(row: TableRowLogger) {
- row.logChange(COL_LEVEL, level)
- row.logChange(COL_NUM_LEVELS, numberOfLevels)
- row.logChange(COL_SHOW_EXCLAMATION, showExclamationMark)
- row.logChange(COL_CARRIER_NETWORK_CHANGE, carrierNetworkChange)
- }
-
- /** Convert this model to an [Int] consumable by [SignalDrawable]. */
- fun toSignalDrawableState(): Int =
- if (carrierNetworkChange) {
- SignalDrawable.getCarrierChangeState(numberOfLevels)
- } else {
- SignalDrawable.getState(level, numberOfLevels, showExclamationMark)
+ /**
+ * For non-terrestrial networks, we can use a resource-backed icon instead of the
+ * [SignalDrawable]-backed version above
+ */
+ data class Satellite(
+ override val level: Int,
+ val icon: Icon.Resource,
+ ) : SignalIconModel {
+ override fun logPartial(prevVal: SignalIconModel, row: TableRowLogger) {
+ if (prevVal !is Satellite) {
+ logFull(row)
+ } else {
+ if (prevVal.level != level) row.logChange(COL_LEVEL, level)
+ }
}
+ override fun logFully(row: TableRowLogger) {
+ row.logChange("numLevels", "HELLO")
+ row.logChange(COL_TYPE, "s")
+ row.logChange(COL_LEVEL, level)
+ }
+ }
+
companion object {
private const val COL_LEVEL = "level"
private const val COL_NUM_LEVELS = "numLevels"
private const val COL_SHOW_EXCLAMATION = "showExclamation"
private const val COL_CARRIER_NETWORK_CHANGE = "carrierNetworkChange"
+ private const val COL_TYPE = "type"
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/VerboseMobileViewLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/VerboseMobileViewLogger.kt
index a1a5370..43cb38f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/VerboseMobileViewLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/VerboseMobileViewLogger.kt
@@ -59,7 +59,7 @@
str1 = parentView.getIdForLogging()
int1 = subId
int2 = icon.level
- bool1 = icon.showExclamationMark
+ bool1 = if (icon is SignalIconModel.Cellular) icon.showExclamationMark else false
},
{
"Binder[subId=$int1, viewId=$str1] received new signal icon: " +
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinder.kt
index 5475528..a0c5618 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinder.kt
@@ -38,6 +38,7 @@
import com.android.systemui.res.R
import com.android.systemui.statusbar.StatusBarIconView
import com.android.systemui.statusbar.StatusBarIconView.STATE_HIDDEN
+import com.android.systemui.statusbar.pipeline.mobile.domain.model.SignalIconModel
import com.android.systemui.statusbar.pipeline.mobile.ui.MobileViewLogger
import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.LocationBasedMobileViewModel
import com.android.systemui.statusbar.pipeline.shared.ui.binder.ModernStatusBarViewBinding
@@ -70,7 +71,7 @@
val networkTypeView = view.requireViewById<ImageView>(R.id.mobile_type)
val networkTypeContainer = view.requireViewById<FrameLayout>(R.id.mobile_type_container)
val iconView = view.requireViewById<ImageView>(R.id.mobile_signal)
- val mobileDrawable = SignalDrawable(view.context).also { iconView.setImageDrawable(it) }
+ val mobileDrawable = SignalDrawable(view.context)
val roamingView = view.requireViewById<ImageView>(R.id.mobile_roaming)
val roamingSpace = view.requireViewById<Space>(R.id.mobile_roaming_space)
val dotView = view.requireViewById<StatusBarIconView>(R.id.status_bar_dot)
@@ -138,7 +139,12 @@
viewModel.subscriptionId,
icon,
)
- mobileDrawable.level = icon.toSignalDrawableState()
+ if (icon is SignalIconModel.Cellular) {
+ iconView.setImageDrawable(mobileDrawable)
+ mobileDrawable.level = icon.toSignalDrawableState()
+ } else if (icon is SignalIconModel.Satellite) {
+ IconViewBinder.bind(icon.icon, iconView)
+ }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt
index 60c662d..eda5c44 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt
@@ -33,12 +33,15 @@
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.stateIn
/** Common interface for all of the location-based mobile icon view models. */
@@ -71,7 +74,6 @@
* model gets the exact same information, as well as allows us to log that unified state only once
* per icon.
*/
-@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
@OptIn(ExperimentalCoroutinesApi::class)
class MobileIconViewModel(
override val subscriptionId: Int,
@@ -81,6 +83,100 @@
flags: FeatureFlagsClassic,
scope: CoroutineScope,
) : MobileIconViewModelCommon {
+ private val cellProvider by lazy {
+ CellularIconViewModel(
+ subscriptionId,
+ iconInteractor,
+ airplaneModeInteractor,
+ constants,
+ flags,
+ scope,
+ )
+ }
+
+ private val satelliteProvider by lazy {
+ CarrierBasedSatelliteViewModelImpl(
+ subscriptionId,
+ iconInteractor,
+ )
+ }
+
+ /**
+ * Similar to repository switching, this allows us to split up the logic of satellite/cellular
+ * states, since they are different by nature
+ */
+ private val vmProvider: Flow<MobileIconViewModelCommon> =
+ iconInteractor.isNonTerrestrial
+ .mapLatest { nonTerrestrial ->
+ if (nonTerrestrial) {
+ satelliteProvider
+ } else {
+ cellProvider
+ }
+ }
+ .stateIn(scope, SharingStarted.WhileSubscribed(), cellProvider)
+
+ override val isVisible: StateFlow<Boolean> =
+ vmProvider
+ .flatMapLatest { it.isVisible }
+ .stateIn(scope, SharingStarted.WhileSubscribed(), false)
+
+ override val icon: Flow<SignalIconModel> = vmProvider.flatMapLatest { it.icon }
+
+ override val contentDescription: Flow<ContentDescription> =
+ vmProvider.flatMapLatest { it.contentDescription }
+
+ override val roaming: Flow<Boolean> = vmProvider.flatMapLatest { it.roaming }
+
+ override val networkTypeIcon: Flow<Icon.Resource?> =
+ vmProvider.flatMapLatest { it.networkTypeIcon }
+
+ override val networkTypeBackground: StateFlow<Icon.Resource?> =
+ vmProvider
+ .flatMapLatest { it.networkTypeBackground }
+ .stateIn(scope, SharingStarted.WhileSubscribed(), null)
+
+ override val activityInVisible: Flow<Boolean> =
+ vmProvider.flatMapLatest { it.activityInVisible }
+
+ override val activityOutVisible: Flow<Boolean> =
+ vmProvider.flatMapLatest { it.activityOutVisible }
+
+ override val activityContainerVisible: Flow<Boolean> =
+ vmProvider.flatMapLatest { it.activityContainerVisible }
+}
+
+/** Representation of this network when it is non-terrestrial (e.g., satellite) */
+private class CarrierBasedSatelliteViewModelImpl(
+ override val subscriptionId: Int,
+ interactor: MobileIconInteractor,
+) : MobileIconViewModelCommon {
+ override val isVisible: StateFlow<Boolean> = MutableStateFlow(true)
+ override val icon: Flow<SignalIconModel> = interactor.signalLevelIcon
+
+ override val contentDescription: Flow<ContentDescription> =
+ MutableStateFlow(ContentDescription.Loaded(""))
+
+ /** These fields are not used for satellite icons currently */
+ override val roaming: Flow<Boolean> = flowOf(false)
+ override val networkTypeIcon: Flow<Icon.Resource?> = flowOf(null)
+ override val networkTypeBackground: StateFlow<Icon.Resource?> = MutableStateFlow(null)
+ override val activityInVisible: Flow<Boolean> = flowOf(false)
+ override val activityOutVisible: Flow<Boolean> = flowOf(false)
+ override val activityContainerVisible: Flow<Boolean> = flowOf(false)
+}
+
+/** Terrestrial (cellular) icon. */
+@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
+@OptIn(ExperimentalCoroutinesApi::class)
+private class CellularIconViewModel(
+ override val subscriptionId: Int,
+ iconInteractor: MobileIconInteractor,
+ airplaneModeInteractor: AirplaneModeInteractor,
+ constants: ConnectivityConstants,
+ flags: FeatureFlagsClassic,
+ scope: CoroutineScope,
+) : MobileIconViewModelCommon {
override val isVisible: StateFlow<Boolean> =
if (!constants.hasDataCapabilities) {
flowOf(false)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/model/SatelliteIconModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/model/SatelliteIconModel.kt
index 6938d66..63566ee 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/model/SatelliteIconModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/model/SatelliteIconModel.kt
@@ -28,7 +28,7 @@
fun fromConnectionState(
connectionState: SatelliteConnectionState,
signalStrength: Int,
- ): Icon? =
+ ): Icon.Resource? =
when (connectionState) {
// TODO(b/316635648): check if this should be null
SatelliteConnectionState.Unknown,
@@ -41,9 +41,13 @@
SatelliteConnectionState.Connected -> fromSignalStrength(signalStrength)
}
- private fun fromSignalStrength(
+ /**
+ * Satellite icon appropriate for when we are connected. Use [fromConnectionState] for a more
+ * generally correct representation.
+ */
+ fun fromSignalStrength(
signalStrength: Int,
- ): Icon? =
+ ): Icon.Resource? =
// TODO(b/316634365): these need content descriptions
when (signalStrength) {
// No signal
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/InternetTileViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/InternetTileViewModel.kt
index ae58398..352413e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/InternetTileViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/InternetTileViewModel.kt
@@ -19,6 +19,7 @@
import android.content.Context
import android.text.Html
import com.android.systemui.common.shared.model.ContentDescription
+import com.android.systemui.common.shared.model.ContentDescription.Companion.loadContentDescription
import com.android.systemui.common.shared.model.Text
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
@@ -28,6 +29,7 @@
import com.android.systemui.statusbar.pipeline.airplane.data.repository.AirplaneModeRepository
import com.android.systemui.statusbar.pipeline.ethernet.domain.EthernetInteractor
import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor
+import com.android.systemui.statusbar.pipeline.mobile.domain.model.SignalIconModel
import com.android.systemui.statusbar.pipeline.shared.data.repository.ConnectivityRepository
import com.android.systemui.statusbar.pipeline.shared.ui.model.InternetTileModel
import com.android.systemui.statusbar.pipeline.shared.ui.model.SignalIcon
@@ -116,14 +118,31 @@
it.signalLevelIcon,
mobileDataContentName,
) { networkNameModel, signalIcon, dataContentDescription ->
- val secondary =
- mobileDataContentConcat(networkNameModel.name, dataContentDescription)
- InternetTileModel.Active(
- secondaryTitle = secondary,
- icon = SignalIcon(signalIcon.toSignalDrawableState()),
- stateDescription = ContentDescription.Loaded(secondary.toString()),
- contentDescription = ContentDescription.Loaded(internetLabel),
- )
+ when (signalIcon) {
+ is SignalIconModel.Cellular -> {
+ val secondary =
+ mobileDataContentConcat(
+ networkNameModel.name,
+ dataContentDescription
+ )
+ InternetTileModel.Active(
+ secondaryTitle = secondary,
+ icon = SignalIcon(signalIcon.toSignalDrawableState()),
+ stateDescription = ContentDescription.Loaded(secondary.toString()),
+ contentDescription = ContentDescription.Loaded(internetLabel),
+ )
+ }
+ is SignalIconModel.Satellite -> {
+ val secondary =
+ signalIcon.icon.contentDescription.loadContentDescription(context)
+ InternetTileModel.Active(
+ secondaryTitle = secondary,
+ iconId = signalIcon.icon.res,
+ stateDescription = ContentDescription.Loaded(secondary),
+ contentDescription = ContentDescription.Loaded(internetLabel),
+ )
+ }
+ }
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseHeadsUpManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseHeadsUpManager.java
index 1528c9b..1414150 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseHeadsUpManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseHeadsUpManager.java
@@ -159,7 +159,7 @@
public void showNotification(@NonNull NotificationEntry entry) {
mLogger.logShowNotification(entry);
addEntry(entry);
- updateNotification(entry.getKey(), true /* show */);
+ updateNotification(entry.getKey(), true /* shouldHeadsUpAgain */);
entry.setInterruption();
}
@@ -190,12 +190,12 @@
/**
* Called when the notification state has been updated.
* @param key the key of the entry that was updated
- * @param show whether the notification should show again and force reevaluation of
- * removal time
+ * @param shouldHeadsUpAgain whether the notification should show again and force reevaluation
+ * of removal time
*/
- public void updateNotification(@NonNull String key, boolean show) {
+ public void updateNotification(@NonNull String key, boolean shouldHeadsUpAgain) {
HeadsUpEntry headsUpEntry = mHeadsUpEntryMap.get(key);
- mLogger.logUpdateNotification(key, show, headsUpEntry != null);
+ mLogger.logUpdateNotification(key, shouldHeadsUpAgain, headsUpEntry != null);
if (headsUpEntry == null) {
// the entry was released before this update (i.e by a listener) This can happen
// with the groupmanager
@@ -204,7 +204,7 @@
headsUpEntry.mEntry.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
- if (show) {
+ if (shouldHeadsUpAgain) {
headsUpEntry.updateEntry(true /* updatePostTime */, "updateNotification");
if (headsUpEntry != null) {
setEntryPinned(headsUpEntry, shouldHeadsUpBecomePinned(headsUpEntry.mEntry));
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/Clock.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/Clock.java
index 20d1fff..a2d8d15 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/Clock.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/Clock.java
@@ -28,6 +28,8 @@
import android.icu.text.DateTimePatternGenerator;
import android.os.Bundle;
import android.os.Handler;
+import android.os.HandlerExecutor;
+import android.os.HandlerThread;
import android.os.Parcelable;
import android.os.SystemClock;
import android.os.UserHandle;
@@ -48,11 +50,11 @@
import com.android.settingslib.Utils;
import com.android.systemui.Dependency;
import com.android.systemui.FontSizeUtils;
-import com.android.systemui.res.R;
import com.android.systemui.broadcast.BroadcastDispatcher;
import com.android.systemui.demomode.DemoModeCommandReceiver;
import com.android.systemui.plugins.DarkIconDispatcher;
import com.android.systemui.plugins.DarkIconDispatcher.DarkReceiver;
+import com.android.systemui.res.R;
import com.android.systemui.settings.UserTracker;
import com.android.systemui.statusbar.CommandQueue;
import com.android.systemui.statusbar.phone.StatusBarIconController;
@@ -106,6 +108,7 @@
private final int mAmPmStyle;
private boolean mShowSeconds;
private Handler mSecondsHandler;
+ private HandlerThread mHandlerThread;
// Fields to cache the width so the clock remains at an approximately constant width
private int mCharsAtCurrentWidth = -1;
@@ -146,6 +149,8 @@
}
mBroadcastDispatcher = Dependency.get(BroadcastDispatcher.class);
mUserTracker = Dependency.get(UserTracker.class);
+ mHandlerThread = new HandlerThread("Clock");
+ mHandlerThread.start();
setIncludeFontPadding(false);
}
@@ -205,7 +210,8 @@
Dependency.get(TunerService.class).addTunable(this, CLOCK_SECONDS,
StatusBarIconController.ICON_HIDE_LIST);
mCommandQueue.addCallback(this);
- mUserTracker.addCallback(mUserChangedCallback, mContext.getMainExecutor());
+ mUserTracker.addCallback(mUserChangedCallback,
+ new HandlerExecutor(mHandlerThread.getThreadHandler()));
mCurrentUserId = mUserTracker.getUserId();
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManager.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManager.kt
index b8c7e20..a7352be 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManager.kt
@@ -182,7 +182,7 @@
*/
fun unpinAll(userUnPinned: Boolean)
- fun updateNotification(key: String, alert: Boolean)
+ fun updateNotification(key: String, shouldHeadsUpAgain: Boolean)
}
/** Sets the animation state of the HeadsUpManager. */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/NextAlarmControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/NextAlarmControllerImpl.java
index b7d8ee3..a7440d6 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/NextAlarmControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/NextAlarmControllerImpl.java
@@ -21,6 +21,8 @@
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
+import android.os.HandlerExecutor;
+import android.os.HandlerThread;
import android.os.UserHandle;
import androidx.annotation.NonNull;
@@ -51,6 +53,7 @@
private final UserTracker mUserTracker;
private AlarmManager mAlarmManager;
private AlarmManager.AlarmClockInfo mNextAlarm;
+ private HandlerThread mHandlerThread;
private final UserTracker.Callback mUserChangedCallback =
new UserTracker.Callback() {
@@ -75,7 +78,10 @@
IntentFilter filter = new IntentFilter();
filter.addAction(AlarmManager.ACTION_NEXT_ALARM_CLOCK_CHANGED);
broadcastDispatcher.registerReceiver(this, filter, null, UserHandle.ALL);
- mUserTracker.addCallback(mUserChangedCallback, mainExecutor);
+ mHandlerThread = new HandlerThread("NextAlarmControllerImpl");
+ mHandlerThread.start();
+ mUserTracker.addCallback(mUserChangedCallback,
+ new HandlerExecutor(mHandlerThread.getThreadHandler()));
updateNextAlarm();
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SecurityControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SecurityControllerImpl.java
index 9f4a906..6a6efbc 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SecurityControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SecurityControllerImpl.java
@@ -157,7 +157,7 @@
// TODO: re-register network callback on user change.
mConnectivityManager.registerNetworkCallback(REQUEST, mNetworkCallback);
onUserSwitched(mUserTracker.getUserId());
- mUserTracker.addCallback(mUserChangedCallback, mMainExecutor);
+ mUserTracker.addCallback(mUserChangedCallback, mBgExecutor);
}
public void dump(PrintWriter pw, String[] args) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionController.java
new file mode 100644
index 0000000..970cc75
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionController.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.policy;
+
+import com.android.systemui.statusbar.notification.collection.NotificationEntry;
+
+/**
+ * A controller which provides the current sensitive notification protections status as well as
+ * to assist in feature usage and exemptions
+ */
+public interface SensitiveNotificationProtectionController {
+ /**
+ * Register a runnable that triggers on changes to protection state
+ *
+ * <p> onSensitiveStateChanged not invoked on registration
+ */
+ void registerSensitiveStateListener(Runnable onSensitiveStateChanged);
+
+ /** Unregister a previously registered onSensitiveStateChanged runnable */
+ void unregisterSensitiveStateListener(Runnable onSensitiveStateChanged);
+
+ /** Return {@code true} if device in state in which notifications should be protected */
+ boolean isSensitiveStateActive();
+
+ /** Return {@code true} when notification should be protected */
+ boolean shouldProtectNotification(NotificationEntry entry);
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerImpl.java
new file mode 100644
index 0000000..3c4ca44
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerImpl.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.policy;
+
+import static com.android.systemui.Flags.screenshareNotificationHiding;
+
+import android.media.projection.MediaProjectionInfo;
+import android.media.projection.MediaProjectionManager;
+import android.os.Handler;
+import android.os.Trace;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.systemui.dagger.SysUISingleton;
+import com.android.systemui.dagger.qualifiers.Main;
+import com.android.systemui.statusbar.notification.collection.NotificationEntry;
+import com.android.systemui.util.ListenerSet;
+
+import javax.inject.Inject;
+
+/** Implementation of SensitiveNotificationProtectionController. **/
+@SysUISingleton
+public class SensitiveNotificationProtectionControllerImpl
+ implements SensitiveNotificationProtectionController {
+ private final MediaProjectionManager mMediaProjectionManager;
+ private final ListenerSet<Runnable> mListeners = new ListenerSet<>();
+ private volatile MediaProjectionInfo mProjection;
+
+ @VisibleForTesting
+ final MediaProjectionManager.Callback mMediaProjectionCallback =
+ new MediaProjectionManager.Callback() {
+ @Override
+ public void onStart(MediaProjectionInfo info) {
+ Trace.beginSection(
+ "SNPC.onProjectionStart");
+ mProjection = info;
+ mListeners.forEach(Runnable::run);
+ Trace.endSection();
+ }
+
+ @Override
+ public void onStop(MediaProjectionInfo info) {
+ Trace.beginSection(
+ "SNPC.onProjectionStop");
+ mProjection = null;
+ mListeners.forEach(Runnable::run);
+ Trace.endSection();
+ }
+ };
+
+ @Inject
+ public SensitiveNotificationProtectionControllerImpl(
+ MediaProjectionManager mediaProjectionManager,
+ @Main Handler mainHandler) {
+ mMediaProjectionManager = mediaProjectionManager;
+
+ if (screenshareNotificationHiding()) {
+ mMediaProjectionManager.addCallback(mMediaProjectionCallback, mainHandler);
+ }
+ }
+
+ @Override
+ public void registerSensitiveStateListener(Runnable onSensitiveStateChanged) {
+ mListeners.addIfAbsent(onSensitiveStateChanged);
+ }
+
+ @Override
+ public void unregisterSensitiveStateListener(Runnable onSensitiveStateChanged) {
+ mListeners.remove(onSensitiveStateChanged);
+ }
+
+ @Override
+ public boolean isSensitiveStateActive() {
+ // TODO(b/316955558): Add disabled by developer option
+ // TODO(b/316955306): Add feature exemption for sysui and bug handlers
+ // TODO(b/316955346): Add feature exemption for single app screen sharing
+ return mProjection != null;
+ }
+
+ @Override
+ public boolean shouldProtectNotification(NotificationEntry entry) {
+ if (!isSensitiveStateActive()) {
+ return false;
+ }
+
+ // Exempt foreground service notifications from protection in effort to keep screen share
+ // stop actions easily accessible
+ // TODO(b/316955208): Exempt FGS notifications only for app that started projection
+ return !entry.getSbn().getNotification().isFgsOrUij();
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserInfoControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserInfoControllerImpl.java
index 2ed9d15..0bc0e88 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserInfoControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserInfoControllerImpl.java
@@ -36,9 +36,9 @@
import com.android.internal.util.UserIcons;
import com.android.settingslib.drawable.UserIconDrawable;
-import com.android.systemui.res.R;
import com.android.systemui.dagger.SysUISingleton;
-import com.android.systemui.dagger.qualifiers.Main;
+import com.android.systemui.dagger.qualifiers.Background;
+import com.android.systemui.res.R;
import com.android.systemui.settings.UserTracker;
import java.util.ArrayList;
@@ -66,11 +66,11 @@
/**
*/
@Inject
- public UserInfoControllerImpl(Context context, @Main Executor mainExecutor,
+ public UserInfoControllerImpl(Context context, @Background Executor bgExecutor,
UserTracker userTracker) {
mContext = context;
mUserTracker = userTracker;
- mUserTracker.addCallback(mUserChangedCallback, mainExecutor);
+ mUserTracker.addCallback(mUserChangedCallback, bgExecutor);
IntentFilter profileFilter = new IntentFilter();
profileFilter.addAction(ContactsContract.Intents.ACTION_PROFILE_CHANGED);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ZenModeControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ZenModeControllerImpl.java
index df210b0..f0b4930 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ZenModeControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ZenModeControllerImpl.java
@@ -29,6 +29,7 @@
import android.net.Uri;
import android.os.Handler;
import android.os.HandlerExecutor;
+import android.os.HandlerThread;
import android.os.Trace;
import android.os.UserHandle;
import android.os.UserManager;
@@ -81,6 +82,7 @@
private volatile int mZenMode;
private long mZenUpdateTime;
private NotificationManager.Policy mConsolidatedNotificationPolicy;
+ private HandlerThread mHandlerThread;
private final UserTracker.Callback mUserChangedCallback =
new UserTracker.Callback() {
@@ -133,6 +135,8 @@
}
}
};
+ mHandlerThread = new HandlerThread("ZenModeControllerImpl");
+ mHandlerThread.start();
mNoMan = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
globalSettings.registerContentObserver(Global.ZEN_MODE, modeContentObserver);
updateZenMode(getModeSettingValueFromProvider());
@@ -143,7 +147,8 @@
mSetupObserver = new SetupObserver(handler);
mSetupObserver.register();
mUserManager = context.getSystemService(UserManager.class);
- mUserTracker.addCallback(mUserChangedCallback, new HandlerExecutor(handler));
+ mUserTracker.addCallback(mUserChangedCallback,
+ new HandlerExecutor(mHandlerThread.getThreadHandler()));
// This registers the alarm broadcast receiver for the current user
mUserChangedCallback.onUserChanged(getCurrentUser(), context);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/dagger/StatusBarPolicyModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/dagger/StatusBarPolicyModule.java
index 3304b98..15200bd 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/dagger/StatusBarPolicyModule.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/dagger/StatusBarPolicyModule.java
@@ -60,6 +60,8 @@
import com.android.systemui.statusbar.policy.RotationLockControllerImpl;
import com.android.systemui.statusbar.policy.SecurityController;
import com.android.systemui.statusbar.policy.SecurityControllerImpl;
+import com.android.systemui.statusbar.policy.SensitiveNotificationProtectionController;
+import com.android.systemui.statusbar.policy.SensitiveNotificationProtectionControllerImpl;
import com.android.systemui.statusbar.policy.SplitShadeStateController;
import com.android.systemui.statusbar.policy.SplitShadeStateControllerImpl;
import com.android.systemui.statusbar.policy.UserInfoController;
@@ -146,6 +148,11 @@
/** */
@Binds
+ SensitiveNotificationProtectionController provideSensitiveNotificationProtectionController(
+ SensitiveNotificationProtectionControllerImpl controllerImpl);
+
+ /** */
+ @Binds
UserInfoController provideUserInfoContrller(UserInfoControllerImpl controllerImpl);
/** */
diff --git a/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java b/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java
index 2b9ad50..77518db 100644
--- a/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java
+++ b/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java
@@ -480,7 +480,7 @@
return;
}
- mUserTracker.addCallback(mUserTrackerCallback, mMainExecutor);
+ mUserTracker.addCallback(mUserTrackerCallback, mBgExecutor);
mDeviceProvisionedController.addCallback(mDeviceProvisionedListener);
diff --git a/packages/SystemUI/src/com/android/systemui/tuner/TunerServiceImpl.java b/packages/SystemUI/src/com/android/systemui/tuner/TunerServiceImpl.java
index 550a65c..f5b4d17 100644
--- a/packages/SystemUI/src/com/android/systemui/tuner/TunerServiceImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/tuner/TunerServiceImpl.java
@@ -26,6 +26,7 @@
import android.net.Uri;
import android.os.Handler;
import android.os.HandlerExecutor;
+import android.os.HandlerThread;
import android.os.Looper;
import android.os.UserManager;
import android.provider.Settings;
@@ -38,11 +39,11 @@
import com.android.internal.util.ArrayUtils;
import com.android.systemui.DejankUtils;
-import com.android.systemui.res.R;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.demomode.DemoModeController;
import com.android.systemui.qs.QSHost;
+import com.android.systemui.res.R;
import com.android.systemui.settings.UserTracker;
import com.android.systemui.statusbar.phone.StatusBarIconController;
import com.android.systemui.statusbar.phone.SystemUIDialog;
@@ -98,6 +99,7 @@
private UserTracker.Callback mCurrentUserTracker;
private UserTracker mUserTracker;
private final ComponentName mTunerComponent;
+ private HandlerThread mHandlerThread;
/**
*/
@@ -117,7 +119,8 @@
mDemoModeController = demoModeController;
mUserTracker = userTracker;
mTunerComponent = new ComponentName(mContext, TunerActivity.class);
-
+ mHandlerThread = new HandlerThread("TunerServiceImpl");
+ mHandlerThread.start();
for (UserInfo user : UserManager.get(mContext).getUsers()) {
mCurrentUser = user.getUserHandle().getIdentifier();
if (getValue(TUNER_VERSION, 0) != CURRENT_TUNER_VERSION) {
@@ -135,7 +138,7 @@
}
};
mUserTracker.addCallback(mCurrentUserTracker,
- new HandlerExecutor(mainHandler));
+ new HandlerExecutor(mHandlerThread.getThreadHandler()));
}
@Override
diff --git a/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt b/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt
index cf76c0d..74e1339 100644
--- a/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt
@@ -190,7 +190,7 @@
}
}
- tracker.addCallback(callback, mainDispatcher.asExecutor())
+ tracker.addCallback(callback, backgroundDispatcher.asExecutor())
send(currentSelectionStatus)
awaitClose { tracker.removeCallback(callback) }
diff --git a/packages/SystemUI/src/com/android/systemui/util/kotlin/BooleanFlowOperators.kt b/packages/SystemUI/src/com/android/systemui/util/kotlin/BooleanFlowOperators.kt
new file mode 100644
index 0000000..693a835
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/util/kotlin/BooleanFlowOperators.kt
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.util.kotlin
+
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.map
+
+object BooleanFlowOperators {
+ /**
+ * Logical AND operator for boolean flows. Will collect all flows and [combine] them to
+ * determine the result.
+ *
+ * Usage:
+ * ```
+ * val result = and(flow1, flow2)
+ * ```
+ */
+ fun and(vararg flows: Flow<Boolean>): Flow<Boolean> =
+ combine(flows.asIterable()) { values -> values.all { it } }
+
+ /**
+ * Logical NOT operator for a boolean flow.
+ *
+ * Usage:
+ * ```
+ * val negatedFlow = not(flow)
+ * ```
+ */
+ fun not(flow: Flow<Boolean>) = flow.map { !it }
+
+ /**
+ * Logical OR operator for a boolean flow. Will collect all flows and [combine] them to
+ * determine the result.
+ */
+ fun or(vararg flows: Flow<Boolean>): Flow<Boolean> =
+ combine(flows.asIterable()) { values -> values.any { it } }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/util/kotlin/Dagger.kt b/packages/SystemUI/src/com/android/systemui/util/kotlin/Dagger.kt
index c587f2e..5150389 100644
--- a/packages/SystemUI/src/com/android/systemui/util/kotlin/Dagger.kt
+++ b/packages/SystemUI/src/com/android/systemui/util/kotlin/Dagger.kt
@@ -17,6 +17,7 @@
package com.android.systemui.util.kotlin
import dagger.Lazy
+import java.util.Optional
import kotlin.reflect.KProperty
/**
@@ -30,3 +31,16 @@
* ```
*/
operator fun <T> Lazy<T>.getValue(thisRef: Any?, property: KProperty<*>): T = get()
+
+/**
+ * Extension operator that allows developers to use [java.util.Optional] as a nullable property
+ * delegate:
+ * ```kotlin
+ * class MyClass @Inject constructor(
+ * optionalDependency: Optional<Foo>,
+ * ) {
+ * val dependency: Foo? by optionalDependency
+ * }
+ * ```
+ */
+operator fun <T> Optional<T>.getValue(thisRef: Any?, property: KProperty<*>): T? = getOrNull()
diff --git a/packages/SystemUI/src/com/android/systemui/util/kotlin/Utils.kt b/packages/SystemUI/src/com/android/systemui/util/kotlin/Utils.kt
index 2336a8e..6993c96 100644
--- a/packages/SystemUI/src/com/android/systemui/util/kotlin/Utils.kt
+++ b/packages/SystemUI/src/com/android/systemui/util/kotlin/Utils.kt
@@ -28,9 +28,13 @@
fun <A, B, C, D> toQuad(a: A, bcd: Triple<B, C, D>) =
Quad(a, bcd.first, bcd.second, bcd.third)
+ fun <A, B, C, D, E> toQuint(a: A, b: B, c: C, d: D, e: E) = Quint(a, b, c, d, e)
fun <A, B, C, D, E> toQuint(a: A, bcde: Quad<B, C, D, E>) =
Quint(a, bcde.first, bcde.second, bcde.third, bcde.fourth)
+ fun <A, B, C, D, E, F> toSextuple(a: A, bcdef: Quint<B, C, D, E, F>) =
+ Sextuple(a, bcdef.first, bcdef.second, bcdef.third, bcdef.fourth, bcdef.fifth)
+
/**
* Samples the provided flows, emitting a tuple of the original flow's value as well as each
* of the combined flows' values.
@@ -69,6 +73,22 @@
): Flow<Quint<A, B, C, D, E>> {
return this.sample(combine(b, c, d, e, ::Quad), ::toQuint)
}
+
+ /**
+ * Samples the provided flows, emitting a tuple of the original flow's value as well as each
+ * of the combined flows' values.
+ *
+ * Flow<A>.sample(Flow<B>, Flow<C>, Flow<D>, Flow<E>, Flow<F>) -> (A, B, C, D, E, F)
+ */
+ fun <A, B, C, D, E, F> Flow<A>.sample(
+ b: Flow<B>,
+ c: Flow<C>,
+ d: Flow<D>,
+ e: Flow<E>,
+ f: Flow<F>,
+ ): Flow<Sextuple<A, B, C, D, E, F>> {
+ return this.sample(combine(b, c, d, e, f, ::Quint), ::toSextuple)
+ }
}
}
@@ -81,3 +101,12 @@
val fourth: D,
val fifth: E
)
+
+data class Sextuple<A, B, C, D, E, F>(
+ val first: A,
+ val second: B,
+ val third: C,
+ val fourth: D,
+ val fifth: E,
+ val sixth: F,
+)
diff --git a/packages/SystemUI/src/com/android/systemui/util/wakelock/DelayedWakeLock.java b/packages/SystemUI/src/com/android/systemui/util/wakelock/DelayedWakeLock.java
index 972895d..039109e 100644
--- a/packages/SystemUI/src/com/android/systemui/util/wakelock/DelayedWakeLock.java
+++ b/packages/SystemUI/src/com/android/systemui/util/wakelock/DelayedWakeLock.java
@@ -19,7 +19,11 @@
import android.content.Context;
import android.os.Handler;
-import javax.inject.Inject;
+import com.android.systemui.dagger.qualifiers.Background;
+
+import dagger.assisted.Assisted;
+import dagger.assisted.AssistedFactory;
+import dagger.assisted.AssistedInject;
/**
* A wake lock that has a built in delay when releasing to give the framebuffer time to update.
@@ -32,9 +36,11 @@
private final Handler mHandler;
private final WakeLock mInner;
- public DelayedWakeLock(Handler h, WakeLock inner) {
- mHandler = h;
- mInner = inner;
+ @AssistedInject
+ public DelayedWakeLock(@Background Handler handler, Context context, WakeLockLogger logger,
+ @Assisted String tag) {
+ mInner = WakeLock.createPartial(context, logger, tag);
+ mHandler = handler;
}
@Override
@@ -58,46 +64,11 @@
}
/**
- * An injectable builder for {@see DelayedWakeLock} that has the context already filled in.
+ * Factory to create the instance of DelayedWakeLock class.
*/
- public static class Builder {
- private final Context mContext;
- private final WakeLockLogger mLogger;
- private String mTag;
- private Handler mHandler;
-
- /**
- * Constructor for DelayedWakeLock.Builder
- */
- @Inject
- public Builder(Context context, WakeLockLogger logger) {
- mContext = context;
- mLogger = logger;
- }
-
- /**
- * Set the tag for the WakeLock.
- */
- public Builder setTag(String tag) {
- mTag = tag;
-
- return this;
- }
-
- /**
- * Set the handler for the DelayedWakeLock.
- */
- public Builder setHandler(Handler handler) {
- mHandler = handler;
-
- return this;
- }
-
- /**
- * Build the DelayedWakeLock.
- */
- public DelayedWakeLock build() {
- return new DelayedWakeLock(mHandler, WakeLock.createPartial(mContext, mLogger, mTag));
- }
+ @AssistedFactory
+ public interface Factory {
+ /** creates the instance of DelayedWakeLock class. */
+ DelayedWakeLock create(String tag);
}
-}
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java
index 9ee3d22..aee441a 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java
@@ -535,6 +535,7 @@
}
if (changed && fromKey) {
Events.writeEvent(Events.EVENT_KEY, stream, lastAudibleStreamVolume);
+ mCallbacks.onVolumeChangedFromKey();
}
return changed;
}
@@ -1030,6 +1031,18 @@
}
@Override
+ public void onVolumeChangedFromKey() {
+ for (final Map.Entry<Callbacks, Handler> entry : mCallbackMap.entrySet()) {
+ entry.getValue().post(new Runnable() {
+ @Override
+ public void run() {
+ entry.getKey().onVolumeChangedFromKey();
+ }
+ });
+ }
+ }
+
+ @Override
public void onAccessibilityModeChanged(Boolean showA11yStream) {
boolean show = showA11yStream != null && showA11yStream;
for (final Map.Entry<Callbacks, Handler> entry : mCallbackMap.entrySet()) {
diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
index 404621d..ce6d740 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
@@ -34,6 +34,7 @@
import static com.android.internal.jank.InteractionJankMonitor.CUJ_VOLUME_CONTROL;
import static com.android.internal.jank.InteractionJankMonitor.Configuration.Builder;
+import static com.android.systemui.Flags.hapticVolumeSlider;
import static com.android.systemui.volume.Events.DISMISS_REASON_POSTURE_CHANGED;
import static com.android.systemui.volume.Events.DISMISS_REASON_SETTINGS_CLICKED;
@@ -49,7 +50,6 @@
import android.content.ContentResolver;
import android.content.Context;
import android.content.DialogInterface;
-import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.res.ColorStateList;
import android.content.res.Configuration;
@@ -76,7 +76,6 @@
import android.provider.Settings;
import android.provider.Settings.Global;
import android.text.InputFilter;
-import android.util.FeatureFlagUtils;
import android.util.Log;
import android.util.Slog;
import android.util.SparseBooleanArray;
@@ -117,14 +116,18 @@
import com.android.settingslib.Utils;
import com.android.systemui.Dumpable;
import com.android.systemui.Prefs;
+import com.android.systemui.dagger.qualifiers.Application;
+import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.dump.DumpManager;
+import com.android.systemui.haptics.slider.SeekableSliderHapticPlugin;
+import com.android.systemui.haptics.slider.SliderHapticFeedbackConfig;
import com.android.systemui.media.dialog.MediaOutputDialogFactory;
-import com.android.systemui.plugins.ActivityStarter;
import com.android.systemui.plugins.VolumeDialog;
import com.android.systemui.plugins.VolumeDialogController;
import com.android.systemui.plugins.VolumeDialogController.State;
import com.android.systemui.plugins.VolumeDialogController.StreamState;
import com.android.systemui.res.R;
+import com.android.systemui.statusbar.VibratorHelper;
import com.android.systemui.statusbar.policy.AccessibilityManagerWrapper;
import com.android.systemui.statusbar.policy.ConfigurationController;
import com.android.systemui.statusbar.policy.DevicePostureController;
@@ -132,6 +135,8 @@
import com.android.systemui.util.AlphaTintDrawableWrapper;
import com.android.systemui.util.RoundedCornerProgressDrawable;
import com.android.systemui.util.settings.SecureSettings;
+import com.android.systemui.volume.domain.interactor.VolumePanelNavigationInteractor;
+import com.android.systemui.volume.ui.navigation.VolumeNavigator;
import dagger.Lazy;
@@ -140,6 +145,9 @@
import java.util.List;
import java.util.function.Consumer;
+import kotlinx.coroutines.CoroutineDispatcher;
+import kotlinx.coroutines.CoroutineScope;
+
/**
* Visual presentation of the volume dialog.
*
@@ -262,9 +270,9 @@
private final Accessibility mAccessibility = new Accessibility();
private final ConfigurationController mConfigurationController;
private final MediaOutputDialogFactory mMediaOutputDialogFactory;
- private final VolumePanelFactory mVolumePanelFactory;
private final CsdWarningDialog.Factory mCsdWarningDialogFactory;
- private final ActivityStarter mActivityStarter;
+ private final VolumePanelNavigationInteractor mVolumePanelNavigationInteractor;
+ private final VolumeNavigator mVolumeNavigator;
private boolean mShowing;
private boolean mShowA11yStream;
private int mActiveStream;
@@ -303,6 +311,10 @@
private int mOrientation;
private final Lazy<SecureSettings> mSecureSettings;
private int mDialogTimeoutMillis;
+ private final CoroutineDispatcher mMainDispatcher;
+ private final CoroutineScope mApplicationScope;
+ private final VibratorHelper mVibratorHelper;
+ private final com.android.systemui.util.time.SystemClock mSystemClock;
public VolumeDialogImpl(
Context context,
@@ -311,19 +323,26 @@
DeviceProvisionedController deviceProvisionedController,
ConfigurationController configurationController,
MediaOutputDialogFactory mediaOutputDialogFactory,
- VolumePanelFactory volumePanelFactory,
- ActivityStarter activityStarter,
InteractionJankMonitor interactionJankMonitor,
+ VolumePanelNavigationInteractor volumePanelNavigationInteractor,
+ VolumeNavigator volumeNavigator,
boolean shouldListenForJank,
CsdWarningDialog.Factory csdWarningDialogFactory,
DevicePostureController devicePostureController,
Looper looper,
DumpManager dumpManager,
- Lazy<SecureSettings> secureSettings) {
+ Lazy<SecureSettings> secureSettings,
+ VibratorHelper vibratorHelper,
+ @Main CoroutineDispatcher mainDispatcher,
+ @Application CoroutineScope applicationScope,
+ com.android.systemui.util.time.SystemClock systemClock) {
mContext =
new ContextThemeWrapper(context, R.style.volume_dialog_theme);
mHandler = new H(looper);
-
+ mMainDispatcher = mainDispatcher;
+ mApplicationScope = applicationScope;
+ mVibratorHelper = vibratorHelper;
+ mSystemClock = systemClock;
mShouldListenForJank = shouldListenForJank;
mController = volumeDialogController;
mKeyguard = (KeyguardManager) mContext.getSystemService(Context.KEYGUARD_SERVICE);
@@ -332,9 +351,7 @@
mDeviceProvisionedController = deviceProvisionedController;
mConfigurationController = configurationController;
mMediaOutputDialogFactory = mediaOutputDialogFactory;
- mVolumePanelFactory = volumePanelFactory;
mCsdWarningDialogFactory = csdWarningDialogFactory;
- mActivityStarter = activityStarter;
mShowActiveStreamOnly = showActiveStreamOnly();
mHasSeenODICaptionsTooltip =
Prefs.getBoolean(context, Prefs.Key.HAS_SEEN_ODI_CAPTIONS_TOOLTIP, false);
@@ -349,6 +366,8 @@
mUseBackgroundBlur =
mContext.getResources().getBoolean(R.bool.config_volumeDialogUseBackgroundBlur);
mInteractionJankMonitor = interactionJankMonitor;
+ mVolumePanelNavigationInteractor = volumePanelNavigationInteractor;
+ mVolumeNavigator = volumeNavigator;
mSecureSettings = secureSettings;
mDialogTimeoutMillis = DIALOG_TIMEOUT_MILLIS;
@@ -839,6 +858,7 @@
row.header.setFilters(new InputFilter[] {new InputFilter.LengthFilter(13)});
}
row.slider = row.view.findViewById(R.id.volume_row_slider);
+ row.createPlugin(mVibratorHelper, mSystemClock, mMainDispatcher, mApplicationScope);
row.slider.setOnSeekBarChangeListener(new VolumeSeekBarChangeListener(row));
row.number = row.view.findViewById(R.id.volume_number);
@@ -1187,13 +1207,8 @@
Events.writeEvent(Events.EVENT_SETTINGS_CLICK);
dismissH(DISMISS_REASON_SETTINGS_CLICKED);
mMediaOutputDialogFactory.dismiss();
- if (FeatureFlagUtils.isEnabled(mContext,
- FeatureFlagUtils.SETTINGS_VOLUME_PANEL_IN_SYSTEMUI)) {
- mVolumePanelFactory.create(true /* aboveStatusBar */, null);
- } else {
- mActivityStarter.startActivity(new Intent(Settings.Panel.ACTION_VOLUME),
- true /* dismissShade */);
- }
+ mVolumeNavigator.openVolumePanel(
+ mVolumePanelNavigationInteractor.getVolumePanelRoute());
});
}
}
@@ -1480,6 +1495,12 @@
mController.getCaptionsComponentState(false);
checkODICaptionsTooltip(false);
updateBackgroundForDrawerClosedAmount();
+ for (int i = 0; i < mRows.size(); i++) {
+ VolumeRow row = mRows.get(i);
+ if (row.slider.getVisibility() == VISIBLE) {
+ row.addHaptics();
+ }
+ }
Trace.endSection();
}
@@ -1532,7 +1553,9 @@
protected void dismissH(int reason) {
Trace.beginSection("VolumeDialogImpl#dismissH");
-
+ for (int i = 0; i < mRows.size(); i++) {
+ mRows.get(i).removeHaptics();
+ }
Log.i(TAG, "mDialog.dismiss() reason: " + Events.DISMISS_REASONS[reason]
+ " from: " + Debug.getCaller());
@@ -2358,6 +2381,14 @@
public void onCaptionEnabledStateChanged(Boolean isEnabled, Boolean checkForSwitchState) {
updateCaptionsEnabledH(isEnabled, checkForSwitchState);
}
+
+ @Override
+ public void onVolumeChangedFromKey() {
+ VolumeRow activeRow = getActiveRow();
+ if (activeRow.mHapticPlugin != null) {
+ activeRow.mHapticPlugin.onKeyDown();
+ }
+ }
};
@VisibleForTesting void onPostureChanged(int posture) {
@@ -2459,6 +2490,15 @@
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
if (mRow.ss == null) return;
+ if (getActiveRow().equals(mRow)
+ && mRow.slider.getVisibility() == VISIBLE
+ && mRow.mHapticPlugin != null) {
+ mRow.mHapticPlugin.onProgressChanged(seekBar, progress, fromUser);
+ if (!fromUser) {
+ // Consider a change from program as the volume key being continuously pressed
+ mRow.mHapticPlugin.onKeyDown();
+ }
+ }
if (D.BUG) Log.d(TAG, AudioSystem.streamToString(mRow.stream)
+ " onProgressChanged " + progress + " fromUser=" + fromUser);
if (!fromUser) return;
@@ -2485,6 +2525,9 @@
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
if (D.BUG) Log.d(TAG, "onStartTrackingTouch"+ " " + mRow.stream);
+ if (mRow.mHapticPlugin != null) {
+ mRow.mHapticPlugin.onStartTrackingTouch(seekBar);
+ }
mController.setActiveStream(mRow.stream);
mRow.tracking = true;
}
@@ -2492,6 +2535,9 @@
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
if (D.BUG) Log.d(TAG, "onStopTrackingTouch"+ " " + mRow.stream);
+ if (mRow.mHapticPlugin != null) {
+ mRow.mHapticPlugin.onStopTrackingTouch(seekBar);
+ }
mRow.tracking = false;
mRow.userAttempt = SystemClock.uptimeMillis();
final int userLevel = getImpliedLevel(seekBar, seekBar.getProgress());
@@ -2524,6 +2570,22 @@
}
private static class VolumeRow {
+ private static final SliderHapticFeedbackConfig sSliderHapticFeedbackConfig =
+ new SliderHapticFeedbackConfig(
+ /* velocityInterpolatorFactor= */ 1f,
+ /* progressInterpolatorFactor= */ 1f,
+ /* progressBasedDragMinScale= */ 0f,
+ /* progressBasedDragMaxScale= */ 0.2f,
+ /* additionalVelocityMaxBump= */ 0.15f,
+ /* deltaMillisForDragInterval= */ 0f,
+ /* deltaProgressForDragThreshold= */ 0.015f,
+ /* numberOfLowTicks= */ 5,
+ /* maxVelocityToScale= */ 300f,
+ /* velocityAxis= */ MotionEvent.AXIS_Y,
+ /* upperBookendScale= */ 1f,
+ /* lowerBookendScale= */ 0.05f,
+ /* exponent= */ 1f / 0.89f);
+
private View view;
private TextView header;
private ImageButton icon;
@@ -2544,6 +2606,7 @@
private ObjectAnimator anim; // slider progress animation for non-touch-related updates
private int animTargetProgress;
private int lastAudibleLevel = 1;
+ private SeekableSliderHapticPlugin mHapticPlugin;
void setIcon(int iconRes, Resources.Theme theme) {
if (icon != null) {
@@ -2554,6 +2617,50 @@
sliderProgressIcon.setDrawable(view.getResources().getDrawable(iconRes, theme));
}
}
+
+ void createPlugin(
+ VibratorHelper vibratorHelper,
+ com.android.systemui.util.time.SystemClock systemClock,
+ CoroutineDispatcher mainDispatcher,
+ CoroutineScope applicationScope) {
+ if (!hapticVolumeSlider() || mHapticPlugin != null) return;
+
+ mHapticPlugin = new SeekableSliderHapticPlugin(
+ vibratorHelper,
+ systemClock,
+ mainDispatcher,
+ applicationScope,
+ sSliderHapticFeedbackConfig);
+ }
+
+
+ @SuppressLint("ClickableViewAccessibility")
+ void addTouchListener() {
+ slider.setOnTouchListener(new View.OnTouchListener() {
+ @Override
+ public boolean onTouch(View view, MotionEvent motionEvent) {
+ if (mHapticPlugin != null) {
+ mHapticPlugin.onTouchEvent(motionEvent);
+ }
+ return false;
+ }
+ });
+ }
+
+ void addHaptics() {
+ if (mHapticPlugin != null) {
+ addTouchListener();
+ mHapticPlugin.start();
+ }
+ }
+
+ @SuppressLint("ClickableViewAccessibility")
+ void removeHaptics() {
+ slider.setOnTouchListener(null);
+ if (mHapticPlugin != null) {
+ mHapticPlugin.stop();
+ }
+ }
}
/**
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java b/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java
index 497c4cb..b1bfbe0 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java
+++ b/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java
@@ -16,30 +16,36 @@
package com.android.systemui.volume.dagger;
+import android.app.Activity;
import android.content.Context;
import android.media.AudioManager;
import android.os.Looper;
import com.android.internal.jank.InteractionJankMonitor;
import com.android.systemui.CoreStartable;
+import com.android.systemui.dagger.qualifiers.Application;
+import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.dump.DumpManager;
import com.android.systemui.media.dialog.MediaOutputDialogFactory;
-import com.android.systemui.plugins.ActivityStarter;
import com.android.systemui.plugins.VolumeDialog;
import com.android.systemui.plugins.VolumeDialogController;
+import com.android.systemui.statusbar.VibratorHelper;
import com.android.systemui.statusbar.policy.AccessibilityManagerWrapper;
import com.android.systemui.statusbar.policy.ConfigurationController;
import com.android.systemui.statusbar.policy.DevicePostureController;
import com.android.systemui.statusbar.policy.DeviceProvisionedController;
import com.android.systemui.util.settings.SecureSettings;
+import com.android.systemui.util.time.SystemClock;
import com.android.systemui.volume.CsdWarningDialog;
import com.android.systemui.volume.VolumeComponent;
import com.android.systemui.volume.VolumeDialogComponent;
import com.android.systemui.volume.VolumeDialogImpl;
-import com.android.systemui.volume.VolumePanelFactory;
import com.android.systemui.volume.VolumeUI;
+import com.android.systemui.volume.domain.interactor.VolumePanelNavigationInteractor;
import com.android.systemui.volume.panel.dagger.VolumePanelComponent;
import com.android.systemui.volume.panel.dagger.factory.VolumePanelComponentFactory;
+import com.android.systemui.volume.panel.ui.activity.VolumePanelActivity;
+import com.android.systemui.volume.ui.navigation.VolumeNavigator;
import dagger.Binds;
import dagger.Lazy;
@@ -49,6 +55,9 @@
import dagger.multibindings.IntoMap;
import dagger.multibindings.IntoSet;
+import kotlinx.coroutines.CoroutineDispatcher;
+import kotlinx.coroutines.CoroutineScope;
+
/** Dagger Module for code in the volume package. */
@Module(
subcomponents = {
@@ -71,6 +80,12 @@
@Binds
VolumeComponent provideVolumeComponent(VolumeDialogComponent volumeDialogComponent);
+ /** Inject into VolumePanelActivity. */
+ @Binds
+ @IntoMap
+ @ClassKey(VolumePanelActivity.class)
+ Activity bindVolumePanelActivity(VolumePanelActivity activity);
+
/** */
@Binds
VolumePanelComponentFactory bindVolumePanelComponentFactory(VolumePanelComponent.Factory impl);
@@ -84,13 +99,17 @@
DeviceProvisionedController deviceProvisionedController,
ConfigurationController configurationController,
MediaOutputDialogFactory mediaOutputDialogFactory,
- VolumePanelFactory volumePanelFactory,
- ActivityStarter activityStarter,
InteractionJankMonitor interactionJankMonitor,
+ VolumePanelNavigationInteractor volumePanelNavigationInteractor,
+ VolumeNavigator volumeNavigator,
CsdWarningDialog.Factory csdFactory,
DevicePostureController devicePostureController,
DumpManager dumpManager,
- Lazy<SecureSettings> secureSettings) {
+ Lazy<SecureSettings> secureSettings,
+ VibratorHelper vibratorHelper,
+ @Main CoroutineDispatcher mainDispatcher,
+ @Application CoroutineScope applicationScope,
+ SystemClock systemClock) {
VolumeDialogImpl impl = new VolumeDialogImpl(
context,
volumeDialogController,
@@ -98,15 +117,19 @@
deviceProvisionedController,
configurationController,
mediaOutputDialogFactory,
- volumePanelFactory,
- activityStarter,
interactionJankMonitor,
+ volumePanelNavigationInteractor,
+ volumeNavigator,
true, /* should listen for jank */
csdFactory,
devicePostureController,
Looper.getMainLooper(),
dumpManager,
- secureSettings);
+ secureSettings,
+ vibratorHelper,
+ mainDispatcher,
+ applicationScope,
+ systemClock);
impl.setStreamImportant(AudioManager.STREAM_SYSTEM, false);
impl.setAutomute(true);
impl.setSilentMode(false);
diff --git a/packages/SystemUI/src/com/android/systemui/volume/domain/interactor/VolumePanelNavigationInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/domain/interactor/VolumePanelNavigationInteractor.kt
new file mode 100644
index 0000000..d64bb03
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/domain/interactor/VolumePanelNavigationInteractor.kt
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.volume.domain.interactor
+
+import android.content.Context
+import android.util.FeatureFlagUtils
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.volume.domain.model.VolumePanelRoute
+import com.android.systemui.volume.panel.shared.flag.VolumePanelFlag
+import javax.inject.Inject
+
+/** Provides navigation routes for Volume space. */
+class VolumePanelNavigationInteractor
+@Inject
+constructor(
+ @Application private val context: Context,
+ private val volumePanelFlag: VolumePanelFlag,
+) {
+
+ fun getVolumePanelRoute(): VolumePanelRoute {
+ return when {
+ volumePanelFlag.canUseNewVolumePanel() -> VolumePanelRoute.COMPOSE_VOLUME_PANEL
+ FeatureFlagUtils.isEnabled(
+ context,
+ FeatureFlagUtils.SETTINGS_VOLUME_PANEL_IN_SYSTEMUI
+ ) -> VolumePanelRoute.SYSTEM_UI_VOLUME_PANEL
+ else -> VolumePanelRoute.SETTINGS_VOLUME_PANEL
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/VolumePanelComponentKey.kt b/packages/SystemUI/src/com/android/systemui/volume/domain/model/VolumePanelRoute.kt
similarity index 70%
copy from packages/SystemUI/src/com/android/systemui/volume/panel/VolumePanelComponentKey.kt
copy to packages/SystemUI/src/com/android/systemui/volume/domain/model/VolumePanelRoute.kt
index 22a74d2..c85af15 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/VolumePanelComponentKey.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/domain/model/VolumePanelRoute.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2023 The Android Open Source Project
+ * Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,7 +14,10 @@
* limitations under the License.
*/
-package com.android.systemui.volume.panel
+package com.android.systemui.volume.domain.model
-/** Uniquely identifies the [com.android.systemui.volume.panel.ui.VolumePanelComponent]. */
-typealias VolumePanelComponentKey = String
+enum class VolumePanelRoute {
+ COMPOSE_VOLUME_PANEL,
+ SETTINGS_VOLUME_PANEL,
+ SYSTEM_UI_VOLUME_PANEL,
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/bottombar/ui/viewmodel/BottomBarViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/bottombar/ui/viewmodel/BottomBarViewModel.kt
new file mode 100644
index 0000000..8ff2837
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/bottombar/ui/viewmodel/BottomBarViewModel.kt
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.volume.panel.component.bottombar.ui.viewmodel
+
+import android.content.Intent
+import android.provider.Settings
+import com.android.systemui.plugins.ActivityStarter
+import com.android.systemui.volume.panel.dagger.scope.VolumePanelScope
+import com.android.systemui.volume.panel.ui.viewmodel.VolumePanelViewModel
+import javax.inject.Inject
+
+@VolumePanelScope
+class BottomBarViewModel
+@Inject
+constructor(
+ private val activityStarter: ActivityStarter,
+ private val volumePanelViewModel: VolumePanelViewModel,
+) {
+
+ fun onDoneClicked() {
+ volumePanelViewModel.dismissPanel()
+ }
+
+ fun onSettingsClicked() {
+ volumePanelViewModel.dismissPanel()
+ activityStarter.startActivity(
+ Intent(Settings.ACTION_SOUND_SETTINGS).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK),
+ true,
+ )
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/VolumePanelComponentKey.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/shared/model/VolumePanelComponents.kt
similarity index 65%
copy from packages/SystemUI/src/com/android/systemui/volume/panel/VolumePanelComponentKey.kt
copy to packages/SystemUI/src/com/android/systemui/volume/panel/component/shared/model/VolumePanelComponents.kt
index 22a74d2..1a4174a 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/VolumePanelComponentKey.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/shared/model/VolumePanelComponents.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2023 The Android Open Source Project
+ * Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,7 +14,11 @@
* limitations under the License.
*/
-package com.android.systemui.volume.panel
+package com.android.systemui.volume.panel.component.shared.model
-/** Uniquely identifies the [com.android.systemui.volume.panel.ui.VolumePanelComponent]. */
-typealias VolumePanelComponentKey = String
+import com.android.systemui.volume.panel.shared.model.VolumePanelComponentKey
+
+object VolumePanelComponents {
+
+ const val BOTTOM_BAR: VolumePanelComponentKey = "bottom_bar"
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/dagger/DefaultMultibindsModule.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/dagger/DefaultMultibindsModule.kt
index 3660ac1..d1d5390 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/dagger/DefaultMultibindsModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/dagger/DefaultMultibindsModule.kt
@@ -16,8 +16,9 @@
package com.android.systemui.volume.panel.dagger
-import com.android.systemui.volume.panel.VolumePanelComponentKey
import com.android.systemui.volume.panel.domain.ComponentAvailabilityCriteria
+import com.android.systemui.volume.panel.shared.model.VolumePanelComponentKey
+import com.android.systemui.volume.panel.shared.model.VolumePanelUiComponent
import dagger.Module
import dagger.multibindings.Multibinds
@@ -28,4 +29,6 @@
interface DefaultMultibindsModule {
@Multibinds fun criteriaMap(): Map<VolumePanelComponentKey, ComponentAvailabilityCriteria>
+
+ @Multibinds fun components(): Map<VolumePanelComponentKey, VolumePanelUiComponent>
}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/dagger/VolumePanelComponent.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/dagger/VolumePanelComponent.kt
index 0a057eb..0f19e9f 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/dagger/VolumePanelComponent.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/dagger/VolumePanelComponent.kt
@@ -16,12 +16,14 @@
package com.android.systemui.volume.panel.dagger
+import com.android.systemui.volume.panel.component.bottombar.BottomBarModule
import com.android.systemui.volume.panel.dagger.factory.VolumePanelComponentFactory
import com.android.systemui.volume.panel.dagger.scope.VolumePanelScope
import com.android.systemui.volume.panel.domain.DomainModule
import com.android.systemui.volume.panel.domain.interactor.ComponentsInteractor
import com.android.systemui.volume.panel.ui.UiModule
-import com.android.systemui.volume.panel.ui.viewmodel.ComponentsLayoutManager
+import com.android.systemui.volume.panel.ui.composable.ComponentsFactory
+import com.android.systemui.volume.panel.ui.layout.ComponentsLayoutManager
import com.android.systemui.volume.panel.ui.viewmodel.VolumePanelViewModel
import dagger.BindsInstance
import dagger.Subcomponent
@@ -41,6 +43,7 @@
DomainModule::class,
UiModule::class,
// Components modules
+ BottomBarModule::class,
]
)
interface VolumePanelComponent {
@@ -49,6 +52,8 @@
fun componentsInteractor(): ComponentsInteractor
+ fun componentsFactory(): ComponentsFactory
+
fun componentsLayoutManager(): ComponentsLayoutManager
@Subcomponent.Factory
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/domain/DomainModule.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/domain/DomainModule.kt
index 7817630..f785eb7 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/domain/DomainModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/domain/DomainModule.kt
@@ -16,10 +16,11 @@
package com.android.systemui.volume.panel.domain
-import com.android.systemui.volume.panel.VolumePanelComponentKey
+import com.android.systemui.volume.panel.component.shared.model.VolumePanelComponents
import com.android.systemui.volume.panel.dagger.scope.VolumePanelScope
import com.android.systemui.volume.panel.domain.interactor.ComponentsInteractor
import com.android.systemui.volume.panel.domain.interactor.ComponentsInteractorImpl
+import com.android.systemui.volume.panel.shared.model.VolumePanelComponentKey
import dagger.Binds
import dagger.Module
import dagger.Provides
@@ -47,6 +48,10 @@
*/
@Provides
@VolumePanelScope
- fun provideEnabledComponents(): Collection<VolumePanelComponentKey> = setOf()
+ fun provideEnabledComponents(): Collection<VolumePanelComponentKey> {
+ return setOf(
+ VolumePanelComponents.BOTTOM_BAR,
+ )
+ }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/domain/interactor/ComponentsInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/domain/interactor/ComponentsInteractor.kt
index e5b52ea..5301b00 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/domain/interactor/ComponentsInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/domain/interactor/ComponentsInteractor.kt
@@ -16,10 +16,10 @@
package com.android.systemui.volume.panel.domain.interactor
-import com.android.systemui.volume.panel.VolumePanelComponentKey
import com.android.systemui.volume.panel.dagger.scope.VolumePanelScope
import com.android.systemui.volume.panel.domain.ComponentAvailabilityCriteria
import com.android.systemui.volume.panel.domain.model.ComponentModel
+import com.android.systemui.volume.panel.shared.model.VolumePanelComponentKey
import javax.inject.Inject
import javax.inject.Provider
import kotlinx.coroutines.CoroutineScope
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/domain/model/ComponentModel.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/domain/model/ComponentModel.kt
index 9765713..11a9916 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/domain/model/ComponentModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/domain/model/ComponentModel.kt
@@ -16,7 +16,7 @@
package com.android.systemui.volume.panel.domain.model
-import com.android.systemui.volume.panel.VolumePanelComponentKey
+import com.android.systemui.volume.panel.shared.model.VolumePanelComponentKey
/**
* Represents a current state of the Volume Panel component.
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/shared/flag/VolumePanelFlag.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/shared/flag/VolumePanelFlag.kt
new file mode 100644
index 0000000..d90a9c7
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/shared/flag/VolumePanelFlag.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.volume.panel.shared.flag
+
+import com.android.systemui.Flags
+import com.android.systemui.compose.ComposeFacade
+import com.android.systemui.flags.RefactorFlagUtils
+import javax.inject.Inject
+
+/** Provides a flag to check for the new Compose based Volume Panel availability. */
+class VolumePanelFlag @Inject constructor() {
+
+ /**
+ * Returns true when the new Volume Panel is available and false the otherwise. The new panel
+ * can only be available when [ComposeFacade.isComposeAvailable] is true.
+ */
+ fun canUseNewVolumePanel(): Boolean {
+ return ComposeFacade.isComposeAvailable() && Flags.newVolumePanel()
+ }
+
+ fun assertNewVolumePanel() {
+ require(ComposeFacade.isComposeAvailable())
+ RefactorFlagUtils.assertInNewMode(Flags.newVolumePanel(), Flags.FLAG_NEW_VOLUME_PANEL)
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/VolumePanelComponentKey.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/shared/model/VolumePanelComponentKey.kt
similarity index 86%
rename from packages/SystemUI/src/com/android/systemui/volume/panel/VolumePanelComponentKey.kt
rename to packages/SystemUI/src/com/android/systemui/volume/panel/shared/model/VolumePanelComponentKey.kt
index 22a74d2..4644ee7 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/VolumePanelComponentKey.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/shared/model/VolumePanelComponentKey.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2023 The Android Open Source Project
+ * Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.systemui.volume.panel
+package com.android.systemui.volume.panel.shared.model
/** Uniquely identifies the [com.android.systemui.volume.panel.ui.VolumePanelComponent]. */
typealias VolumePanelComponentKey = String
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/shared/model/VolumePanelUiComponent.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/shared/model/VolumePanelUiComponent.kt
new file mode 100644
index 0000000..24de41f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/shared/model/VolumePanelUiComponent.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.volume.panel.shared.model
+
+/**
+ * An element of a Volume Panel. This can be a button bar, group of sliders or something else. The
+ * only real implementation is Compose-based and located in `compose/features/`.
+ *
+ * Steps for adding an implementation in SystemUI:
+ * 1) Implement `ComposeVolumePanelUiComponent` in `compose/features/`
+ * 2) Add a module binding `ComposeVolumePanelUiComponent` into a map in compose/facade/enabled
+ * 3) Add an interface with the same name as the 2-step module in compose/facade/disabled to stub it
+ * when the Compose is disabled
+ * 4) Add the module to the VolumePanelComponent
+ */
+interface VolumePanelUiComponent
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/ui/UiModule.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/ui/UiModule.kt
index bfa7ef2..1346c54 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/ui/UiModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/ui/UiModule.kt
@@ -16,8 +16,8 @@
package com.android.systemui.volume.panel.ui
-import com.android.systemui.volume.panel.ui.viewmodel.ComponentsLayoutManager
-import com.android.systemui.volume.panel.ui.viewmodel.DefaultComponentsLayoutManager
+import com.android.systemui.volume.panel.ui.layout.ComponentsLayoutManager
+import com.android.systemui.volume.panel.ui.layout.DefaultComponentsLayoutManager
import dagger.Binds
import dagger.Module
@@ -25,5 +25,6 @@
@Module
interface UiModule {
- @Binds fun bindSorter(impl: DefaultComponentsLayoutManager): ComponentsLayoutManager
+ @Binds
+ fun bindComponentsLayoutManager(impl: DefaultComponentsLayoutManager): ComponentsLayoutManager
}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/ui/activity/VolumePanelActivity.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/ui/activity/VolumePanelActivity.kt
new file mode 100644
index 0000000..1b2265b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/ui/activity/VolumePanelActivity.kt
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.volume.panel.ui.activity
+
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.enableEdgeToEdge
+import androidx.activity.viewModels
+import androidx.core.view.WindowCompat
+import com.android.systemui.compose.ComposeFacade
+import com.android.systemui.volume.panel.shared.flag.VolumePanelFlag
+import com.android.systemui.volume.panel.ui.viewmodel.VolumePanelViewModel
+import javax.inject.Inject
+import javax.inject.Provider
+
+class VolumePanelActivity
+@Inject
+constructor(
+ private val volumePanelViewModelFactory: Provider<VolumePanelViewModel.Factory>,
+ private val volumePanelFlag: VolumePanelFlag,
+) : ComponentActivity() {
+
+ private val viewModel: VolumePanelViewModel by
+ viewModels(factoryProducer = { volumePanelViewModelFactory.get() })
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ enableEdgeToEdge()
+ super.onCreate(savedInstanceState)
+
+ volumePanelFlag.assertNewVolumePanel()
+
+ WindowCompat.setDecorFitsSystemWindows(window, false)
+ ComposeFacade.setVolumePanelActivityContent(this, viewModel) { finish() }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/ui/composable/ComponentsFactory.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/ui/composable/ComponentsFactory.kt
new file mode 100644
index 0000000..db1c121
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/ui/composable/ComponentsFactory.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.volume.panel.ui.composable
+
+import com.android.systemui.volume.panel.dagger.VolumePanelComponent
+import com.android.systemui.volume.panel.dagger.scope.VolumePanelScope
+import com.android.systemui.volume.panel.shared.model.VolumePanelComponentKey
+import com.android.systemui.volume.panel.shared.model.VolumePanelUiComponent
+import javax.inject.Inject
+import javax.inject.Provider
+
+/** Provides [VolumePanelComponent] implementation for each [VolumePanelComponentKey]. */
+@VolumePanelScope
+class ComponentsFactory
+@Inject
+constructor(
+ private val componentByKey:
+ Map<
+ VolumePanelComponentKey,
+ @JvmSuppressWildcards
+ Provider<@JvmSuppressWildcards VolumePanelUiComponent>
+ >
+) {
+
+ fun createComponent(key: VolumePanelComponentKey): VolumePanelUiComponent {
+ require(componentByKey.containsKey(key)) { "Component for key=$key is not bound." }
+ return componentByKey.getValue(key).get()
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/ui/model/ComponentsLayout.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/ui/layout/ComponentsLayout.kt
similarity index 80%
rename from packages/SystemUI/src/com/android/systemui/volume/panel/ui/model/ComponentsLayout.kt
rename to packages/SystemUI/src/com/android/systemui/volume/panel/ui/layout/ComponentsLayout.kt
index 5690ac3..25a95d8 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/ui/model/ComponentsLayout.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/ui/layout/ComponentsLayout.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2023 The Android Open Source Project
+ * Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,7 +14,9 @@
* limitations under the License.
*/
-package com.android.systemui.volume.panel.ui.model
+package com.android.systemui.volume.panel.ui.layout
+
+import com.android.systemui.volume.panel.ui.viewmodel.ComponentState
/** Represents components grouping into the layout. */
data class ComponentsLayout(
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/ui/viewmodel/ComponentsLayoutManager.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/ui/layout/ComponentsLayoutManager.kt
similarity index 78%
rename from packages/SystemUI/src/com/android/systemui/volume/panel/ui/viewmodel/ComponentsLayoutManager.kt
rename to packages/SystemUI/src/com/android/systemui/volume/panel/ui/layout/ComponentsLayoutManager.kt
index f45401a..71ca95c 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/ui/viewmodel/ComponentsLayoutManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/ui/layout/ComponentsLayoutManager.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2023 The Android Open Source Project
+ * Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,11 +14,10 @@
* limitations under the License.
*/
-package com.android.systemui.volume.panel.ui.viewmodel
+package com.android.systemui.volume.panel.ui.layout
-import com.android.systemui.volume.panel.ui.model.ComponentState
-import com.android.systemui.volume.panel.ui.model.ComponentsLayout
-import com.android.systemui.volume.panel.ui.model.VolumePanelState
+import com.android.systemui.volume.panel.ui.viewmodel.ComponentState
+import com.android.systemui.volume.panel.ui.viewmodel.VolumePanelState
/**
* Lays out components to [ComponentsLayout], that UI uses to render the Volume Panel.
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/ui/layout/DefaultComponentsLayoutManager.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/ui/layout/DefaultComponentsLayoutManager.kt
new file mode 100644
index 0000000..ff485c2
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/ui/layout/DefaultComponentsLayoutManager.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.volume.panel.ui.layout
+
+import com.android.systemui.volume.panel.component.shared.model.VolumePanelComponents
+import com.android.systemui.volume.panel.dagger.scope.VolumePanelScope
+import com.android.systemui.volume.panel.ui.viewmodel.ComponentState
+import com.android.systemui.volume.panel.ui.viewmodel.VolumePanelState
+import javax.inject.Inject
+
+@VolumePanelScope
+class DefaultComponentsLayoutManager @Inject constructor() : ComponentsLayoutManager {
+
+ override fun layout(
+ volumePanelState: VolumePanelState,
+ components: Collection<ComponentState>
+ ): ComponentsLayout {
+ val bottomBarKey = VolumePanelComponents.BOTTOM_BAR
+ return ComponentsLayout(
+ components.filter { it.key != bottomBarKey }.sortedBy { it.key },
+ components.find { it.key == bottomBarKey }
+ ?: error(
+ "VolumePanelComponents.BOTTOM_BAR must be present in the default " +
+ "components layout."
+ )
+ )
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/ui/model/ComponentState.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/ui/viewmodel/ComponentState.kt
similarity index 71%
rename from packages/SystemUI/src/com/android/systemui/volume/panel/ui/model/ComponentState.kt
rename to packages/SystemUI/src/com/android/systemui/volume/panel/ui/viewmodel/ComponentState.kt
index 0a226e2..5f4dbfb 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/ui/model/ComponentState.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/ui/viewmodel/ComponentState.kt
@@ -14,12 +14,14 @@
* limitations under the License.
*/
-package com.android.systemui.volume.panel.ui.model
+package com.android.systemui.volume.panel.ui.viewmodel
-import com.android.systemui.volume.panel.VolumePanelComponentKey
+import com.android.systemui.volume.panel.shared.model.VolumePanelComponentKey
+import com.android.systemui.volume.panel.shared.model.VolumePanelUiComponent
/**
- * State of the [VolumePanelComponent].
+ * State of the [VolumePanelComponent]. It has everything the UI layer needs to layout a particular
+ * component.
*
* @property key uniquely identifies this component
* @property component is an inflated component obtained be the View Model
@@ -27,5 +29,6 @@
*/
data class ComponentState(
val key: VolumePanelComponentKey,
+ val component: VolumePanelUiComponent,
val isVisible: Boolean,
)
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/ui/viewmodel/DefaultComponentsLayoutManager.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/ui/viewmodel/DefaultComponentsLayoutManager.kt
deleted file mode 100644
index cedfaf3..0000000
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/ui/viewmodel/DefaultComponentsLayoutManager.kt
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
- * 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.volume.panel.ui.viewmodel
-
-import com.android.systemui.volume.panel.dagger.scope.VolumePanelScope
-import com.android.systemui.volume.panel.ui.model.ComponentState
-import com.android.systemui.volume.panel.ui.model.ComponentsLayout
-import com.android.systemui.volume.panel.ui.model.VolumePanelState
-import javax.inject.Inject
-
-/**
- * Default [ComponentsLayoutManager]. It places [VolumePanelComponents.BOTTOM_BAR] to
- * [ComponentsLayout.bottomBarComponent] and everything else to
- * [ComponentsLayout.contentComponents].
- */
-@VolumePanelScope
-class DefaultComponentsLayoutManager @Inject constructor() : ComponentsLayoutManager {
-
- override fun layout(
- volumePanelState: VolumePanelState,
- components: Collection<ComponentState>
- ): ComponentsLayout = TODO("Unimplemented yet")
-}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/ui/model/VolumePanelState.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/ui/viewmodel/VolumePanelState.kt
similarity index 95%
rename from packages/SystemUI/src/com/android/systemui/volume/panel/ui/model/VolumePanelState.kt
rename to packages/SystemUI/src/com/android/systemui/volume/panel/ui/viewmodel/VolumePanelState.kt
index 399342f..f67db96 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/ui/model/VolumePanelState.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/ui/viewmodel/VolumePanelState.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.systemui.volume.panel.ui.model
+package com.android.systemui.volume.panel.ui.viewmodel
import android.content.res.Configuration
import android.content.res.Configuration.Orientation
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/ui/viewmodel/VolumePanelViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/ui/viewmodel/VolumePanelViewModel.kt
index dda361a..d87a79e 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/ui/viewmodel/VolumePanelViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/ui/viewmodel/VolumePanelViewModel.kt
@@ -26,9 +26,9 @@
import com.android.systemui.volume.panel.dagger.VolumePanelComponent
import com.android.systemui.volume.panel.dagger.factory.VolumePanelComponentFactory
import com.android.systemui.volume.panel.domain.interactor.ComponentsInteractor
-import com.android.systemui.volume.panel.ui.model.ComponentState
-import com.android.systemui.volume.panel.ui.model.ComponentsLayout
-import com.android.systemui.volume.panel.ui.model.VolumePanelState
+import com.android.systemui.volume.panel.ui.composable.ComponentsFactory
+import com.android.systemui.volume.panel.ui.layout.ComponentsLayout
+import com.android.systemui.volume.panel.ui.layout.ComponentsLayoutManager
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.cancel
@@ -38,9 +38,10 @@
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.flow.stateIn
-import kotlinx.coroutines.launch
+import kotlinx.coroutines.flow.update
class VolumePanelViewModel(
resources: Resources,
@@ -56,6 +57,9 @@
private val componentsInteractor: ComponentsInteractor
get() = volumePanelComponent.componentsInteractor()
+ private val componentsFactory: ComponentsFactory
+ get() = volumePanelComponent.componentsFactory()
+
private val componentsLayoutManager: ComponentsLayoutManager
get() = volumePanelComponent.componentsLayoutManager()
@@ -63,20 +67,22 @@
val volumePanelState: StateFlow<VolumePanelState> =
combine(
- configurationController.onConfigChanged.distinctUntilChanged(),
+ configurationController.onConfigChanged
+ .onStart { emit(resources.configuration) }
+ .distinctUntilChanged(),
mutablePanelVisibility,
) { configuration, isVisible ->
VolumePanelState(orientation = configuration.orientation, isVisible = isVisible)
}
.stateIn(
- volumePanelComponent.coroutineScope(),
+ scope,
SharingStarted.Eagerly,
VolumePanelState(
orientation = resources.configuration.orientation,
isVisible = mutablePanelVisibility.value,
),
)
- val mComponentsLayout: Flow<ComponentsLayout> =
+ val componentsLayout: Flow<ComponentsLayout> =
combine(
componentsInteractor.components,
volumePanelState,
@@ -85,19 +91,20 @@
components.map { model ->
ComponentState(
model.key,
+ componentsFactory.createComponent(model.key),
model.isAvailable,
)
}
componentsLayoutManager.layout(scope, componentStates)
}
.shareIn(
- volumePanelComponent.coroutineScope(),
+ scope,
SharingStarted.Eagerly,
replay = 1,
)
fun dismissPanel() {
- scope.launch { mutablePanelVisibility.emit(false) }
+ mutablePanelVisibility.update { false }
}
override fun onCleared() {
diff --git a/packages/SystemUI/src/com/android/systemui/volume/ui/navigation/VolumeNavigator.kt b/packages/SystemUI/src/com/android/systemui/volume/ui/navigation/VolumeNavigator.kt
new file mode 100644
index 0000000..790638c
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/ui/navigation/VolumeNavigator.kt
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.volume.ui.navigation
+
+import android.content.Context
+import android.content.Intent
+import android.provider.Settings
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.plugins.ActivityStarter
+import com.android.systemui.volume.VolumePanelFactory
+import com.android.systemui.volume.domain.model.VolumePanelRoute
+import com.android.systemui.volume.panel.ui.activity.VolumePanelActivity
+import javax.inject.Inject
+
+class VolumeNavigator
+@Inject
+constructor(
+ @Application private val context: Context,
+ private val volumePanelFactory: VolumePanelFactory,
+ private val activityStarter: ActivityStarter,
+) {
+
+ fun openVolumePanel(route: VolumePanelRoute) {
+ when (route) {
+ VolumePanelRoute.COMPOSE_VOLUME_PANEL ->
+ activityStarter.startActivityDismissingKeyguard(
+ /* intent = */ Intent(context, VolumePanelActivity::class.java),
+ /* onlyProvisioned = */ false,
+ /* dismissShade= */ true,
+ /* disallowEnterPictureInPictureWhileLaunching = */ true,
+ /* callback= */ null,
+ /* flags= */ 0,
+ /* animationController= */ null,
+ /* userHandle= */ null,
+ )
+ VolumePanelRoute.SETTINGS_VOLUME_PANEL ->
+ activityStarter.startActivity(
+ /* intent= */ Intent(Settings.Panel.ACTION_VOLUME),
+ /* dismissShade= */ true
+ )
+ VolumePanelRoute.SYSTEM_UI_VOLUME_PANEL ->
+ volumePanelFactory.create(aboveStatusBar = true, view = null)
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java b/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java
index 1e801ae..7c6ad23 100644
--- a/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java
+++ b/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java
@@ -32,6 +32,8 @@
import android.content.res.Configuration;
import android.graphics.Rect;
import android.inputmethodservice.InputMethodService;
+import android.os.HandlerExecutor;
+import android.os.HandlerThread;
import android.os.IBinder;
import android.util.Log;
import android.view.Display;
@@ -125,6 +127,7 @@
private final DisplayTracker mDisplayTracker;
private final NoteTaskInitializer mNoteTaskInitializer;
private final Executor mSysUiMainExecutor;
+ private HandlerThread mHandlerThread;
// Listeners and callbacks. Note that we prefer member variable over anonymous class here to
// avoid the situation that some implementations, like KeyguardUpdateMonitor, use WeakReference
@@ -206,6 +209,8 @@
mDisplayTracker = displayTracker;
mNoteTaskInitializer = noteTaskInitializer;
mSysUiMainExecutor = sysUiMainExecutor;
+ mHandlerThread = new HandlerThread("WMShell");
+ mHandlerThread.start();
}
@Override
@@ -219,7 +224,8 @@
mKeyguardUpdateMonitor.registerCallback(mKeyguardUpdateMonitorCallback);
// Subscribe to user changes
- mUserTracker.addCallback(mUserChangedCallback, mContext.getMainExecutor());
+ mUserTracker.addCallback(mUserChangedCallback,
+ new HandlerExecutor(mHandlerThread.getThreadHandler()));
mCommandQueue.addCallback(this);
mPipOptional.ifPresent(this::initPip);
diff --git a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt
similarity index 100%
rename from packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt
rename to packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt
diff --git a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSimPinViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSimPinViewControllerTest.kt
similarity index 100%
rename from packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSimPinViewControllerTest.kt
rename to packages/SystemUI/tests/src/com/android/keyguard/KeyguardSimPinViewControllerTest.kt
diff --git a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSimPukViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSimPukViewControllerTest.kt
similarity index 100%
rename from packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSimPukViewControllerTest.kt
rename to packages/SystemUI/tests/src/com/android/keyguard/KeyguardSimPukViewControllerTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java
index d8eb05a..be06cc5 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java
@@ -121,7 +121,6 @@
import com.android.keyguard.KeyguardUpdateMonitor.BiometricAuthenticated;
import com.android.keyguard.logging.KeyguardUpdateMonitorLogger;
import com.android.settingslib.fuelgauge.BatteryStatus;
-import com.android.systemui.Flags;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.biometrics.AuthController;
import com.android.systemui.biometrics.FingerprintInteractiveToAuthProvider;
@@ -930,7 +929,8 @@
@Test
public void trustAgentHasTrust() {
// WHEN user has trust
- givenSelectedUserCanSkipBouncerFromTrustedState();
+ mKeyguardUpdateMonitor.onTrustChanged(true, true,
+ mSelectedUserInteractor.getSelectedUserId(), 0, null);
// THEN user is considered as "having trust" and bouncer can be skipped
Assert.assertTrue(mKeyguardUpdateMonitor.getUserHasTrust(
@@ -954,7 +954,8 @@
@Test
public void trustAgentHasTrust_fingerprintLockout() {
// GIVEN user has trust
- givenSelectedUserCanSkipBouncerFromTrustedState();
+ mKeyguardUpdateMonitor.onTrustChanged(true, true,
+ mSelectedUserInteractor.getSelectedUserId(), 0, null);
Assert.assertTrue(mKeyguardUpdateMonitor.getUserHasTrust(
mSelectedUserInteractor.getSelectedUserId()));
@@ -1720,6 +1721,24 @@
}
@Test
+ public void assistantVisible_sendEventToFaceAuthInteractor() {
+ // WHEN the assistant is visible
+ mKeyguardUpdateMonitor.setAssistantVisible(true);
+
+ // THEN send event to face auth interactor
+ verify(mFaceAuthInteractor).onAssistantTriggeredOnLockScreen();
+ }
+
+ @Test
+ public void assistantNotVisible_doesNotSendEventToFaceAuthInteractor() {
+ // WHEN the assistant is visible
+ mKeyguardUpdateMonitor.setAssistantVisible(false);
+
+ // THEN never send event to face auth interactor
+ verify(mFaceAuthInteractor, never()).onAssistantTriggeredOnLockScreen();
+ }
+
+ @Test
public void fingerprintFailure_requestActiveUnlock_dismissKeyguard() {
// GIVEN shouldTriggerActiveUnlock
bouncerFullyVisible();
@@ -1997,43 +2016,6 @@
}
@Test
- public void runFpDetectFlagDisabled_sideFps_keyguardDismissible_fingerprintAuthenticateRuns() {
- mSetFlagsRule.disableFlags(Flags.FLAG_RUN_FINGERPRINT_DETECT_ON_DISMISSIBLE_KEYGUARD);
-
- // Clear invocations, since previous setup (e.g. registering BiometricManager callbacks)
- // will trigger updateBiometricListeningState();
- clearInvocations(mFingerprintManager);
- mKeyguardUpdateMonitor.resetBiometricListeningState();
-
- // GIVEN the user can skip the bouncer
- givenSelectedUserCanSkipBouncerFromTrustedState();
- when(mStrongAuthTracker.isUnlockingWithBiometricAllowed(anyBoolean())).thenReturn(true);
- mKeyguardUpdateMonitor.dispatchStartedGoingToSleep(0 /* why */);
- mTestableLooper.processAllMessages();
-
- // WHEN verify authenticate runs
- verifyFingerprintAuthenticateCall();
- }
-
- @Test
- public void sideFps_keyguardDismissible_fingerprintDetectRuns() {
- mSetFlagsRule.enableFlags(Flags.FLAG_RUN_FINGERPRINT_DETECT_ON_DISMISSIBLE_KEYGUARD);
- // Clear invocations, since previous setup (e.g. registering BiometricManager callbacks)
- // will trigger updateBiometricListeningState();
- clearInvocations(mFingerprintManager);
- mKeyguardUpdateMonitor.resetBiometricListeningState();
-
- // GIVEN the user can skip the bouncer
- givenSelectedUserCanSkipBouncerFromTrustedState();
- when(mStrongAuthTracker.isUnlockingWithBiometricAllowed(anyBoolean())).thenReturn(true);
- mKeyguardUpdateMonitor.dispatchStartedGoingToSleep(0 /* why */);
- mTestableLooper.processAllMessages();
-
- // WHEN verify detect runs
- verifyFingerprintDetectCall();
- }
-
- @Test
public void testFingerprintSensorProperties() throws RemoteException {
mFingerprintAuthenticatorsRegisteredCallback.onAllAuthenticatorsRegistered(
new ArrayList<>());
diff --git a/packages/SystemUI/tests/src/com/android/systemui/back/domain/interactor/BackActionInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/back/domain/interactor/BackActionInteractorTest.kt
index e0c6bba..0ba9abe 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/back/domain/interactor/BackActionInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/back/domain/interactor/BackActionInteractorTest.kt
@@ -32,12 +32,16 @@
import com.android.systemui.Flags
import com.android.systemui.SysuiTestCase
import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testScope
import com.android.systemui.plugins.statusbar.StatusBarStateController
import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAsleepForTest
import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAwakeForTest
import com.android.systemui.power.domain.interactor.PowerInteractorFactory
import com.android.systemui.scene.data.repository.WindowRootViewVisibilityRepository
import com.android.systemui.scene.domain.interactor.WindowRootViewVisibilityInteractor
+import com.android.systemui.scene.domain.interactor.sceneInteractor
+import com.android.systemui.scene.shared.flag.sceneContainerFlags
import com.android.systemui.scene.ui.view.WindowRootView
import com.android.systemui.shade.QuickSettingsController
import com.android.systemui.shade.ShadeController
@@ -59,7 +63,6 @@
import junit.framework.Assert.assertTrue
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.StandardTestDispatcher
-import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runCurrent
import org.junit.Before
import org.junit.Rule
@@ -76,7 +79,8 @@
@RunWith(AndroidJUnit4::class)
@OptIn(ExperimentalCoroutinesApi::class)
class BackActionInteractorTest : SysuiTestCase() {
- private val testScope = TestScope()
+ private val kosmos = Kosmos()
+ private val testScope = kosmos.testScope
private val executor = FakeExecutor(FakeSystemClock())
@JvmField @Rule var mockitoRule = MockitoJUnit.rule()
@@ -105,6 +109,8 @@
headsUpManager,
powerInteractor,
activeNotificationsInteractor,
+ kosmos.sceneContainerFlags,
+ kosmos::sceneInteractor,
)
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinderTest.kt
index 54dbd04..3603c3c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinderTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinderTest.kt
@@ -63,14 +63,20 @@
import com.android.systemui.keyguard.data.repository.FakeBiometricSettingsRepository
import com.android.systemui.keyguard.data.repository.FakeDeviceEntryFingerprintAuthRepository
import com.android.systemui.keyguard.data.repository.FakeTrustRepository
+import com.android.systemui.keyguard.data.repository.biometricSettingsRepository
import com.android.systemui.keyguard.domain.interactor.DeviceEntrySideFpsOverlayInteractor
+import com.android.systemui.keyguard.domain.interactor.keyguardInteractor
+import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
import com.android.systemui.keyguard.ui.viewmodel.SideFpsProgressBarViewModel
import com.android.systemui.log.SideFpsLogger
import com.android.systemui.log.logcatLogBuffer
import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.power.domain.interactor.powerInteractor
import com.android.systemui.res.R
import com.android.systemui.shared.Flags.FLAG_SIDEFPS_CONTROLLER_REFACTOR
+import com.android.systemui.statusbar.phone.dozeServiceHost
import com.android.systemui.statusbar.policy.KeyguardStateController
+import com.android.systemui.testKosmos
import com.android.systemui.unfold.compat.ScreenSizeFoldProvider
import com.android.systemui.user.domain.interactor.SelectedUserInteractor
import com.android.systemui.util.concurrency.FakeExecutor
@@ -107,6 +113,8 @@
@RunWith(JUnit4::class)
@TestableLooper.RunWithLooper(setAsMainLooper = true)
class SideFpsOverlayViewBinderTest : SysuiTestCase() {
+ private val kosmos = testKosmos()
+
@JvmField @Rule var mockitoRule: MockitoRule = MockitoJUnit.rule()
@Mock private lateinit var activityTaskManager: ActivityTaskManager
@Mock private lateinit var displayManager: DisplayManager
@@ -237,7 +245,8 @@
windowManager,
displayStateInteractor,
Optional.of(fingerprintInteractiveToAuthProvider),
- mock(),
+ kosmos.biometricSettingsRepository,
+ kosmos.keyguardTransitionInteractor,
SideFpsLogger(logcatLogBuffer("SfpsLogger"))
)
@@ -246,10 +255,12 @@
mContext,
mock(),
sfpsSensorInteractor,
- mock(),
+ kosmos.dozeServiceHost,
+ kosmos.keyguardInteractor,
displayStateInteractor,
UnconfinedTestDispatcher(),
testScope.backgroundScope,
+ kosmos.powerInteractor,
)
viewModel =
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt
index 3888f2b..6a9c881 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt
@@ -21,6 +21,7 @@
import android.graphics.Bitmap
import android.graphics.Point
import android.graphics.drawable.BitmapDrawable
+import android.hardware.biometrics.Flags.FLAG_CUSTOM_BIOMETRIC_PROMPT
import android.hardware.biometrics.PromptContentItemBulletedText
import android.hardware.biometrics.PromptContentView
import android.hardware.biometrics.PromptInfo
@@ -1225,6 +1226,7 @@
@Test
fun descriptionOverriddenByContentView() =
runGenericTest(contentView = promptContentView, description = "test description") {
+ mSetFlagsRule.enableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT)
val contentView by collectLastValue(viewModel.contentView)
val description by collectLastValue(viewModel.description)
@@ -1235,6 +1237,7 @@
@Test
fun descriptionWithoutContentView() =
runGenericTest(description = "test description") {
+ mSetFlagsRule.enableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT)
val contentView by collectLastValue(viewModel.contentView)
val description by collectLastValue(viewModel.description)
@@ -1244,6 +1247,7 @@
@Test
fun defaultLogoIfNoLogoSet() = runGenericTest {
+ mSetFlagsRule.enableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT)
val logo by collectLastValue(viewModel.logo)
assertThat(logo).isEqualTo(defaultLogoIcon)
}
@@ -1251,6 +1255,7 @@
@Test
fun logoResSetByApp() =
runGenericTest(logoRes = logoResFromApp) {
+ mSetFlagsRule.enableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT)
val logo by collectLastValue(viewModel.logo)
assertThat(logo).isEqualTo(logoFromApp)
}
@@ -1258,6 +1263,7 @@
@Test
fun logoBitmapSetByApp() =
runGenericTest(logoBitmap = logoBitmapFromApp) {
+ mSetFlagsRule.enableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT)
val logo by collectLastValue(viewModel.logo)
assertThat((logo as BitmapDrawable).bitmap).isEqualTo(logoBitmapFromApp)
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModelTest.kt
index 1fa60fc..3c43031 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModelTest.kt
@@ -56,19 +56,27 @@
import com.android.systemui.classifier.FalsingCollector
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFaceAuthInteractor
+import com.android.systemui.deviceentry.domain.interactor.deviceEntryFingerprintAuthInteractor
import com.android.systemui.display.data.repository.FakeDisplayRepository
import com.android.systemui.keyguard.DismissCallbackRegistry
import com.android.systemui.keyguard.data.repository.FakeBiometricSettingsRepository
import com.android.systemui.keyguard.data.repository.FakeDeviceEntryFingerprintAuthRepository
import com.android.systemui.keyguard.data.repository.FakeTrustRepository
+import com.android.systemui.keyguard.data.repository.biometricSettingsRepository
import com.android.systemui.keyguard.domain.interactor.DeviceEntrySideFpsOverlayInteractor
+import com.android.systemui.keyguard.domain.interactor.keyguardInteractor
+import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
import com.android.systemui.keyguard.ui.viewmodel.SideFpsProgressBarViewModel
+import com.android.systemui.kosmos.testDispatcher
import com.android.systemui.log.SideFpsLogger
import com.android.systemui.log.logcatLogBuffer
import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.power.domain.interactor.powerInteractor
import com.android.systemui.res.R
import com.android.systemui.shared.Flags.FLAG_SIDEFPS_CONTROLLER_REFACTOR
+import com.android.systemui.statusbar.phone.dozeServiceHost
import com.android.systemui.statusbar.policy.KeyguardStateController
+import com.android.systemui.testKosmos
import com.android.systemui.unfold.compat.ScreenSizeFoldProvider
import com.android.systemui.user.domain.interactor.SelectedUserInteractor
import com.android.systemui.util.concurrency.FakeExecutor
@@ -98,6 +106,7 @@
@SmallTest
@RunWith(JUnit4::class)
class SideFpsOverlayViewModelTest : SysuiTestCase() {
+ private val kosmos = testKosmos()
@JvmField @Rule var mockitoRule: MockitoRule = MockitoJUnit.rule()
@Mock private lateinit var activityTaskManager: ActivityTaskManager
@@ -239,19 +248,22 @@
windowManager,
displayStateInteractor,
Optional.of(fingerprintInteractiveToAuthProvider),
- mock(),
+ kosmos.biometricSettingsRepository,
+ kosmos.keyguardTransitionInteractor,
SideFpsLogger(logcatLogBuffer("SfpsLogger"))
)
sideFpsProgressBarViewModel =
SideFpsProgressBarViewModel(
mContext,
- mock(),
+ kosmos.deviceEntryFingerprintAuthInteractor,
sfpsSensorInteractor,
- mock(),
+ kosmos.dozeServiceHost,
+ kosmos.keyguardInteractor,
displayStateInteractor,
- StandardTestDispatcher(),
+ kosmos.testDispatcher,
testScope.backgroundScope,
+ kosmos.powerInteractor,
)
underTest =
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyboard/stickykeys/ui/StickyKeysIndicatorCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyboard/stickykeys/ui/StickyKeysIndicatorCoordinatorTest.kt
new file mode 100644
index 0000000..df73cc8
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyboard/stickykeys/ui/StickyKeysIndicatorCoordinatorTest.kt
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyboard.stickykeys.ui
+
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.compose.ComposeFacade
+import com.android.systemui.keyboard.data.repository.FakeStickyKeysRepository
+import com.android.systemui.keyboard.data.repository.keyboardRepository
+import com.android.systemui.keyboard.stickykeys.StickyKeysLogger
+import com.android.systemui.keyboard.stickykeys.shared.model.Locked
+import com.android.systemui.keyboard.stickykeys.shared.model.ModifierKey.SHIFT
+import com.android.systemui.keyboard.stickykeys.ui.viewmodel.StickyKeysIndicatorViewModel
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.statusbar.phone.ComponentSystemUIDialog
+import com.android.systemui.statusbar.phone.SystemUIDialogFactory
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
+import org.junit.Assume
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.mockito.Mockito.anyBoolean
+import org.mockito.Mockito.anyInt
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.verifyZeroInteractions
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(JUnit4::class)
+class StickyKeysIndicatorCoordinatorTest : SysuiTestCase() {
+
+ private lateinit var coordinator: StickyKeysIndicatorCoordinator
+ private val testScope = TestScope(StandardTestDispatcher())
+ private val stickyKeysRepository = FakeStickyKeysRepository()
+ private val dialog = mock<ComponentSystemUIDialog>()
+
+ @Before
+ fun setup() {
+ Assume.assumeTrue(ComposeFacade.isComposeAvailable())
+ val dialogFactory = mock<SystemUIDialogFactory> {
+ whenever(applicationContext).thenReturn(context)
+ whenever(create(any(), anyInt(), anyBoolean())).thenReturn(dialog)
+ }
+ val keyboardRepository = Kosmos().keyboardRepository
+ val viewModel = StickyKeysIndicatorViewModel(
+ stickyKeysRepository,
+ keyboardRepository,
+ testScope.backgroundScope)
+ coordinator = StickyKeysIndicatorCoordinator(
+ testScope.backgroundScope,
+ dialogFactory,
+ viewModel,
+ mock<StickyKeysLogger>())
+ coordinator.startListening()
+ keyboardRepository.setIsAnyKeyboardConnected(true)
+ }
+
+ @Test
+ fun dialogIsShownWhenStickyKeysAreEmitted() {
+ testScope.run {
+ verifyZeroInteractions(dialog)
+
+ stickyKeysRepository.setStickyKeys(linkedMapOf(SHIFT to Locked(true)))
+ runCurrent()
+
+ verify(dialog).show()
+ }
+ }
+
+ @Test
+ fun dialogDisappearsWhenStickyKeysAreEmpty() {
+ testScope.run {
+ verifyZeroInteractions(dialog)
+
+ stickyKeysRepository.setStickyKeys(linkedMapOf(SHIFT to Locked(true)))
+ runCurrent()
+ stickyKeysRepository.setStickyKeys(linkedMapOf())
+ runCurrent()
+
+ verify(dialog).dismiss()
+ }
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyboard/stickykeys/ui/viewmodel/StickyKeysIndicatorViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyboard/stickykeys/ui/viewmodel/StickyKeysIndicatorViewModelTest.kt
index d397fc2..8a71368 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyboard/stickykeys/ui/viewmodel/StickyKeysIndicatorViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyboard/stickykeys/ui/viewmodel/StickyKeysIndicatorViewModelTest.kt
@@ -34,6 +34,7 @@
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.mock
import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runCurrent
@@ -46,6 +47,7 @@
import org.mockito.Mockito.verify
import org.mockito.Mockito.verifyZeroInteractions
+@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@RunWith(JUnit4::class)
class StickyKeysIndicatorViewModelTest : SysuiTestCase() {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java
index b57cf53..8a3a434 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java
@@ -97,6 +97,7 @@
import com.android.systemui.flags.SystemPropertiesHelper;
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor;
import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel;
+import com.android.systemui.kosmos.KosmosJavaAdapter;
import com.android.systemui.log.SessionTracker;
import com.android.systemui.navigationbar.NavigationModeController;
import com.android.systemui.scene.FakeWindowRootViewComponent;
@@ -150,6 +151,7 @@
@TestableLooper.RunWithLooper
@SmallTest
public class KeyguardViewMediatorTest extends SysuiTestCase {
+ private final KosmosJavaAdapter mKosmos = new KosmosJavaAdapter(this);
private KeyguardViewMediator mViewMediator;
private final TestScope mTestScope = TestScopeProvider.getTestScope();
@@ -265,7 +267,8 @@
mShadeWindowLogger,
() -> mSelectedUserInteractor,
mUserTracker,
- mSceneContainerFlags);
+ mSceneContainerFlags,
+ mKosmos::getCommunalInteractor);
mFeatureFlags = new FakeFeatureFlags();
mFeatureFlags.set(Flags.KEYGUARD_WM_STATE_REFACTOR, false);
mSetFlagsRule.enableFlags(FLAG_REFACTOR_GET_CURRENT_USER);
@@ -306,6 +309,28 @@
@Test
@TestableLooper.RunWithLooper(setAsMainLooper = true)
+ public void testRaceCondition_doNotRegisterCentralSurfacesImmediately() {
+ create(false);
+
+ // GIVEN central surfaces is not registered with KeyguardViewMediator, but a call to enable
+ // keyguard comes in
+ mViewMediator.onSystemReady();
+ mViewMediator.setKeyguardEnabled(true);
+ TestableLooper.get(this).processAllMessages();
+
+ // If this step has been reached, then system ui has not crashed. Now register
+ // CentralSurfaces
+ assertFalse(mViewMediator.isShowingAndNotOccluded());
+ register();
+ TestableLooper.get(this).moveTimeForward(100);
+ TestableLooper.get(this).processAllMessages();
+
+ // THEN keyguard is shown
+ assertTrue(mViewMediator.isShowingAndNotOccluded());
+ }
+
+ @Test
+ @TestableLooper.RunWithLooper(setAsMainLooper = true)
public void onLockdown_showKeyguard_evenIfKeyguardIsNotEnabledExternally() {
// GIVEN keyguard is not enabled and isn't showing
mViewMediator.onSystemReady();
@@ -1136,6 +1161,11 @@
}
private void createAndStartViewMediator(boolean orderUnlockAndWake) {
+ create(orderUnlockAndWake);
+ register();
+ }
+
+ private void create(boolean orderUnlockAndWake) {
mContext.getOrCreateTestableResources().addOverride(
com.android.internal.R.bool.config_orderUnlockAndWake, orderUnlockAndWake);
@@ -1186,7 +1216,9 @@
mSelectedUserInteractor,
mKeyguardInteractor);
mViewMediator.start();
+ }
+ private void register() {
mViewMediator.registerCentralSurfaces(mCentralSurfaces, null, null, null, null, null);
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractorTest.kt
index 809947d..6092b6b35 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractorTest.kt
@@ -66,6 +66,7 @@
bgDispatcher = super.testDispatcher,
mainDispatcher = super.testDispatcher,
keyguardInteractor = super.keyguardInteractor,
+ communalInteractor = super.communalInteractor,
flags = FakeFeatureFlags(),
keyguardSecurityModel = mock(),
powerInteractor = PowerInteractorFactory.create().powerInteractor,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTestCase.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTestCase.kt
index 339fd22..a03aed0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTestCase.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTestCase.kt
@@ -17,14 +17,18 @@
package com.android.systemui.keyguard.domain.interactor
import com.android.systemui.SysuiTestCase
+import com.android.systemui.communal.domain.interactor.CommunalInteractor
+import com.android.systemui.communal.domain.interactor.communalInteractor
import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
+import com.android.systemui.testKosmos
import com.android.systemui.util.mockito.mock
import dagger.Lazy
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestScope
open class KeyguardTransitionInteractorTestCase : SysuiTestCase() {
+ private val kosmos = testKosmos()
val testDispatcher = StandardTestDispatcher()
var testScope = TestScope(testDispatcher)
@@ -32,6 +36,7 @@
lateinit var transitionRepository: FakeKeyguardTransitionRepository
lateinit var keyguardInteractor: KeyguardInteractor
+ lateinit var communalInteractor: CommunalInteractor
lateinit var transitionInteractor: KeyguardTransitionInteractor
/**
@@ -51,6 +56,8 @@
keyguardInteractor =
KeyguardInteractorFactory.create(repository = keyguardRepository).keyguardInteractor
+ communalInteractor = kosmos.communalInteractor
+
transitionInteractor =
KeyguardTransitionInteractorFactory.create(
repository = transitionRepository,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt
index dafd9e6..4f3a63d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt
@@ -156,10 +156,11 @@
val glanceableHubTransitions =
GlanceableHubTransitions(
- testScope,
- transitionInteractor,
- transitionRepository,
- communalInteractor
+ scope = testScope,
+ bgDispatcher = kosmos.testDispatcher,
+ transitionInteractor = transitionInteractor,
+ transitionRepository = transitionRepository,
+ communalInteractor = communalInteractor
)
fromLockscreenTransitionInteractor =
FromLockscreenTransitionInteractor(
@@ -196,6 +197,7 @@
flags = featureFlags,
keyguardSecurityModel = keyguardSecurityModel,
powerInteractor = powerInteractor,
+ communalInteractor = communalInteractor,
selectedUserInteractor = mSelectedUserInteractor,
)
.apply { start() }
@@ -242,6 +244,7 @@
transitionRepository = transitionRepository,
transitionInteractor = transitionInteractor,
powerInteractor = powerInteractor,
+ communalInteractor = communalInteractor,
)
.apply { start() }
@@ -267,6 +270,7 @@
transitionRepository = transitionRepository,
transitionInteractor = transitionInteractor,
powerInteractor = powerInteractor,
+ communalInteractor = communalInteractor,
)
.apply { start() }
@@ -278,6 +282,7 @@
keyguardInteractor = keyguardInteractor,
transitionRepository = transitionRepository,
transitionInteractor = transitionInteractor,
+ communalInteractor = communalInteractor,
powerInteractor = powerInteractor,
)
.apply { start() }
@@ -288,6 +293,7 @@
bgDispatcher = kosmos.testDispatcher,
mainDispatcher = kosmos.testDispatcher,
glanceableHubTransitions = glanceableHubTransitions,
+ keyguardInteractor = keyguardInteractor,
transitionRepository = transitionRepository,
transitionInteractor = transitionInteractor,
powerInteractor = powerInteractor,
@@ -902,6 +908,37 @@
}
@Test
+ fun goneToGlanceableHub() =
+ testScope.runTest {
+ // GIVEN a prior transition has run to GONE
+ runTransitionAndSetWakefulness(KeyguardState.LOCKSCREEN, KeyguardState.GONE)
+
+ // GIVEN the device is idle on the glanceable hub
+ val idleTransitionState =
+ MutableStateFlow<ObservableCommunalTransitionState>(
+ ObservableCommunalTransitionState.Idle(CommunalSceneKey.Communal)
+ )
+ communalInteractor.setTransitionState(idleTransitionState)
+ runCurrent()
+
+ // WHEN the keyguard starts to show
+ keyguardRepository.setKeyguardShowing(true)
+ runCurrent()
+
+ val info =
+ withArgCaptor<TransitionInfo> {
+ verify(transitionRepository).startTransition(capture())
+ }
+ // THEN a transition to DOZING should occur
+ assertThat(info.ownerName).isEqualTo(FromGoneTransitionInteractor::class.simpleName)
+ assertThat(info.from).isEqualTo(KeyguardState.GONE)
+ assertThat(info.to).isEqualTo(KeyguardState.GLANCEABLE_HUB)
+ assertThat(info.animator).isNotNull()
+
+ coroutineContext.cancelChildren()
+ }
+
+ @Test
fun alternateBouncerToPrimaryBouncer() =
testScope.runTest {
// GIVEN a prior transition has run to ALTERNATE_BOUNCER
@@ -1023,6 +1060,45 @@
}
@Test
+ fun alternateBouncerToGlanceableHub() =
+ testScope.runTest {
+ // GIVEN a prior transition has run to ALTERNATE_BOUNCER
+ bouncerRepository.setAlternateVisible(true)
+ runTransitionAndSetWakefulness(
+ KeyguardState.LOCKSCREEN,
+ KeyguardState.ALTERNATE_BOUNCER
+ )
+
+ // GIVEN the primary bouncer isn't showing and device not sleeping
+ bouncerRepository.setPrimaryShow(false)
+
+ // GIVEN the device is idle on the glanceable hub
+ val idleTransitionState =
+ MutableStateFlow<ObservableCommunalTransitionState>(
+ ObservableCommunalTransitionState.Idle(CommunalSceneKey.Communal)
+ )
+ communalInteractor.setTransitionState(idleTransitionState)
+ runCurrent()
+
+ // WHEN the alternateBouncer stops showing
+ bouncerRepository.setAlternateVisible(false)
+ advanceUntilIdle()
+
+ val info =
+ withArgCaptor<TransitionInfo> {
+ verify(transitionRepository).startTransition(capture())
+ }
+ // THEN a transition to LOCKSCREEN should occur
+ assertThat(info.ownerName)
+ .isEqualTo(FromAlternateBouncerTransitionInteractor::class.simpleName)
+ assertThat(info.from).isEqualTo(KeyguardState.ALTERNATE_BOUNCER)
+ assertThat(info.to).isEqualTo(KeyguardState.GLANCEABLE_HUB)
+ assertThat(info.animator).isNotNull()
+
+ coroutineContext.cancelChildren()
+ }
+
+ @Test
fun primaryBouncerToAod() =
testScope.runTest {
// GIVEN a prior transition has run to PRIMARY_BOUNCER
@@ -1085,7 +1161,7 @@
bouncerRepository.setPrimaryShow(true)
runTransitionAndSetWakefulness(KeyguardState.LOCKSCREEN, KeyguardState.PRIMARY_BOUNCER)
- // WHEN the alternateBouncer stops showing
+ // WHEN the primaryBouncer stops showing
bouncerRepository.setPrimaryShow(false)
runCurrent()
@@ -1103,6 +1179,39 @@
}
@Test
+ fun primaryBouncerToGlanceableHub() =
+ testScope.runTest {
+ // GIVEN a prior transition has run to PRIMARY_BOUNCER
+ bouncerRepository.setPrimaryShow(true)
+ runTransitionAndSetWakefulness(KeyguardState.LOCKSCREEN, KeyguardState.PRIMARY_BOUNCER)
+
+ // GIVEN the device is idle on the glanceable hub
+ val idleTransitionState =
+ MutableStateFlow<ObservableCommunalTransitionState>(
+ ObservableCommunalTransitionState.Idle(CommunalSceneKey.Communal)
+ )
+ communalInteractor.setTransitionState(idleTransitionState)
+ runCurrent()
+
+ // WHEN the primaryBouncer stops showing
+ bouncerRepository.setPrimaryShow(false)
+ runCurrent()
+
+ val info =
+ withArgCaptor<TransitionInfo> {
+ verify(transitionRepository).startTransition(capture())
+ }
+ // THEN a transition to LOCKSCREEN should occur
+ assertThat(info.ownerName)
+ .isEqualTo(FromPrimaryBouncerTransitionInteractor::class.simpleName)
+ assertThat(info.from).isEqualTo(KeyguardState.PRIMARY_BOUNCER)
+ assertThat(info.to).isEqualTo(KeyguardState.GLANCEABLE_HUB)
+ assertThat(info.animator).isNotNull()
+
+ coroutineContext.cancelChildren()
+ }
+
+ @Test
fun primaryBouncerToDreamingLockscreenHosted() =
testScope.runTest {
// GIVEN device dreaming with the lockscreen hosted dream and not dozing
@@ -1193,6 +1302,43 @@
}
@Test
+ fun occludedToGlanceableHub() =
+ testScope.runTest {
+ // GIVEN a device on lockscreen
+ keyguardRepository.setKeyguardShowing(true)
+ runCurrent()
+
+ // GIVEN the device is idle on the glanceable hub
+ val idleTransitionState =
+ MutableStateFlow<ObservableCommunalTransitionState>(
+ ObservableCommunalTransitionState.Idle(CommunalSceneKey.Communal)
+ )
+ communalInteractor.setTransitionState(idleTransitionState)
+ runCurrent()
+
+ // GIVEN a prior transition has run to OCCLUDED
+ runTransitionAndSetWakefulness(KeyguardState.GLANCEABLE_HUB, KeyguardState.OCCLUDED)
+ keyguardRepository.setKeyguardOccluded(true)
+ runCurrent()
+
+ // WHEN occlusion ends
+ keyguardRepository.setKeyguardOccluded(false)
+ runCurrent()
+
+ val info =
+ withArgCaptor<TransitionInfo> {
+ verify(transitionRepository).startTransition(capture())
+ }
+ // THEN a transition to GLANCEABLE_HUB should occur
+ assertThat(info.ownerName).isEqualTo(FromOccludedTransitionInteractor::class.simpleName)
+ assertThat(info.from).isEqualTo(KeyguardState.OCCLUDED)
+ assertThat(info.to).isEqualTo(KeyguardState.GLANCEABLE_HUB)
+ assertThat(info.animator).isNotNull()
+
+ coroutineContext.cancelChildren()
+ }
+
+ @Test
fun occludedToAlternateBouncer() =
testScope.runTest {
// GIVEN a prior transition has run to OCCLUDED
@@ -1640,6 +1786,111 @@
coroutineContext.cancelChildren()
}
+ @Test
+ fun glanceableHubToPrimaryBouncer() =
+ testScope.runTest {
+ // GIVEN a prior transition has run to ALTERNATE_BOUNCER
+ runTransitionAndSetWakefulness(KeyguardState.LOCKSCREEN, KeyguardState.GLANCEABLE_HUB)
+
+ // WHEN the primary bouncer shows
+ bouncerRepository.setPrimaryShow(true)
+ runCurrent()
+
+ val info =
+ withArgCaptor<TransitionInfo> {
+ verify(transitionRepository).startTransition(capture())
+ }
+ // THEN a transition to PRIMARY_BOUNCER should occur
+ assertThat(info.ownerName)
+ .isEqualTo(FromGlanceableHubTransitionInteractor::class.simpleName)
+ assertThat(info.from).isEqualTo(KeyguardState.GLANCEABLE_HUB)
+ assertThat(info.to).isEqualTo(KeyguardState.PRIMARY_BOUNCER)
+ assertThat(info.animator).isNotNull()
+
+ coroutineContext.cancelChildren()
+ }
+
+ @Test
+ fun glanceableHubToAlternateBouncer() =
+ testScope.runTest {
+ // GIVEN a prior transition has run to ALTERNATE_BOUNCER
+ runTransitionAndSetWakefulness(KeyguardState.LOCKSCREEN, KeyguardState.GLANCEABLE_HUB)
+
+ // WHEN the primary bouncer shows
+ bouncerRepository.setAlternateVisible(true)
+ runCurrent()
+
+ val info =
+ withArgCaptor<TransitionInfo> {
+ verify(transitionRepository).startTransition(capture())
+ }
+ // THEN a transition to PRIMARY_BOUNCER should occur
+ assertThat(info.ownerName)
+ .isEqualTo(FromGlanceableHubTransitionInteractor::class.simpleName)
+ assertThat(info.from).isEqualTo(KeyguardState.GLANCEABLE_HUB)
+ assertThat(info.to).isEqualTo(KeyguardState.ALTERNATE_BOUNCER)
+ assertThat(info.animator).isNotNull()
+
+ coroutineContext.cancelChildren()
+ }
+
+ @Test
+ fun glanceableHubToOccluded() =
+ testScope.runTest {
+ // GIVEN a prior transition has run to GLANCEABLE_HUB
+ runTransitionAndSetWakefulness(KeyguardState.GONE, KeyguardState.GLANCEABLE_HUB)
+ runCurrent()
+
+ // GIVEN the device is idle on the glanceable hub
+ val idleTransitionState =
+ MutableStateFlow<ObservableCommunalTransitionState>(
+ ObservableCommunalTransitionState.Idle(CommunalSceneKey.Communal)
+ )
+ communalInteractor.setTransitionState(idleTransitionState)
+ runCurrent()
+
+ // WHEN the keyguard is occluded
+ keyguardRepository.setKeyguardOccluded(true)
+ runCurrent()
+
+ val info =
+ withArgCaptor<TransitionInfo> {
+ verify(transitionRepository).startTransition(capture())
+ }
+ // THEN a transition to OCCLUDED should occur
+ assertThat(info.ownerName)
+ .isEqualTo(FromGlanceableHubTransitionInteractor::class.simpleName)
+ assertThat(info.from).isEqualTo(KeyguardState.GLANCEABLE_HUB)
+ assertThat(info.to).isEqualTo(KeyguardState.OCCLUDED)
+ assertThat(info.animator).isNotNull()
+
+ coroutineContext.cancelChildren()
+ }
+
+ @Test
+ fun glanceableHubToGone() =
+ testScope.runTest {
+ // GIVEN a prior transition has run to GLANCEABLE_HUB
+ runTransitionAndSetWakefulness(KeyguardState.LOCKSCREEN, KeyguardState.GLANCEABLE_HUB)
+
+ // WHEN keyguard goes away
+ keyguardRepository.setKeyguardGoingAway(true)
+ runCurrent()
+
+ val info =
+ withArgCaptor<TransitionInfo> {
+ verify(transitionRepository).startTransition(capture())
+ }
+ // THEN a transition to DOZING should occur
+ assertThat(info.ownerName)
+ .isEqualTo(FromGlanceableHubTransitionInteractor::class.simpleName)
+ assertThat(info.from).isEqualTo(KeyguardState.GLANCEABLE_HUB)
+ assertThat(info.to).isEqualTo(KeyguardState.GONE)
+ assertThat(info.animator).isNotNull()
+
+ coroutineContext.cancelChildren()
+ }
+
private fun createKeyguardInteractor(): KeyguardInteractor {
return KeyguardInteractorFactory.create(
featureFlags = featureFlags,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSectionTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSectionTest.kt
index 57b5559..acb6ff0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSectionTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSectionTest.kt
@@ -19,12 +19,15 @@
import android.content.pm.PackageManager
import android.content.res.Resources
+import android.view.View.GONE
+import android.view.View.VISIBLE
import androidx.constraintlayout.widget.ConstraintSet
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.keyguard.domain.interactor.KeyguardBlueprintInteractor
import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor
import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel
+import com.android.systemui.keyguard.ui.viewmodel.KeyguardSmartspaceViewModel
import com.android.systemui.res.R
import com.android.systemui.statusbar.policy.SplitShadeStateController
import com.android.systemui.util.Utils
@@ -49,8 +52,11 @@
@Mock private lateinit var keyguardClockInteractor: KeyguardClockInteractor
@Mock private lateinit var keyguardClockViewModel: KeyguardClockViewModel
@Mock private lateinit var splitShadeStateController: SplitShadeStateController
+ @Mock private lateinit var smartspaceViewModel: KeyguardSmartspaceViewModel
@Mock private lateinit var blueprintInteractor: Lazy<KeyguardBlueprintInteractor>
+ private val bcSmartspaceVisibility: MutableStateFlow<Int> = MutableStateFlow(VISIBLE)
private val clockShouldBeCentered: MutableStateFlow<Boolean> = MutableStateFlow(true)
+ private val isAodIconsVisible: MutableStateFlow<Boolean> = MutableStateFlow(true)
private lateinit var underTest: ClockSection
@@ -110,6 +116,8 @@
mContext.setMockPackageManager(packageManager)
whenever(keyguardClockViewModel.clockShouldBeCentered).thenReturn(clockShouldBeCentered)
+ whenever(keyguardClockViewModel.isAodIconsVisible).thenReturn(isAodIconsVisible)
+ whenever(smartspaceViewModel.bcSmartspaceVisibility).thenReturn(bcSmartspaceVisibility)
underTest =
ClockSection(
@@ -117,6 +125,7 @@
keyguardClockViewModel,
mContext,
splitShadeStateController,
+ smartspaceViewModel,
blueprintInteractor
)
}
@@ -176,6 +185,40 @@
assetSmallClockTop(cs, expectedSmallClockTopMargin)
}
+ @Test
+ fun testSmartspaceVisible_weatherClockDateAndIconsBarrierBottomBelowBCSmartspace() {
+ isAodIconsVisible.value = false
+ bcSmartspaceVisibility.value = VISIBLE
+ val cs = ConstraintSet()
+ underTest.applyDefaultConstraints(cs)
+ val referencedIds = cs.getReferencedIds(R.id.weather_clock_date_and_icons_barrier_bottom)
+ referencedIds.contentEquals(intArrayOf(com.android.systemui.shared.R.id.bc_smartspace_view))
+ }
+
+ @Test
+ fun testSmartspaceGone_weatherClockDateAndIconsBarrierBottomBelowSmartspaceDateWeather() {
+ isAodIconsVisible.value = false
+ bcSmartspaceVisibility.value = GONE
+ val cs = ConstraintSet()
+ underTest.applyDefaultConstraints(cs)
+ val referencedIds = cs.getReferencedIds(R.id.weather_clock_date_and_icons_barrier_bottom)
+ referencedIds.contentEquals(intArrayOf(R.id.lockscreen_clock_view))
+ }
+
+ @Test
+ fun testHasAodIcons_weatherClockDateAndIconsBarrierBottomBelowSmartspaceDateWeather() {
+ isAodIconsVisible.value = true
+ val cs = ConstraintSet()
+ underTest.applyDefaultConstraints(cs)
+ val referencedIds = cs.getReferencedIds(R.id.weather_clock_date_and_icons_barrier_bottom)
+ referencedIds.contentEquals(
+ intArrayOf(
+ com.android.systemui.shared.R.id.bc_smartspace_view,
+ R.id.aod_notification_icon_container
+ )
+ )
+ }
+
private fun setLargeClock(useLargeClock: Boolean) {
whenever(keyguardClockViewModel.useLargeClock).thenReturn(useLargeClock)
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModelTest.kt
index 1b4573d..22a2e93 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModelTest.kt
@@ -34,12 +34,14 @@
import com.android.systemui.plugins.clocks.ClockFaceConfig
import com.android.systemui.plugins.clocks.ClockFaceController
import com.android.systemui.shared.clocks.ClockRegistry
+import com.android.systemui.statusbar.notification.domain.interactor.NotificationsKeyguardInteractor
import com.android.systemui.statusbar.policy.SplitShadeStateController
import com.android.systemui.util.mockito.whenever
import com.android.systemui.util.settings.FakeSettings
import com.google.common.truth.Truth.assertThat
import kotlin.test.Test
import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestCoroutineScheduler
import kotlinx.coroutines.test.TestScope
@@ -68,6 +70,8 @@
@Mock private lateinit var clockFaceConfig: ClockFaceConfig
@Mock private lateinit var eventController: ClockEventController
@Mock private lateinit var splitShadeStateController: SplitShadeStateController
+ @Mock private lateinit var notifsKeyguardInteractor: NotificationsKeyguardInteractor
+ @Mock private lateinit var areNotificationsFullyHidden: Flow<Boolean>
@Before
fun setup() {
@@ -90,12 +94,15 @@
scope.backgroundScope
)
keyguardClockInteractor = KeyguardClockInteractor(keyguardClockRepository)
+ whenever(notifsKeyguardInteractor.areNotificationsFullyHidden)
+ .thenReturn(areNotificationsFullyHidden)
underTest =
KeyguardClockViewModel(
keyguardInteractor,
keyguardClockInteractor,
scope.backgroundScope,
splitShadeStateController,
+ notifsKeyguardInteractor
)
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/gestural/BackPanelControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/gestural/BackPanelControllerTest.kt
index f93d52b..aa54565 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/gestural/BackPanelControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/gestural/BackPanelControllerTest.kt
@@ -28,6 +28,7 @@
import android.view.ViewConfiguration
import android.view.WindowManager
import androidx.test.filters.SmallTest
+import com.android.internal.jank.InteractionJankMonitor
import com.android.internal.util.LatencyTracker
import com.android.systemui.SysuiTestCase
import com.android.systemui.plugins.NavigationEdgeBackPlugin
@@ -40,8 +41,10 @@
import org.mockito.ArgumentMatchers.any
import org.mockito.ArgumentMatchers.eq
import org.mockito.Mock
+import org.mockito.Mockito.anyInt
import org.mockito.Mockito.clearInvocations
import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
import org.mockito.MockitoAnnotations
@SmallTest
@@ -59,12 +62,16 @@
@Mock private lateinit var windowManager: WindowManager
@Mock private lateinit var configurationController: ConfigurationController
@Mock private lateinit var latencyTracker: LatencyTracker
+ @Mock private lateinit var interactionJankMonitor: InteractionJankMonitor
@Mock private lateinit var layoutParams: WindowManager.LayoutParams
@Mock private lateinit var backCallback: NavigationEdgeBackPlugin.BackCallback
@Before
fun setup() {
MockitoAnnotations.initMocks(this)
+ `when`(interactionJankMonitor.begin(any(), anyInt())).thenReturn(true)
+ `when`(interactionJankMonitor.end(anyInt())).thenReturn(true)
+ `when`(interactionJankMonitor.cancel(anyInt())).thenReturn(true)
mBackPanelController =
BackPanelController(
context,
@@ -74,6 +81,7 @@
vibratorHelper,
configurationController,
latencyTracker,
+ interactionJankMonitor,
)
mBackPanelController.setLayoutParams(layoutParams)
mBackPanelController.setBackCallback(backCallback)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/privacy/PrivacyDialogControllerV2Test.kt b/packages/SystemUI/tests/src/com/android/systemui/privacy/PrivacyDialogControllerV2Test.kt
index e4432f3..0636831 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/privacy/PrivacyDialogControllerV2Test.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/privacy/PrivacyDialogControllerV2Test.kt
@@ -30,13 +30,11 @@
import android.permission.PermissionGroupUsage
import android.permission.PermissionManager
import android.testing.AndroidTestingRunner
-import android.view.View
import android.widget.LinearLayout
import androidx.test.filters.SmallTest
import com.android.internal.logging.UiEventLogger
import com.android.systemui.SysuiTestCase
import com.android.systemui.animation.DialogLaunchAnimator
-import com.android.systemui.animation.LaunchableView
import com.android.systemui.appops.AppOpsController
import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.privacy.logging.PrivacyLogger
@@ -206,10 +204,7 @@
@Test
fun testShowDialogShowsDialogWithView() {
val parent = LinearLayout(context)
- val view =
- object : View(context), LaunchableView {
- override fun setShouldBlockVisibilityChanges(block: Boolean) {}
- }
+ val view = OngoingPrivacyChip(context)
parent.addView(view)
val usage = createMockPermGroupUsage()
`when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(listOf(usage))
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/HeaderPrivacyIconsControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/HeaderPrivacyIconsControllerTest.kt
index fa02e8c..f98b68f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/HeaderPrivacyIconsControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/HeaderPrivacyIconsControllerTest.kt
@@ -173,7 +173,7 @@
captor.value.onClick(privacyChip)
verify(privacyDialogController).showDialog(any(Context::class.java))
verify(privacyDialogControllerV2, never())
- .showDialog(any(Context::class.java), any(View::class.java))
+ .showDialog(any(Context::class.java), any(OngoingPrivacyChip::class.java))
}
@Test
@@ -186,7 +186,7 @@
captor.value.onClick(privacyChip)
verify(privacyDialogController).showDialog(any(Context::class.java))
verify(privacyDialogControllerV2, never())
- .showDialog(any(Context::class.java), any(View::class.java))
+ .showDialog(any(Context::class.java), any(OngoingPrivacyChip::class.java))
}
@Test
@@ -207,7 +207,7 @@
captor.value.onClick(privacyChip)
verify(privacyDialogController, never()).showDialog(any(Context::class.java))
verify(privacyDialogControllerV2, never())
- .showDialog(any(Context::class.java), any(View::class.java))
+ .showDialog(any(Context::class.java), any(OngoingPrivacyChip::class.java))
}
@Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/QSImplTest.java
index c8c134a..563a3fe 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSImplTest.java
@@ -35,6 +35,7 @@
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyZeroInteractions;
import static org.mockito.Mockito.when;
import android.annotation.Nullable;
@@ -47,6 +48,7 @@
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
+import android.widget.FrameLayout;
import androidx.lifecycle.Lifecycle;
import androidx.test.filters.SmallTest;
@@ -63,6 +65,7 @@
import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel;
import com.android.systemui.qs.logging.QSLogger;
import com.android.systemui.res.R;
+import com.android.systemui.scene.shared.flag.SceneContainerFlags;
import com.android.systemui.settings.FakeDisplayTracker;
import com.android.systemui.shade.transition.LargeScreenShadeInterpolator;
import com.android.systemui.statusbar.CommandQueue;
@@ -111,7 +114,8 @@
@Mock private FooterActionsViewBinder mFooterActionsViewBinder;
@Mock private LargeScreenShadeInterpolator mLargeScreenShadeInterpolator;
@Mock private FeatureFlagsClassic mFeatureFlags;
- private View mQsView;
+ @Mock private SceneContainerFlags mSceneContainerFlags;
+ private ViewGroup mQsView;
private final CommandQueue mCommandQueue =
new CommandQueue(mContext, new FakeDisplayTracker(mContext));
@@ -121,6 +125,9 @@
@Before
public void setup() {
+ MockitoAnnotations.initMocks(this);
+ when(mSceneContainerFlags.isEnabled()).thenReturn(false);
+
mUnderTest = instantiate();
mUnderTest.onComponentCreated(mQsComponent, null);
@@ -487,9 +494,24 @@
verify(mQSAnimator).setOnKeyguard(true);
}
- private QSImpl instantiate() {
- MockitoAnnotations.initMocks(this);
+ @Test
+ public void testSceneContainerFlagsEnabled_FooterActionsRemoved_controllerNotStarted() {
+ when(mSceneContainerFlags.isEnabled()).thenReturn(true);
+ clearInvocations(
+ mFooterActionsViewBinder, mFooterActionsViewModel, mFooterActionsViewModelFactory);
+ QSImpl other = instantiate();
+ other.onComponentCreated(mQsComponent, null);
+
+ assertThat((View) other.getView().findViewById(R.id.qs_footer_actions)).isNull();
+ verifyZeroInteractions(
+ mFooterActionsViewModel,
+ mFooterActionsViewBinder,
+ mFooterActionsViewModelFactory
+ );
+ }
+
+ private QSImpl instantiate() {
setupQsComponent();
setUpViews();
setUpInflater();
@@ -514,7 +536,8 @@
mFooterActionsViewModelFactory,
mFooterActionsViewBinder,
mLargeScreenShadeInterpolator,
- mFeatureFlags);
+ mFeatureFlags,
+ mSceneContainerFlags);
}
private void setUpOther() {
@@ -533,14 +556,23 @@
}
private void setUpViews() {
- mQsView = spy(new View(mContext));
+ mQsView = spy(new FrameLayout(mContext));
when(mQsComponent.getRootView()).thenReturn(mQsView);
- when(mQsView.findViewById(R.id.expanded_qs_scroll_view))
+
+ when(mQSPanelScrollView.findViewById(R.id.expanded_qs_scroll_view))
.thenReturn(mQSPanelScrollView);
- when(mQsView.findViewById(R.id.header)).thenReturn(mHeader);
- when(mQsView.findViewById(android.R.id.edit)).thenReturn(new View(mContext));
- when(mQsView.findViewById(R.id.qs_footer_actions)).thenAnswer(
- invocation -> new FooterActionsViewBinder().create(mContext));
+ mQsView.addView(mQSPanelScrollView);
+
+ when(mHeader.findViewById(R.id.header)).thenReturn(mHeader);
+ mQsView.addView(mHeader);
+
+ View customizer = new View(mContext);
+ customizer.setId(android.R.id.edit);
+ mQsView.addView(customizer);
+
+ View footerActionsView = new FooterActionsViewBinder().create(mContext);
+ footerActionsView.setId(R.id.qs_footer_actions);
+ mQsView.addView(footerActionsView);
}
private void setUpInflater() {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSIconViewImplTest_311121830.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSIconViewImplTest_311121830.kt
new file mode 100644
index 0000000..e8aa8f0
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSIconViewImplTest_311121830.kt
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.tileimpl
+
+import android.animation.AnimatorTestRule
+import android.content.Context
+import android.service.quicksettings.Tile
+import android.testing.AndroidTestingRunner
+import android.testing.UiThreadTest
+import android.view.ContextThemeWrapper
+import android.view.View
+import android.widget.ImageView
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.plugins.qs.QSTile
+import com.android.systemui.res.R
+import com.android.systemui.statusbar.connectivity.WifiIcons
+import com.google.common.truth.Truth.assertThat
+import kotlin.test.Test
+import org.junit.Rule
+import org.junit.runner.RunWith
+
+/** Test for regression b/311121830 */
+@RunWith(AndroidTestingRunner::class)
+@UiThreadTest
+@SmallTest
+class QSIconViewImplTest_311121830 : SysuiTestCase() {
+
+ @get:Rule val animatorRule = AnimatorTestRule()
+
+ @Test
+ fun alwaysLastIcon() {
+ // Need to inflate with the correct theme so the colors can be retrieved and the animations
+ // are run
+ val iconView =
+ AnimateQSIconViewImpl(
+ ContextThemeWrapper(context, R.style.Theme_SystemUI_QuickSettings)
+ )
+
+ val initialState =
+ QSTile.State().apply {
+ state = Tile.STATE_INACTIVE
+ icon = QSTileImpl.ResourceIcon.get(R.drawable.ic_qs_no_internet_available)
+ }
+ val firstState =
+ QSTile.State().apply {
+ state = Tile.STATE_ACTIVE
+ icon = QSTileImpl.ResourceIcon.get(WifiIcons.WIFI_NO_INTERNET_ICONS[4])
+ }
+ val secondState =
+ QSTile.State().apply {
+ state = Tile.STATE_ACTIVE
+ icon = QSTileImpl.ResourceIcon.get(WifiIcons.WIFI_FULL_ICONS[4])
+ }
+
+ // Start with the initial state
+ iconView.setIcon(initialState, /* allowAnimations= */ false)
+
+ // Set the first state to animate, and advance time to half the time of the animation
+ iconView.setIcon(firstState, /* allowAnimations= */ true)
+ animatorRule.advanceTimeBy(QSIconViewImpl.QS_ANIM_LENGTH / 2)
+
+ // Set the second state to animate (it shouldn't, because `State.state` is the same) and
+ // advance time to 2 animations length
+ iconView.setIcon(secondState, /* allowAnimations= */ true)
+ animatorRule.advanceTimeBy(QSIconViewImpl.QS_ANIM_LENGTH * 2)
+
+ assertThat(iconView.mLastIcon).isEqualTo(secondState.icon)
+ }
+
+ private class AnimateQSIconViewImpl(context: Context) : QSIconViewImpl(context) {
+ override fun createIcon(): View {
+ return object : ImageView(context) {
+ override fun isShown(): Boolean {
+ return true
+ }
+ }
+ }
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/RecordIssueTileTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/RecordIssueTileTest.kt
index c7479fd5..1ed8c3c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/RecordIssueTileTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/RecordIssueTileTest.kt
@@ -32,6 +32,7 @@
import com.android.systemui.qs.logging.QSLogger
import com.android.systemui.recordissue.RecordIssueDialogDelegate
import com.android.systemui.res.R
+import com.android.systemui.settings.UserContextProvider
import com.android.systemui.statusbar.phone.KeyguardDismissUtil
import com.android.systemui.statusbar.phone.SystemUIDialog
import com.android.systemui.statusbar.policy.KeyguardStateController
@@ -65,6 +66,7 @@
@Mock private lateinit var keyguardDismissUtil: KeyguardDismissUtil
@Mock private lateinit var keyguardStateController: KeyguardStateController
@Mock private lateinit var dialogLauncherAnimator: DialogLaunchAnimator
+ @Mock private lateinit var userContextProvider: UserContextProvider
@Mock private lateinit var delegateFactory: RecordIssueDialogDelegate.Factory
@Mock private lateinit var dialogDelegate: RecordIssueDialogDelegate
@Mock private lateinit var dialog: SystemUIDialog
@@ -94,6 +96,7 @@
keyguardDismissUtil,
keyguardStateController,
dialogLauncherAnimator,
+ userContextProvider,
delegateFactory,
)
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDialogControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDialogControllerTest.java
index b24b877..c0ef50f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDialogControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDialogControllerTest.java
@@ -1069,6 +1069,22 @@
assertThat(mInternetDialogController.mCallback).isNull();
}
+ @Test
+ public void hasActiveSubId_activeSubIdListIsEmpty_returnFalse() {
+ when(mSubscriptionManager.getActiveSubscriptionIdList()).thenReturn(new int[]{});
+ mInternetDialogController.mOnSubscriptionsChangedListener.onSubscriptionsChanged();
+
+ assertThat(mInternetDialogController.hasActiveSubId()).isFalse();
+ }
+
+ @Test
+ public void hasActiveSubId_activeSubIdListNotEmpty_returnTrue() {
+ when(mSubscriptionManager.getActiveSubscriptionIdList()).thenReturn(new int[]{SUB_ID});
+ mInternetDialogController.mOnSubscriptionsChangedListener.onSubscriptionsChanged();
+
+ assertThat(mInternetDialogController.hasActiveSubId()).isTrue();
+ }
+
private String getResourcesString(String name) {
return mContext.getResources().getString(getResourcesId(name));
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt
index 81ff817..a6e240b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt
@@ -22,6 +22,7 @@
import android.testing.ViewUtils
import android.view.MotionEvent
import android.view.View
+import android.widget.FrameLayout
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.communal.data.repository.FakeCommunalRepository
@@ -61,6 +62,7 @@
@Mock private lateinit var shadeInteractor: ShadeInteractor
@Mock private lateinit var powerManager: PowerManager
+ private lateinit var parentView: FrameLayout
private lateinit var containerView: View
private lateinit var testableLooper: TestableLooper
@@ -94,7 +96,12 @@
.thenReturn(bouncerShowingFlow)
whenever(shadeInteractor.isAnyFullyExpanded).thenReturn(shadeShowingFlow)
- overrideResource(R.dimen.communal_grid_gutter_size, SWIPE_REGION_WIDTH)
+ overrideResource(R.dimen.communal_right_edge_swipe_region_width, RIGHT_SWIPE_REGION_WIDTH)
+ overrideResource(R.dimen.communal_top_edge_swipe_region_height, TOP_SWIPE_REGION_WIDTH)
+ overrideResource(
+ R.dimen.communal_bottom_edge_swipe_region_height,
+ BOTTOM_SWIPE_REGION_WIDTH
+ )
}
@Test
@@ -135,7 +142,7 @@
initAndAttachContainerView()
// Touch events are intercepted.
- assertThat(underTest.onTouchEvent(DOWN_IN_SWIPE_REGION_EVENT)).isTrue()
+ assertThat(underTest.onTouchEvent(DOWN_IN_RIGHT_SWIPE_REGION_EVENT)).isTrue()
}
@Test
@@ -147,7 +154,7 @@
// Initial touch down is intercepted, and so are touches outside of the region, until an up
// event is received.
- assertThat(underTest.onTouchEvent(DOWN_IN_SWIPE_REGION_EVENT)).isTrue()
+ assertThat(underTest.onTouchEvent(DOWN_IN_RIGHT_SWIPE_REGION_EVENT)).isTrue()
assertThat(underTest.onTouchEvent(MOVE_EVENT)).isTrue()
assertThat(underTest.onTouchEvent(UP_EVENT)).isTrue()
assertThat(underTest.onTouchEvent(MOVE_EVENT)).isFalse()
@@ -168,6 +175,28 @@
}
@Test
+ fun onTouchEvent_topSwipeWhenHubOpen_returnsFalse() {
+ // Communal is open.
+ communalRepository.setDesiredScene(CommunalSceneKey.Communal)
+
+ initAndAttachContainerView()
+
+ // Touch event in the top swipe reqgion is not intercepted.
+ assertThat(underTest.onTouchEvent(DOWN_IN_TOP_SWIPE_REGION_EVENT)).isFalse()
+ }
+
+ @Test
+ fun onTouchEvent_bottomSwipeWhenHubOpen_returnsFalse() {
+ // Communal is open.
+ communalRepository.setDesiredScene(CommunalSceneKey.Communal)
+
+ initAndAttachContainerView()
+
+ // Touch event in the bottom swipe reqgion is not intercepted.
+ assertThat(underTest.onTouchEvent(DOWN_IN_BOTTOM_SWIPE_REGION_EVENT)).isFalse()
+ }
+
+ @Test
fun onTouchEvent_communalAndBouncerShowing_doesNotIntercept() {
// Communal is open.
communalRepository.setDesiredScene(CommunalSceneKey.Communal)
@@ -198,26 +227,70 @@
assertThat(underTest.onTouchEvent(DOWN_EVENT)).isFalse()
}
+ @Test
+ fun onTouchEvent_containerViewDisposed_doesNotIntercept() {
+ // Communal is open.
+ communalRepository.setDesiredScene(CommunalSceneKey.Communal)
+
+ initAndAttachContainerView()
+ testableLooper.processAllMessages()
+
+ // Touch events are intercepted.
+ assertThat(underTest.onTouchEvent(DOWN_EVENT)).isTrue()
+
+ // Container view disposed.
+ underTest.disposeView()
+
+ // Touch events are not intercepted.
+ assertThat(underTest.onTouchEvent(DOWN_EVENT)).isFalse()
+ }
+
private fun initAndAttachContainerView() {
containerView = View(context)
+
+ parentView = FrameLayout(context)
+ parentView.addView(containerView)
+
// Make view clickable so that dispatchTouchEvent returns true.
containerView.isClickable = true
underTest.initView(containerView)
// Attach the view so that flows start collecting.
- ViewUtils.attachView(containerView)
+ ViewUtils.attachView(parentView)
// Give the view a size so that determining if a touch starts at the right edge works.
+ parentView.layout(0, 0, CONTAINER_WIDTH, CONTAINER_HEIGHT)
containerView.layout(0, 0, CONTAINER_WIDTH, CONTAINER_HEIGHT)
}
companion object {
private const val CONTAINER_WIDTH = 100
private const val CONTAINER_HEIGHT = 100
- private const val SWIPE_REGION_WIDTH = 20
+ private const val RIGHT_SWIPE_REGION_WIDTH = 20
+ private const val TOP_SWIPE_REGION_WIDTH = 20
+ private const val BOTTOM_SWIPE_REGION_WIDTH = 20
- private val DOWN_EVENT = MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 0f, 0f, 0)
- private val DOWN_IN_SWIPE_REGION_EVENT =
+ private val DOWN_EVENT =
+ MotionEvent.obtain(
+ 0L,
+ 0L,
+ MotionEvent.ACTION_DOWN,
+ CONTAINER_WIDTH.toFloat(),
+ CONTAINER_HEIGHT.toFloat(),
+ 0
+ )
+ private val DOWN_IN_RIGHT_SWIPE_REGION_EVENT =
MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, CONTAINER_WIDTH.toFloat(), 0f, 0)
+ private val DOWN_IN_TOP_SWIPE_REGION_EVENT =
+ MotionEvent.obtain(
+ 0L,
+ 0L,
+ MotionEvent.ACTION_DOWN,
+ 0f,
+ TOP_SWIPE_REGION_WIDTH.toFloat(),
+ 0
+ )
+ private val DOWN_IN_BOTTOM_SWIPE_REGION_EVENT =
+ MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 0f, CONTAINER_HEIGHT.toFloat(), 0)
private val MOVE_EVENT = MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_MOVE, 0f, 0f, 0)
private val UP_EVENT = MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_UP, 0f, 0f, 0)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java
index 0bffa1c..461db8e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java
@@ -226,6 +226,7 @@
powerInteractor,
new GlanceableHubTransitions(
mTestScope,
+ mKosmos.getTestDispatcher(),
keyguardTransitionInteractor,
keyguardTransitionRepository,
communalInteractor
@@ -247,6 +248,7 @@
mKosmos.getTestDispatcher(),
mKosmos.getTestDispatcher(),
keyguardInteractor,
+ communalInteractor,
featureFlags,
mKeyguardSecurityModel,
mSelectedUserInteractor,
@@ -301,7 +303,8 @@
mShadeWindowLogger,
() -> mSelectedUserInteractor,
mUserTracker,
- mSceneContainerFlags) {
+ mSceneContainerFlags,
+ () -> communalInteractor) {
@Override
protected boolean isDebuggable() {
return false;
@@ -449,6 +452,24 @@
}
@Test
+ public void setCommunalShowing_userTimeout() {
+ setKeyguardShowing();
+ clearInvocations(mWindowManager);
+
+ mNotificationShadeWindowController.onCommunalShowingChanged(true);
+ verify(mWindowManager).updateViewLayout(any(), mLayoutParameters.capture());
+ assertThat(mLayoutParameters.getValue().userActivityTimeout)
+ .isEqualTo(CommunalInteractor.AWAKE_INTERVAL_MS);
+ clearInvocations(mWindowManager);
+
+ // Bouncer showing over communal overrides communal value
+ mNotificationShadeWindowController.setBouncerShowing(true);
+ verify(mWindowManager).updateViewLayout(any(), mLayoutParameters.capture());
+ assertThat(mLayoutParameters.getValue().userActivityTimeout)
+ .isEqualTo(KeyguardViewMediator.AWAKE_INTERVAL_BOUNCER_MS);
+ }
+
+ @Test
public void setKeyguardShowing_notFocusable_byDefault() {
mNotificationShadeWindowController.setKeyguardShowing(false);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt
index a11839c..6681cee 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt
@@ -74,10 +74,12 @@
import com.google.common.truth.Truth.assertThat
import java.util.Optional
import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest
import org.junit.Before
+import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentCaptor
@@ -480,6 +482,7 @@
}
@Test
+ @Ignore("b/321332798")
fun setsUpCommunalHubLayout_whenFlagEnabled() {
if (!isComposeAvailable()) {
return
@@ -511,6 +514,8 @@
}
whenever(mGlanceableHubContainerController.isEnabled()).thenReturn(false)
+ whenever(mGlanceableHubContainerController.enabledState())
+ .thenReturn(MutableStateFlow(false))
val mockCommunalPlaceholder = mock(View::class.java)
val fakeViewIndex = 20
@@ -520,8 +525,7 @@
underTest.setupCommunalHubLayout()
- // No adding or removing of views occurs.
- verify(view, times(0)).removeView(mockCommunalPlaceholder)
+ // No adding of views occurs.
verify(view, times(0)).addView(any(), eq(fakeViewIndex))
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerBaseTest.java
index 6fc88ce..3e0a647 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerBaseTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerBaseTest.java
@@ -257,6 +257,7 @@
powerInteractor,
new GlanceableHubTransitions(
mTestScope,
+ mKosmos.getTestDispatcher(),
keyguardTransitionInteractor,
keyguardTransitionRepository,
communalInteractor
@@ -278,6 +279,7 @@
mKosmos.getTestDispatcher(),
mKosmos.getTestDispatcher(),
keyguardInteractor,
+ communalInteractor,
featureFlags,
mock(KeyguardSecurityModel.class),
mSelectedUserInteractor,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/ShadeControllerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/ShadeControllerImplTest.kt
index c4911a4..cc79ca4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/ShadeControllerImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/ShadeControllerImplTest.kt
@@ -22,15 +22,18 @@
import android.view.WindowManager
import androidx.test.filters.SmallTest
import com.android.internal.statusbar.IStatusBarService
-import com.android.keyguard.TestScopeProvider
import com.android.systemui.SysuiTestCase
import com.android.systemui.assist.AssistManager
import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testScope
import com.android.systemui.log.LogBuffer
import com.android.systemui.plugins.statusbar.StatusBarStateController
import com.android.systemui.power.domain.interactor.PowerInteractorFactory
import com.android.systemui.scene.data.repository.WindowRootViewVisibilityRepository
import com.android.systemui.scene.domain.interactor.WindowRootViewVisibilityInteractor
+import com.android.systemui.scene.domain.interactor.sceneInteractor
+import com.android.systemui.scene.shared.flag.sceneContainerFlags
import com.android.systemui.statusbar.CommandQueue
import com.android.systemui.statusbar.NotificationShadeWindowController
import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationListRepository
@@ -64,6 +67,8 @@
private val executor = FakeExecutor(FakeSystemClock())
private val testDispatcher = StandardTestDispatcher()
private val activeNotificationsRepository = ActiveNotificationListRepository()
+ private val kosmos = Kosmos()
+ private val testScope = kosmos.testScope
@Mock private lateinit var commandQueue: CommandQueue
@Mock private lateinit var keyguardStateController: KeyguardStateController
@@ -84,12 +89,14 @@
private val windowRootViewVisibilityInteractor: WindowRootViewVisibilityInteractor by lazy {
WindowRootViewVisibilityInteractor(
- TestScopeProvider.getTestScope(),
+ testScope,
WindowRootViewVisibilityRepository(iStatusBarService, executor),
FakeKeyguardRepository(),
headsUpManager,
PowerInteractorFactory.create().powerInteractor,
- ActiveNotificationsInteractor(activeNotificationsRepository, testDispatcher)
+ ActiveNotificationsInteractor(activeNotificationsRepository, testDispatcher),
+ kosmos.sceneContainerFlags,
+ kosmos::sceneInteractor,
)
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt
index 13934da..8cb064d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt
@@ -158,6 +158,7 @@
powerInteractor,
GlanceableHubTransitions(
testScope,
+ testDispatcher,
keyguardTransitionInteractor,
keyguardTransitionRepository,
communalInteractor
@@ -180,6 +181,7 @@
testDispatcher,
testDispatcher,
keyguardInteractor,
+ communalInteractor,
featureFlags,
mock(),
mock(),
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/ConversationCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/ConversationCoordinatorTest.kt
index 65697b73..36f643a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/ConversationCoordinatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/ConversationCoordinatorTest.kt
@@ -24,7 +24,6 @@
import android.testing.TestableLooper
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
-import com.android.systemui.flags.FakeFeatureFlagsClassic
import com.android.systemui.statusbar.notification.collection.GroupEntry
import com.android.systemui.statusbar.notification.collection.GroupEntryBuilder
import com.android.systemui.statusbar.notification.collection.NotifPipeline
@@ -53,8 +52,8 @@
import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.Mockito.verify
-import org.mockito.MockitoAnnotations
import org.mockito.Mockito.`when` as whenever
+import org.mockito.MockitoAnnotations
@SmallTest
@RunWith(AndroidTestingRunner::class)
@@ -78,28 +77,22 @@
private lateinit var coordinator: ConversationCoordinator
- private val featureFlags = FakeFeatureFlagsClassic()
-
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
- coordinator = ConversationCoordinator(
- peopleNotificationIdentifier,
- conversationIconManager,
- HighPriorityProvider(
+ coordinator =
+ ConversationCoordinator(
peopleNotificationIdentifier,
- GroupMembershipManagerImpl(featureFlags)
- ),
- headerController
- )
+ conversationIconManager,
+ HighPriorityProvider(peopleNotificationIdentifier, GroupMembershipManagerImpl()),
+ headerController
+ )
whenever(channel.isImportantConversation).thenReturn(true)
coordinator.attach(pipeline)
// capture arguments:
- promoter = withArgCaptor {
- verify(pipeline).addPromoter(capture())
- }
+ promoter = withArgCaptor { verify(pipeline).addPromoter(capture()) }
beforeRenderListListener = withArgCaptor {
verify(pipeline).addOnBeforeRenderListListener(capture())
}
@@ -111,10 +104,10 @@
entry = NotificationEntryBuilder().setChannel(channel).build()
val section = NotifSection(peopleAlertingSectioner, 0)
- entryA = NotificationEntryBuilder().setChannel(channel)
- .setSection(section).setTag("A").build()
- entryB = NotificationEntryBuilder().setChannel(channel)
- .setSection(section).setTag("B").build()
+ entryA =
+ NotificationEntryBuilder().setChannel(channel).setSection(section).setTag("A").build()
+ entryB =
+ NotificationEntryBuilder().setChannel(channel).setSection(section).setTag("B").build()
}
@Test
@@ -129,11 +122,12 @@
val altChildA = NotificationEntryBuilder().setTag("A").build()
val altChildB = NotificationEntryBuilder().setTag("B").build()
val summary = NotificationEntryBuilder().setId(2).setChannel(channel).build()
- val groupEntry = GroupEntryBuilder()
- .setParent(GroupEntry.ROOT_ENTRY)
- .setSummary(summary)
- .setChildren(listOf(entry, altChildA, altChildB))
- .build()
+ val groupEntry =
+ GroupEntryBuilder()
+ .setParent(GroupEntry.ROOT_ENTRY)
+ .setSummary(summary)
+ .setChildren(listOf(entry, altChildA, altChildB))
+ .build()
assertTrue(promoter.shouldPromoteToTopLevel(entry))
assertFalse(promoter.shouldPromoteToTopLevel(altChildA))
assertFalse(promoter.shouldPromoteToTopLevel(altChildB))
@@ -146,41 +140,42 @@
@Test
fun testInAlertingPeopleSectionWhenTheImportanceIsAtLeastDefault() {
// GIVEN
- val alertingEntry = NotificationEntryBuilder().setChannel(channel)
- .setImportance(IMPORTANCE_DEFAULT).build()
+ val alertingEntry =
+ NotificationEntryBuilder().setChannel(channel).setImportance(IMPORTANCE_DEFAULT).build()
whenever(peopleNotificationIdentifier.getPeopleNotificationType(alertingEntry))
- .thenReturn(TYPE_PERSON)
+ .thenReturn(TYPE_PERSON)
// put alerting people notifications in this section
assertThat(peopleAlertingSectioner.isInSection(alertingEntry)).isTrue()
- }
+ }
@Test
fun testInSilentPeopleSectionWhenTheImportanceIsLowerThanDefault() {
// GIVEN
- val silentEntry = NotificationEntryBuilder().setChannel(channel)
- .setImportance(IMPORTANCE_LOW).build()
+ val silentEntry =
+ NotificationEntryBuilder().setChannel(channel).setImportance(IMPORTANCE_LOW).build()
whenever(peopleNotificationIdentifier.getPeopleNotificationType(silentEntry))
- .thenReturn(TYPE_PERSON)
+ .thenReturn(TYPE_PERSON)
// THEN put silent people notifications in this section
assertThat(peopleSilentSectioner.isInSection(silentEntry)).isTrue()
// People Alerting sectioning happens before the silent one.
- // It claims high important conversations and rest of conversations will be considered as silent.
+ // It claims high important conversations and rest of conversations will be considered as
+ // silent.
assertThat(peopleAlertingSectioner.isInSection(silentEntry)).isFalse()
}
@Test
fun testNotInPeopleSection() {
// GIVEN
- val entry = NotificationEntryBuilder().setChannel(channel)
- .setImportance(IMPORTANCE_LOW).build()
- val importantEntry = NotificationEntryBuilder().setChannel(channel)
- .setImportance(IMPORTANCE_HIGH).build()
+ val entry =
+ NotificationEntryBuilder().setChannel(channel).setImportance(IMPORTANCE_LOW).build()
+ val importantEntry =
+ NotificationEntryBuilder().setChannel(channel).setImportance(IMPORTANCE_HIGH).build()
whenever(peopleNotificationIdentifier.getPeopleNotificationType(entry))
- .thenReturn(TYPE_NON_PERSON)
+ .thenReturn(TYPE_NON_PERSON)
whenever(peopleNotificationIdentifier.getPeopleNotificationType(importantEntry))
- .thenReturn(TYPE_NON_PERSON)
+ .thenReturn(TYPE_NON_PERSON)
// THEN - only put people notification either silent or alerting
assertThat(peopleSilentSectioner.isInSection(entry)).isFalse()
@@ -190,19 +185,23 @@
@Test
fun testInAlertingPeopleSectionWhenThereIsAnImportantChild() {
// GIVEN
- val altChildA = NotificationEntryBuilder().setTag("A")
- .setImportance(IMPORTANCE_DEFAULT).build()
- val altChildB = NotificationEntryBuilder().setTag("B")
- .setImportance(IMPORTANCE_LOW).build()
- val summary = NotificationEntryBuilder().setId(2)
- .setImportance(IMPORTANCE_LOW).setChannel(channel).build()
- val groupEntry = GroupEntryBuilder()
+ val altChildA =
+ NotificationEntryBuilder().setTag("A").setImportance(IMPORTANCE_DEFAULT).build()
+ val altChildB = NotificationEntryBuilder().setTag("B").setImportance(IMPORTANCE_LOW).build()
+ val summary =
+ NotificationEntryBuilder()
+ .setId(2)
+ .setImportance(IMPORTANCE_LOW)
+ .setChannel(channel)
+ .build()
+ val groupEntry =
+ GroupEntryBuilder()
.setParent(GroupEntry.ROOT_ENTRY)
.setSummary(summary)
.setChildren(listOf(altChildA, altChildB))
.build()
whenever(peopleNotificationIdentifier.getPeopleNotificationType(summary))
- .thenReturn(TYPE_PERSON)
+ .thenReturn(TYPE_PERSON)
// THEN
assertThat(peopleAlertingSectioner.isInSection(groupEntry)).isTrue()
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorTest.java
index 58eec2e..4519ba6 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorTest.java
@@ -65,6 +65,7 @@
import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifSectioner;
import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener;
import com.android.systemui.statusbar.notification.collection.provider.SectionStyleProvider;
+import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManager;
import com.android.systemui.statusbar.notification.collection.render.NotifViewBarn;
import com.android.systemui.statusbar.notification.row.NotifInflationErrorManager;
import com.android.systemui.util.settings.SecureSettings;
@@ -111,6 +112,7 @@
@Spy private FakeNotifInflater mNotifInflater = new FakeNotifInflater();
private final SectionStyleProvider mSectionStyleProvider = new SectionStyleProvider();
@Mock private UserTracker mUserTracker;
+ @Mock private GroupMembershipManager mGroupMembershipManager;
private NotifUiAdjustmentProvider mAdjustmentProvider;
@@ -127,7 +129,9 @@
mSecureSettings,
mLockscreenUserManager,
mSectionStyleProvider,
- mUserTracker);
+ mUserTracker,
+ mGroupMembershipManager
+ );
mEntry = getNotificationEntryBuilder().setParent(ROOT_ENTRY).build();
mInflationError = new Exception(TEST_MESSAGE);
mErrorManager = new NotifInflationErrorManager();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/SensitiveContentCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/SensitiveContentCoordinatorTest.kt
index df547ae..350ed2d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/SensitiveContentCoordinatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/SensitiveContentCoordinatorTest.kt
@@ -17,9 +17,11 @@
package com.android.systemui.statusbar.notification.collection.coordinator
import android.os.UserHandle
+import android.platform.test.annotations.EnableFlags
import android.service.notification.StatusBarNotification
import androidx.test.filters.SmallTest
import com.android.keyguard.KeyguardUpdateMonitor
+import com.android.systemui.Flags.FLAG_SCREENSHARE_NOTIFICATION_HIDING
import com.android.systemui.SysuiTestCase
import com.android.systemui.plugins.statusbar.StatusBarStateController
import com.android.systemui.statusbar.NotificationLockscreenUserManager
@@ -33,6 +35,7 @@
import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.Invalidator
import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.Pluggable
import com.android.systemui.statusbar.policy.KeyguardStateController
+import com.android.systemui.statusbar.policy.SensitiveNotificationProtectionController
import com.android.systemui.user.domain.interactor.SelectedUserInteractor
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.eq
@@ -55,28 +58,31 @@
val statusBarStateController: StatusBarStateController = mock()
val keyguardStateController: KeyguardStateController = mock()
val mSelectedUserInteractor: SelectedUserInteractor = mock()
+ val sensitiveNotificationProtectionController: SensitiveNotificationProtectionController =
+ mock()
val coordinator: SensitiveContentCoordinator =
- DaggerTestSensitiveContentCoordinatorComponent
- .factory()
- .create(
- dynamicPrivacyController,
- lockscreenUserManager,
- keyguardUpdateMonitor,
- statusBarStateController,
- keyguardStateController,
- mSelectedUserInteractor)
- .coordinator
+ DaggerTestSensitiveContentCoordinatorComponent.factory()
+ .create(
+ dynamicPrivacyController,
+ lockscreenUserManager,
+ keyguardUpdateMonitor,
+ statusBarStateController,
+ keyguardStateController,
+ mSelectedUserInteractor,
+ sensitiveNotificationProtectionController
+ )
+ .coordinator
@Test
fun onDynamicPrivacyChanged_invokeInvalidationListener() {
coordinator.attach(pipeline)
- val invalidator = withArgCaptor<Invalidator> {
- verify(pipeline).addPreRenderInvalidator(capture())
- }
- val dynamicPrivacyListener = withArgCaptor<DynamicPrivacyController.Listener> {
- verify(dynamicPrivacyController).addListener(capture())
- }
+ val invalidator =
+ withArgCaptor<Invalidator> { verify(pipeline).addPreRenderInvalidator(capture()) }
+ val dynamicPrivacyListener =
+ withArgCaptor<DynamicPrivacyController.Listener> {
+ verify(dynamicPrivacyController).addListener(capture())
+ }
val invalidationListener = mock<Pluggable.PluggableListener<Invalidator>>()
invalidator.setInvalidationListener(invalidationListener)
@@ -89,9 +95,10 @@
@Test
fun onBeforeRenderList_deviceUnlocked_notifDoesNotNeedRedaction() {
coordinator.attach(pipeline)
- val onBeforeRenderListListener = withArgCaptor<OnBeforeRenderListListener> {
- verify(pipeline).addOnBeforeRenderListListener(capture())
- }
+ val onBeforeRenderListListener =
+ withArgCaptor<OnBeforeRenderListListener> {
+ verify(pipeline).addOnBeforeRenderListListener(capture())
+ }
whenever(lockscreenUserManager.currentUserId).thenReturn(1)
whenever(lockscreenUserManager.isLockscreenPublicMode(1)).thenReturn(false)
@@ -105,11 +112,59 @@
}
@Test
+ @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING)
+ fun onBeforeRenderList_deviceUnlocked_notifDoesNotNeedRedaction_sensitiveActive() {
+ coordinator.attach(pipeline)
+ val onBeforeRenderListListener =
+ withArgCaptor<OnBeforeRenderListListener> {
+ verify(pipeline).addOnBeforeRenderListListener(capture())
+ }
+
+ whenever(lockscreenUserManager.currentUserId).thenReturn(1)
+ whenever(lockscreenUserManager.isLockscreenPublicMode(1)).thenReturn(false)
+ whenever(lockscreenUserManager.userAllowsPrivateNotificationsInPublic(1)).thenReturn(true)
+ whenever(dynamicPrivacyController.isDynamicallyUnlocked).thenReturn(false)
+ val entry = fakeNotification(1, false)
+ whenever(sensitiveNotificationProtectionController.isSensitiveStateActive).thenReturn(true)
+
+ onBeforeRenderListListener.onBeforeRenderList(listOf(entry))
+
+ verify(entry.representativeEntry!!).setSensitive(false, true)
+ }
+
+ @Test
+ @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING)
+ fun onBeforeRenderList_deviceUnlocked_notifDoesNotNeedRedaction_shouldProtectNotification() {
+ coordinator.attach(pipeline)
+ val onBeforeRenderListListener =
+ withArgCaptor<OnBeforeRenderListListener> {
+ verify(pipeline).addOnBeforeRenderListListener(capture())
+ }
+
+ whenever(lockscreenUserManager.currentUserId).thenReturn(1)
+ whenever(lockscreenUserManager.isLockscreenPublicMode(1)).thenReturn(false)
+ whenever(lockscreenUserManager.userAllowsPrivateNotificationsInPublic(1)).thenReturn(true)
+ whenever(dynamicPrivacyController.isDynamicallyUnlocked).thenReturn(false)
+ val entry = fakeNotification(1, false)
+ whenever(
+ sensitiveNotificationProtectionController.shouldProtectNotification(
+ entry.getRepresentativeEntry()
+ )
+ )
+ .thenReturn(true)
+
+ onBeforeRenderListListener.onBeforeRenderList(listOf(entry))
+
+ verify(entry.representativeEntry!!).setSensitive(true, false)
+ }
+
+ @Test
fun onBeforeRenderList_deviceUnlocked_notifWouldNeedRedaction() {
coordinator.attach(pipeline)
- val onBeforeRenderListListener = withArgCaptor<OnBeforeRenderListListener> {
- verify(pipeline).addOnBeforeRenderListListener(capture())
- }
+ val onBeforeRenderListListener =
+ withArgCaptor<OnBeforeRenderListListener> {
+ verify(pipeline).addOnBeforeRenderListListener(capture())
+ }
whenever(lockscreenUserManager.currentUserId).thenReturn(1)
whenever(lockscreenUserManager.isLockscreenPublicMode(1)).thenReturn(false)
@@ -123,11 +178,59 @@
}
@Test
+ @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING)
+ fun onBeforeRenderList_deviceUnlocked_notifWouldNeedRedaction_sensitiveActive() {
+ coordinator.attach(pipeline)
+ val onBeforeRenderListListener =
+ withArgCaptor<OnBeforeRenderListListener> {
+ verify(pipeline).addOnBeforeRenderListListener(capture())
+ }
+
+ whenever(lockscreenUserManager.currentUserId).thenReturn(1)
+ whenever(lockscreenUserManager.isLockscreenPublicMode(1)).thenReturn(false)
+ whenever(lockscreenUserManager.userAllowsPrivateNotificationsInPublic(1)).thenReturn(true)
+ whenever(dynamicPrivacyController.isDynamicallyUnlocked).thenReturn(false)
+ val entry = fakeNotification(1, true)
+ whenever(sensitiveNotificationProtectionController.isSensitiveStateActive).thenReturn(true)
+
+ onBeforeRenderListListener.onBeforeRenderList(listOf(entry))
+
+ verify(entry.representativeEntry!!).setSensitive(false, true)
+ }
+
+ @Test
+ @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING)
+ fun onBeforeRenderList_deviceUnlocked_notifWouldNeedRedaction_shouldProtectNotification() {
+ coordinator.attach(pipeline)
+ val onBeforeRenderListListener =
+ withArgCaptor<OnBeforeRenderListListener> {
+ verify(pipeline).addOnBeforeRenderListListener(capture())
+ }
+
+ whenever(lockscreenUserManager.currentUserId).thenReturn(1)
+ whenever(lockscreenUserManager.isLockscreenPublicMode(1)).thenReturn(false)
+ whenever(lockscreenUserManager.userAllowsPrivateNotificationsInPublic(1)).thenReturn(true)
+ whenever(dynamicPrivacyController.isDynamicallyUnlocked).thenReturn(false)
+ val entry = fakeNotification(1, true)
+ whenever(
+ sensitiveNotificationProtectionController.shouldProtectNotification(
+ entry.getRepresentativeEntry()
+ )
+ )
+ .thenReturn(true)
+
+ onBeforeRenderListListener.onBeforeRenderList(listOf(entry))
+
+ verify(entry.representativeEntry!!).setSensitive(true, false)
+ }
+
+ @Test
fun onBeforeRenderList_deviceLocked_userAllowsPublicNotifs() {
coordinator.attach(pipeline)
- val onBeforeRenderListListener = withArgCaptor<OnBeforeRenderListListener> {
- verify(pipeline).addOnBeforeRenderListListener(capture())
- }
+ val onBeforeRenderListListener =
+ withArgCaptor<OnBeforeRenderListListener> {
+ verify(pipeline).addOnBeforeRenderListListener(capture())
+ }
whenever(lockscreenUserManager.currentUserId).thenReturn(1)
whenever(lockscreenUserManager.isLockscreenPublicMode(1)).thenReturn(true)
@@ -141,11 +244,59 @@
}
@Test
+ @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING)
+ fun onBeforeRenderList_deviceLocked_userAllowsPublicNotifs_sensitiveActive() {
+ coordinator.attach(pipeline)
+ val onBeforeRenderListListener =
+ withArgCaptor<OnBeforeRenderListListener> {
+ verify(pipeline).addOnBeforeRenderListListener(capture())
+ }
+
+ whenever(lockscreenUserManager.currentUserId).thenReturn(1)
+ whenever(lockscreenUserManager.isLockscreenPublicMode(1)).thenReturn(true)
+ whenever(lockscreenUserManager.userAllowsPrivateNotificationsInPublic(1)).thenReturn(true)
+ whenever(dynamicPrivacyController.isDynamicallyUnlocked).thenReturn(false)
+ val entry = fakeNotification(1, false)
+ whenever(sensitiveNotificationProtectionController.isSensitiveStateActive).thenReturn(true)
+
+ onBeforeRenderListListener.onBeforeRenderList(listOf(entry))
+
+ verify(entry.representativeEntry!!).setSensitive(false, true)
+ }
+
+ @Test
+ @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING)
+ fun onBeforeRenderList_deviceLocked_userAllowsPublicNotifs_shouldProtectNotification() {
+ coordinator.attach(pipeline)
+ val onBeforeRenderListListener =
+ withArgCaptor<OnBeforeRenderListListener> {
+ verify(pipeline).addOnBeforeRenderListListener(capture())
+ }
+
+ whenever(lockscreenUserManager.currentUserId).thenReturn(1)
+ whenever(lockscreenUserManager.isLockscreenPublicMode(1)).thenReturn(true)
+ whenever(lockscreenUserManager.userAllowsPrivateNotificationsInPublic(1)).thenReturn(true)
+ whenever(dynamicPrivacyController.isDynamicallyUnlocked).thenReturn(false)
+ val entry = fakeNotification(1, false)
+ whenever(
+ sensitiveNotificationProtectionController.shouldProtectNotification(
+ entry.getRepresentativeEntry()
+ )
+ )
+ .thenReturn(true)
+
+ onBeforeRenderListListener.onBeforeRenderList(listOf(entry))
+
+ verify(entry.representativeEntry!!).setSensitive(true, false)
+ }
+
+ @Test
fun onBeforeRenderList_deviceLocked_userDisallowsPublicNotifs_notifDoesNotNeedRedaction() {
coordinator.attach(pipeline)
- val onBeforeRenderListListener = withArgCaptor<OnBeforeRenderListListener> {
- verify(pipeline).addOnBeforeRenderListListener(capture())
- }
+ val onBeforeRenderListListener =
+ withArgCaptor<OnBeforeRenderListListener> {
+ verify(pipeline).addOnBeforeRenderListListener(capture())
+ }
whenever(lockscreenUserManager.currentUserId).thenReturn(1)
whenever(lockscreenUserManager.isLockscreenPublicMode(1)).thenReturn(true)
@@ -159,11 +310,61 @@
}
@Test
+ @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING)
+ @Suppress("ktlint:standard:max-line-length")
+ fun onBeforeRenderList_deviceLocked_userDisallowsPublicNotifs_notifDoesNotNeedRedaction_sensitiveActive() {
+ coordinator.attach(pipeline)
+ val onBeforeRenderListListener =
+ withArgCaptor<OnBeforeRenderListListener> {
+ verify(pipeline).addOnBeforeRenderListListener(capture())
+ }
+
+ whenever(lockscreenUserManager.currentUserId).thenReturn(1)
+ whenever(lockscreenUserManager.isLockscreenPublicMode(1)).thenReturn(true)
+ whenever(lockscreenUserManager.userAllowsPrivateNotificationsInPublic(1)).thenReturn(false)
+ whenever(dynamicPrivacyController.isDynamicallyUnlocked).thenReturn(false)
+ val entry = fakeNotification(1, false)
+ whenever(sensitiveNotificationProtectionController.isSensitiveStateActive).thenReturn(true)
+
+ onBeforeRenderListListener.onBeforeRenderList(listOf(entry))
+
+ verify(entry.representativeEntry!!).setSensitive(false, true)
+ }
+
+ @Test
+ @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING)
+ @Suppress("ktlint:standard:max-line-length")
+ fun onBeforeRenderList_deviceLocked_userDisallowsPublicNotifs_notifDoesNotNeedRedaction_shouldProtectNotification() {
+ coordinator.attach(pipeline)
+ val onBeforeRenderListListener =
+ withArgCaptor<OnBeforeRenderListListener> {
+ verify(pipeline).addOnBeforeRenderListListener(capture())
+ }
+
+ whenever(lockscreenUserManager.currentUserId).thenReturn(1)
+ whenever(lockscreenUserManager.isLockscreenPublicMode(1)).thenReturn(true)
+ whenever(lockscreenUserManager.userAllowsPrivateNotificationsInPublic(1)).thenReturn(false)
+ whenever(dynamicPrivacyController.isDynamicallyUnlocked).thenReturn(false)
+ val entry = fakeNotification(1, false)
+ whenever(
+ sensitiveNotificationProtectionController.shouldProtectNotification(
+ entry.getRepresentativeEntry()
+ )
+ )
+ .thenReturn(true)
+
+ onBeforeRenderListListener.onBeforeRenderList(listOf(entry))
+
+ verify(entry.representativeEntry!!).setSensitive(true, true)
+ }
+
+ @Test
fun onBeforeRenderList_deviceLocked_notifNeedsRedaction() {
coordinator.attach(pipeline)
- val onBeforeRenderListListener = withArgCaptor<OnBeforeRenderListListener> {
- verify(pipeline).addOnBeforeRenderListListener(capture())
- }
+ val onBeforeRenderListListener =
+ withArgCaptor<OnBeforeRenderListListener> {
+ verify(pipeline).addOnBeforeRenderListListener(capture())
+ }
whenever(lockscreenUserManager.currentUserId).thenReturn(1)
whenever(lockscreenUserManager.isLockscreenPublicMode(1)).thenReturn(true)
@@ -177,11 +378,59 @@
}
@Test
+ @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING)
+ fun onBeforeRenderList_deviceLocked_notifNeedsRedaction_sensitiveActive() {
+ coordinator.attach(pipeline)
+ val onBeforeRenderListListener =
+ withArgCaptor<OnBeforeRenderListListener> {
+ verify(pipeline).addOnBeforeRenderListListener(capture())
+ }
+
+ whenever(lockscreenUserManager.currentUserId).thenReturn(1)
+ whenever(lockscreenUserManager.isLockscreenPublicMode(1)).thenReturn(true)
+ whenever(lockscreenUserManager.userAllowsPrivateNotificationsInPublic(1)).thenReturn(false)
+ whenever(dynamicPrivacyController.isDynamicallyUnlocked).thenReturn(false)
+ val entry = fakeNotification(1, true)
+ whenever(sensitiveNotificationProtectionController.isSensitiveStateActive).thenReturn(true)
+
+ onBeforeRenderListListener.onBeforeRenderList(listOf(entry))
+
+ verify(entry.representativeEntry!!).setSensitive(true, true)
+ }
+
+ @Test
+ @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING)
+ fun onBeforeRenderList_deviceLocked_notifNeedsRedaction_shouldProtectNotification() {
+ coordinator.attach(pipeline)
+ val onBeforeRenderListListener =
+ withArgCaptor<OnBeforeRenderListListener> {
+ verify(pipeline).addOnBeforeRenderListListener(capture())
+ }
+
+ whenever(lockscreenUserManager.currentUserId).thenReturn(1)
+ whenever(lockscreenUserManager.isLockscreenPublicMode(1)).thenReturn(true)
+ whenever(lockscreenUserManager.userAllowsPrivateNotificationsInPublic(1)).thenReturn(false)
+ whenever(dynamicPrivacyController.isDynamicallyUnlocked).thenReturn(false)
+ val entry = fakeNotification(1, true)
+ whenever(
+ sensitiveNotificationProtectionController.shouldProtectNotification(
+ entry.getRepresentativeEntry()
+ )
+ )
+ .thenReturn(true)
+
+ onBeforeRenderListListener.onBeforeRenderList(listOf(entry))
+
+ verify(entry.representativeEntry!!).setSensitive(true, true)
+ }
+
+ @Test
fun onBeforeRenderList_deviceDynamicallyUnlocked_notifNeedsRedaction() {
coordinator.attach(pipeline)
- val onBeforeRenderListListener = withArgCaptor<OnBeforeRenderListListener> {
- verify(pipeline).addOnBeforeRenderListListener(capture())
- }
+ val onBeforeRenderListListener =
+ withArgCaptor<OnBeforeRenderListListener> {
+ verify(pipeline).addOnBeforeRenderListListener(capture())
+ }
whenever(lockscreenUserManager.currentUserId).thenReturn(1)
whenever(lockscreenUserManager.isLockscreenPublicMode(1)).thenReturn(true)
@@ -195,18 +444,66 @@
}
@Test
+ @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING)
+ fun onBeforeRenderList_deviceDynamicallyUnlocked_notifNeedsRedaction_sensitiveActive() {
+ coordinator.attach(pipeline)
+ val onBeforeRenderListListener =
+ withArgCaptor<OnBeforeRenderListListener> {
+ verify(pipeline).addOnBeforeRenderListListener(capture())
+ }
+
+ whenever(lockscreenUserManager.currentUserId).thenReturn(1)
+ whenever(lockscreenUserManager.isLockscreenPublicMode(1)).thenReturn(true)
+ whenever(lockscreenUserManager.userAllowsPrivateNotificationsInPublic(1)).thenReturn(false)
+ whenever(dynamicPrivacyController.isDynamicallyUnlocked).thenReturn(true)
+ val entry = fakeNotification(1, true)
+ whenever(sensitiveNotificationProtectionController.isSensitiveStateActive).thenReturn(true)
+
+ onBeforeRenderListListener.onBeforeRenderList(listOf(entry))
+
+ verify(entry.representativeEntry!!).setSensitive(false, true)
+ }
+
+ @Test
+ @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING)
+ @Suppress("ktlint:standard:max-line-length")
+ fun onBeforeRenderList_deviceDynamicallyUnlocked_notifNeedsRedaction_shouldProtectNotification() {
+ coordinator.attach(pipeline)
+ val onBeforeRenderListListener =
+ withArgCaptor<OnBeforeRenderListListener> {
+ verify(pipeline).addOnBeforeRenderListListener(capture())
+ }
+
+ whenever(lockscreenUserManager.currentUserId).thenReturn(1)
+ whenever(lockscreenUserManager.isLockscreenPublicMode(1)).thenReturn(true)
+ whenever(lockscreenUserManager.userAllowsPrivateNotificationsInPublic(1)).thenReturn(false)
+ whenever(dynamicPrivacyController.isDynamicallyUnlocked).thenReturn(true)
+ val entry = fakeNotification(1, true)
+ whenever(
+ sensitiveNotificationProtectionController.shouldProtectNotification(
+ entry.getRepresentativeEntry()
+ )
+ )
+ .thenReturn(true)
+
+ onBeforeRenderListListener.onBeforeRenderList(listOf(entry))
+
+ verify(entry.representativeEntry!!).setSensitive(true, true)
+ }
+
+ @Test
fun onBeforeRenderList_deviceDynamicallyUnlocked_notifUserNeedsWorkChallenge() {
coordinator.attach(pipeline)
- val onBeforeRenderListListener = withArgCaptor<OnBeforeRenderListListener> {
- verify(pipeline).addOnBeforeRenderListListener(capture())
- }
+ val onBeforeRenderListListener =
+ withArgCaptor<OnBeforeRenderListListener> {
+ verify(pipeline).addOnBeforeRenderListListener(capture())
+ }
whenever(lockscreenUserManager.currentUserId).thenReturn(1)
whenever(lockscreenUserManager.isLockscreenPublicMode(1)).thenReturn(true)
whenever(lockscreenUserManager.userAllowsPrivateNotificationsInPublic(1)).thenReturn(false)
whenever(dynamicPrivacyController.isDynamicallyUnlocked).thenReturn(true)
whenever(lockscreenUserManager.needsSeparateWorkChallenge(2)).thenReturn(true)
-
val entry = fakeNotification(2, true)
onBeforeRenderListListener.onBeforeRenderList(listOf(entry))
@@ -215,11 +512,62 @@
}
@Test
+ @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING)
+ fun onBeforeRenderList_deviceDynamicallyUnlocked_notifUserNeedsWorkChallenge_sensitiveActive() {
+ coordinator.attach(pipeline)
+ val onBeforeRenderListListener =
+ withArgCaptor<OnBeforeRenderListListener> {
+ verify(pipeline).addOnBeforeRenderListListener(capture())
+ }
+
+ whenever(lockscreenUserManager.currentUserId).thenReturn(1)
+ whenever(lockscreenUserManager.isLockscreenPublicMode(1)).thenReturn(true)
+ whenever(lockscreenUserManager.userAllowsPrivateNotificationsInPublic(1)).thenReturn(false)
+ whenever(dynamicPrivacyController.isDynamicallyUnlocked).thenReturn(true)
+ whenever(lockscreenUserManager.needsSeparateWorkChallenge(2)).thenReturn(true)
+ val entry = fakeNotification(2, true)
+ whenever(sensitiveNotificationProtectionController.isSensitiveStateActive).thenReturn(true)
+
+ onBeforeRenderListListener.onBeforeRenderList(listOf(entry))
+
+ verify(entry.representativeEntry!!).setSensitive(true, true)
+ }
+
+ @Test
+ @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING)
+ @Suppress("ktlint:standard:max-line-length")
+ fun onBeforeRenderList_deviceDynamicallyUnlocked_notifUserNeedsWorkChallenge_shouldProtectNotification() {
+ coordinator.attach(pipeline)
+ val onBeforeRenderListListener =
+ withArgCaptor<OnBeforeRenderListListener> {
+ verify(pipeline).addOnBeforeRenderListListener(capture())
+ }
+
+ whenever(lockscreenUserManager.currentUserId).thenReturn(1)
+ whenever(lockscreenUserManager.isLockscreenPublicMode(1)).thenReturn(true)
+ whenever(lockscreenUserManager.userAllowsPrivateNotificationsInPublic(1)).thenReturn(false)
+ whenever(dynamicPrivacyController.isDynamicallyUnlocked).thenReturn(true)
+ whenever(lockscreenUserManager.needsSeparateWorkChallenge(2)).thenReturn(true)
+ val entry = fakeNotification(2, true)
+ whenever(
+ sensitiveNotificationProtectionController.shouldProtectNotification(
+ entry.getRepresentativeEntry()
+ )
+ )
+ .thenReturn(true)
+
+ onBeforeRenderListListener.onBeforeRenderList(listOf(entry))
+
+ verify(entry.representativeEntry!!).setSensitive(true, true)
+ }
+
+ @Test
fun onBeforeRenderList_deviceDynamicallyUnlocked_deviceBiometricBypassingLockScreen() {
coordinator.attach(pipeline)
- val onBeforeRenderListListener = withArgCaptor<OnBeforeRenderListListener> {
- verify(pipeline).addOnBeforeRenderListListener(capture())
- }
+ val onBeforeRenderListListener =
+ withArgCaptor<OnBeforeRenderListListener> {
+ verify(pipeline).addOnBeforeRenderListListener(capture())
+ }
whenever(lockscreenUserManager.currentUserId).thenReturn(1)
whenever(lockscreenUserManager.isLockscreenPublicMode(1)).thenReturn(true)
@@ -227,9 +575,11 @@
whenever(dynamicPrivacyController.isDynamicallyUnlocked).thenReturn(true)
whenever(statusBarStateController.getState()).thenReturn(StatusBarState.KEYGUARD)
whenever(keyguardUpdateMonitor.getUserUnlockedWithBiometricAndIsBypassing(any()))
- .thenReturn(true)
-
+ .thenReturn(true)
val entry = fakeNotification(2, true)
+ whenever(sensitiveNotificationProtectionController.isSensitiveStateActive).thenReturn(true)
+ whenever(sensitiveNotificationProtectionController.shouldProtectNotification(any()))
+ .thenReturn(true)
onBeforeRenderListListener.onBeforeRenderList(listOf(entry))
@@ -237,15 +587,11 @@
}
private fun fakeNotification(notifUserId: Int, needsRedaction: Boolean): ListEntry {
- val mockUserHandle = mock<UserHandle>().apply {
- whenever(identifier).thenReturn(notifUserId)
- }
- val mockSbn: StatusBarNotification = mock<StatusBarNotification>().apply {
- whenever(user).thenReturn(mockUserHandle)
- }
- val mockEntry = mock<NotificationEntry>().apply {
- whenever(sbn).thenReturn(mockSbn)
- }
+ val mockUserHandle =
+ mock<UserHandle>().apply { whenever(identifier).thenReturn(notifUserId) }
+ val mockSbn: StatusBarNotification =
+ mock<StatusBarNotification>().apply { whenever(user).thenReturn(mockUserHandle) }
+ val mockEntry = mock<NotificationEntry>().apply { whenever(sbn).thenReturn(mockSbn) }
whenever(lockscreenUserManager.needsRedaction(mockEntry)).thenReturn(needsRedaction)
whenever(mockEntry.rowExists()).thenReturn(true)
return object : ListEntry("key", 0) {
@@ -268,6 +614,8 @@
@BindsInstance statusBarStateController: StatusBarStateController,
@BindsInstance keyguardStateController: KeyguardStateController,
@BindsInstance selectedUserInteractor: SelectedUserInteractor,
+ @BindsInstance
+ sensitiveNotificationProtectionController: SensitiveNotificationProtectionController,
): TestSensitiveContentCoordinatorComponent
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/inflation/NotifUiAdjustmentProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/inflation/NotifUiAdjustmentProviderTest.kt
index f9f8d8a..73c49c0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/inflation/NotifUiAdjustmentProviderTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/inflation/NotifUiAdjustmentProviderTest.kt
@@ -17,6 +17,8 @@
import android.database.ContentObserver
import android.os.Handler
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
import android.provider.Settings.Secure.SHOW_NOTIFICATION_SNOOZE
import android.testing.AndroidTestingRunner
import android.testing.TestableLooper.RunWithLooper
@@ -28,6 +30,8 @@
import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder
import com.android.systemui.statusbar.notification.collection.listbuilder.NotifSection
import com.android.systemui.statusbar.notification.collection.provider.SectionStyleProvider
+import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManager
+import com.android.systemui.statusbar.notification.row.shared.AsyncHybridViewInflation
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.eq
import com.android.systemui.util.mockito.mock
@@ -35,6 +39,8 @@
import com.android.systemui.util.settings.FakeSettings
import com.android.systemui.util.settings.SecureSettings
import com.google.common.truth.Truth.assertThat
+import kotlin.test.assertFalse
+import kotlin.test.assertTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@@ -55,6 +61,7 @@
private val uri = FakeSettings().getUriFor(SHOW_NOTIFICATION_SNOOZE)
private val dirtyListener: Runnable = mock()
private val userTracker: UserTracker = mock()
+ private val groupMembershipManager: GroupMembershipManager = mock()
private val section = NotifSection(mock(), 0)
private val entry = NotificationEntryBuilder()
@@ -69,7 +76,8 @@
secureSettings,
lockscreenUserManager,
sectionStyleProvider,
- userTracker
+ userTracker,
+ groupMembershipManager,
)
@Before
@@ -127,4 +135,42 @@
assertThat(withSnoozing.isSnoozeEnabled).isTrue()
assertThat(withSnoozing).isNotEqualTo(original)
}
+
+ @Test
+ @EnableFlags(AsyncHybridViewInflation.FLAG_NAME)
+ fun changeIsChildInGroup_asyncHybirdFlagEnabled_needReInflation() {
+ // Given: an Entry that is not child in group
+ // AsyncHybridViewInflation flag is enabled
+ whenever(groupMembershipManager.isChildInGroup(entry)).thenReturn(false)
+ val oldAdjustment = adjustmentProvider.calculateAdjustment(entry)
+ assertThat(oldAdjustment.isChildInGroup).isFalse()
+
+ // When: the Entry becomes a group child
+ whenever(groupMembershipManager.isChildInGroup(entry)).thenReturn(true)
+ val newAdjustment = adjustmentProvider.calculateAdjustment(entry)
+ assertThat(newAdjustment.isChildInGroup).isTrue()
+ assertThat(newAdjustment).isNotEqualTo(oldAdjustment)
+
+ // Then: need re-inflation
+ assertTrue(NotifUiAdjustment.needReinflate(oldAdjustment, newAdjustment))
+ }
+
+ @Test
+ @DisableFlags(AsyncHybridViewInflation.FLAG_NAME)
+ fun changeIsChildInGroup_asyncHybirdFlagDisabled_noNeedForReInflation() {
+ // Given: an Entry that is not child in group
+ // AsyncHybridViewInflation flag is disabled
+ whenever(groupMembershipManager.isChildInGroup(entry)).thenReturn(false)
+ val oldAdjustment = adjustmentProvider.calculateAdjustment(entry)
+ assertThat(oldAdjustment.isChildInGroup).isFalse()
+
+ // When: the Entry becomes a group child
+ whenever(groupMembershipManager.isChildInGroup(entry)).thenReturn(true)
+ val newAdjustment = adjustmentProvider.calculateAdjustment(entry)
+ assertThat(newAdjustment.isChildInGroup).isTrue()
+ assertThat(newAdjustment).isNotEqualTo(oldAdjustment)
+
+ // Then: need no re-inflation
+ assertFalse(NotifUiAdjustment.needReinflate(oldAdjustment, newAdjustment))
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/render/GroupMembershipManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/render/GroupMembershipManagerTest.kt
deleted file mode 100644
index c1ffa64..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/render/GroupMembershipManagerTest.kt
+++ /dev/null
@@ -1,173 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.statusbar.notification.collection.render
-
-import androidx.test.filters.SmallTest
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.flags.FakeFeatureFlagsClassic
-import com.android.systemui.flags.Flags
-import com.android.systemui.statusbar.notification.collection.GroupEntry
-import com.android.systemui.statusbar.notification.collection.GroupEntryBuilder
-import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder
-import com.google.common.truth.Truth.assertThat
-import org.junit.Before
-import org.junit.Test
-
-@SmallTest
-class GroupMembershipManagerTest : SysuiTestCase() {
- private lateinit var gmm: GroupMembershipManagerImpl
-
- private val featureFlags = FakeFeatureFlagsClassic()
-
- @Before
- fun setUp() {
- gmm = GroupMembershipManagerImpl(featureFlags)
- }
-
- @Test
- fun testIsChildInGroup_topLevel() {
- featureFlags.set(Flags.NOTIFICATION_GROUP_EXPANSION_CHANGE, false)
- val topLevelEntry = NotificationEntryBuilder().setParent(GroupEntry.ROOT_ENTRY).build()
- assertThat(gmm.isChildInGroup(topLevelEntry)).isFalse()
- }
-
- @Test
- fun testIsChildInGroup_noParent_old() {
- featureFlags.set(Flags.NOTIFICATION_GROUP_EXPANSION_CHANGE, false)
- val noParentEntry = NotificationEntryBuilder().setParent(null).build()
- assertThat(gmm.isChildInGroup(noParentEntry)).isTrue()
- }
-
- @Test
- fun testIsChildInGroup_noParent_new() {
- featureFlags.set(Flags.NOTIFICATION_GROUP_EXPANSION_CHANGE, true)
- val noParentEntry = NotificationEntryBuilder().setParent(null).build()
- assertThat(gmm.isChildInGroup(noParentEntry)).isFalse()
- }
- @Test
- fun testIsChildInGroup_summary_old() {
- featureFlags.set(Flags.NOTIFICATION_GROUP_EXPANSION_CHANGE, false)
-
- val groupKey = "group"
- val summary =
- NotificationEntryBuilder()
- .setGroup(mContext, groupKey)
- .setGroupSummary(mContext, true)
- .build()
- GroupEntryBuilder().setKey(groupKey).setSummary(summary).build()
-
- assertThat(gmm.isChildInGroup(summary)).isTrue()
- }
-
- @Test
- fun testIsChildInGroup_summary_new() {
- featureFlags.set(Flags.NOTIFICATION_GROUP_EXPANSION_CHANGE, true)
-
- val groupKey = "group"
- val summary =
- NotificationEntryBuilder()
- .setGroup(mContext, groupKey)
- .setGroupSummary(mContext, true)
- .build()
- GroupEntryBuilder().setKey(groupKey).setSummary(summary).build()
-
- assertThat(gmm.isChildInGroup(summary)).isFalse()
- }
-
- @Test
- fun testIsChildInGroup_child() {
- featureFlags.set(Flags.NOTIFICATION_GROUP_EXPANSION_CHANGE, false)
- val childEntry = NotificationEntryBuilder().build()
- assertThat(gmm.isChildInGroup(childEntry)).isTrue()
- }
-
- @Test
- fun testIsGroupSummary_topLevelEntry() {
- featureFlags.set(Flags.NOTIFICATION_GROUP_EXPANSION_CHANGE, true)
- val entry = NotificationEntryBuilder().setParent(GroupEntry.ROOT_ENTRY).build()
- assertThat(gmm.isGroupSummary(entry)).isFalse()
- }
-
- @Test
- fun testIsGroupSummary_summary() {
- featureFlags.set(Flags.NOTIFICATION_GROUP_EXPANSION_CHANGE, true)
-
- val groupKey = "group"
- val summary =
- NotificationEntryBuilder()
- .setGroup(mContext, groupKey)
- .setGroupSummary(mContext, true)
- .build()
- GroupEntryBuilder().setKey(groupKey).setSummary(summary).build()
-
- assertThat(gmm.isGroupSummary(summary)).isTrue()
- }
-
- @Test
- fun testIsGroupSummary_child() {
- featureFlags.set(Flags.NOTIFICATION_GROUP_EXPANSION_CHANGE, true)
-
- val groupKey = "group"
- val summary =
- NotificationEntryBuilder()
- .setGroup(mContext, groupKey)
- .setGroupSummary(mContext, true)
- .build()
- val entry = NotificationEntryBuilder().setGroup(mContext, groupKey).build()
- GroupEntryBuilder().setKey(groupKey).setSummary(summary).addChild(entry).build()
-
- assertThat(gmm.isGroupSummary(entry)).isFalse()
- }
-
- @Test
- fun testGetGroupSummary_topLevelEntry() {
- featureFlags.set(Flags.NOTIFICATION_GROUP_EXPANSION_CHANGE, true)
- val entry = NotificationEntryBuilder().setParent(GroupEntry.ROOT_ENTRY).build()
- assertThat(gmm.getGroupSummary(entry)).isNull()
- }
-
- @Test
- fun testGetGroupSummary_summary() {
- featureFlags.set(Flags.NOTIFICATION_GROUP_EXPANSION_CHANGE, true)
-
- val groupKey = "group"
- val summary =
- NotificationEntryBuilder()
- .setGroup(mContext, groupKey)
- .setGroupSummary(mContext, true)
- .build()
- GroupEntryBuilder().setKey(groupKey).setSummary(summary).build()
-
- assertThat(gmm.getGroupSummary(summary)).isEqualTo(summary)
- }
-
- @Test
- fun testGetGroupSummary_child() {
- featureFlags.set(Flags.NOTIFICATION_GROUP_EXPANSION_CHANGE, true)
-
- val groupKey = "group"
- val summary =
- NotificationEntryBuilder()
- .setGroup(mContext, groupKey)
- .setGroupSummary(mContext, true)
- .build()
- val entry = NotificationEntryBuilder().setGroup(mContext, groupKey).build()
- GroupEntryBuilder().setKey(groupKey).setSummary(summary).addChild(entry).build()
-
- assertThat(gmm.getGroupSummary(entry)).isEqualTo(summary)
- }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationLoggerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationLoggerTest.java
index ff02ef3..b01281c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationLoggerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationLoggerTest.java
@@ -42,9 +42,9 @@
import com.android.internal.logging.InstanceId;
import com.android.internal.statusbar.IStatusBarService;
import com.android.internal.statusbar.NotificationVisibility;
-import com.android.keyguard.TestScopeProvider;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository;
+import com.android.systemui.kosmos.KosmosJavaAdapter;
import com.android.systemui.power.domain.interactor.PowerInteractor;
import com.android.systemui.power.domain.interactor.PowerInteractorFactory;
import com.android.systemui.scene.data.repository.WindowRootViewVisibilityRepository;
@@ -110,7 +110,8 @@
private FakeExecutor mUiBgExecutor = new FakeExecutor(new FakeSystemClock());
private NotificationPanelLoggerFake mNotificationPanelLoggerFake =
new NotificationPanelLoggerFake();
- private final TestScope mTestScope = TestScopeProvider.getTestScope();
+ private final KosmosJavaAdapter mKosmos = new KosmosJavaAdapter(this);
+ private final TestScope mTestScope = mKosmos.getTestScope();
private final FakeKeyguardRepository mKeyguardRepository = new FakeKeyguardRepository();
private final PowerInteractor mPowerInteractor =
PowerInteractorFactory.create().getPowerInteractor();
@@ -133,7 +134,9 @@
mKeyguardRepository,
mHeadsUpManager,
mPowerInteractor,
- mActiveNotificationsInteractor);
+ mActiveNotificationsInteractor,
+ mKosmos.getFakeSceneContainerFlags(),
+ () -> mKosmos.getSceneInteractor());
mWindowRootViewVisibilityInteractor.setIsLockscreenOrShadeVisible(true);
mEntry = new NotificationEntryBuilder()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotifLayoutInflaterFactoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotifLayoutInflaterFactoryTest.kt
index 3f7fc97..fd41921 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotifLayoutInflaterFactoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotifLayoutInflaterFactoryTest.kt
@@ -62,7 +62,7 @@
fun onCreateView_noMatchingViewForName_returnNull() {
// GIVEN we have ViewFactories that replaces TextViews in expanded and collapsed layouts
val layoutType = FLAG_CONTENT_VIEW_EXPANDED
- inflaterFactory = NotifLayoutInflaterFactory(row, layoutType, viewFactorySpies)
+ inflaterFactory = createNotifLayoutInflaterFactory(row, layoutType, viewFactorySpies)
// WHEN we try to inflate an ImageView for the expanded layout
val createdView = inflaterFactory.onCreateView("ImageView", context, attrs)
@@ -78,7 +78,7 @@
fun onCreateView_noMatchingViewForLayoutType_returnNull() {
// GIVEN we have ViewFactories that replaces TextViews in expanded and collapsed layouts
val layoutType = FLAG_CONTENT_VIEW_HEADS_UP
- inflaterFactory = NotifLayoutInflaterFactory(row, layoutType, viewFactorySpies)
+ inflaterFactory = createNotifLayoutInflaterFactory(row, layoutType, viewFactorySpies)
// WHEN we try to inflate a TextView for the heads-up layout
val createdView = inflaterFactory.onCreateView("TextView", context, attrs)
@@ -94,7 +94,7 @@
fun onCreateView_matchingViews_returnReplacementView() {
// GIVEN we have ViewFactories that replaces TextViews in expanded and collapsed layouts
val layoutType = FLAG_CONTENT_VIEW_EXPANDED
- inflaterFactory = NotifLayoutInflaterFactory(row, layoutType, viewFactorySpies)
+ inflaterFactory = createNotifLayoutInflaterFactory(row, layoutType, viewFactorySpies)
// WHEN we try to inflate a TextView for the expanded layout
val createdView = inflaterFactory.onCreateView("TextView", context, attrs)
@@ -110,7 +110,7 @@
// GIVEN we have two factories that replaces TextViews in expanded layouts
val layoutType = FLAG_CONTENT_VIEW_EXPANDED
inflaterFactory =
- NotifLayoutInflaterFactory(
+ createNotifLayoutInflaterFactory(
row,
layoutType,
setOf(
@@ -147,4 +147,18 @@
null
}
}
+
+ private fun createNotifLayoutInflaterFactory(
+ row: ExpandableNotificationRow,
+ layoutType: Int,
+ notifRemoteViewsFactoryContainer: Set<NotifRemoteViewsFactory>
+ ) =
+ NotifLayoutInflaterFactory(
+ row,
+ layoutType,
+ object : NotifRemoteViewsFactoryContainer {
+ override val factories: Set<NotifRemoteViewsFactory> =
+ notifRemoteViewsFactoryContainer
+ }
+ )
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterTest.java
index b0996ad..a0d1075 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterTest.java
@@ -88,6 +88,8 @@
private Notification.Builder mBuilder;
private ExpandableNotificationRow mRow;
+ private NotificationTestHelper mHelper;
+
@Mock private NotifRemoteViewCache mCache;
@Mock private ConversationNotificationProcessor mConversationNotificationProcessor;
@Mock private InflatedSmartReplyState mInflatedSmartReplyState;
@@ -119,11 +121,11 @@
.setContentTitle("Title")
.setContentText("Text")
.setStyle(new Notification.BigTextStyle().bigText("big text"));
- NotificationTestHelper helper = new NotificationTestHelper(
+ mHelper = new NotificationTestHelper(
mContext,
mDependency,
TestableLooper.get(this));
- ExpandableNotificationRow row = helper.createRow(mBuilder.build());
+ ExpandableNotificationRow row = mHelper.createRow(mBuilder.build());
mRow = spy(row);
when(mNotifLayoutInflaterFactoryProvider.provide(any(), any()))
.thenReturn(mNotifLayoutInflaterFactory);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerTest.java
index 71613ed..6549193 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerTest.java
@@ -69,9 +69,9 @@
import com.android.internal.logging.UiEventLogger;
import com.android.internal.logging.testing.UiEventLoggerFake;
import com.android.internal.statusbar.IStatusBarService;
-import com.android.keyguard.TestScopeProvider;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository;
+import com.android.systemui.kosmos.KosmosJavaAdapter;
import com.android.systemui.people.widget.PeopleSpaceWidgetManager;
import com.android.systemui.plugins.ActivityStarter;
import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin;
@@ -124,7 +124,8 @@
private NotificationChannel mTestNotificationChannel = new NotificationChannel(
TEST_CHANNEL_ID, TEST_CHANNEL_ID, IMPORTANCE_DEFAULT);
- private TestScope mTestScope = TestScopeProvider.getTestScope();
+ private KosmosJavaAdapter mKosmos = new KosmosJavaAdapter(this);
+ private TestScope mTestScope = mKosmos.getTestScope();
private JavaAdapter mJavaAdapter = new JavaAdapter(mTestScope.getBackgroundScope());
private FakeExecutor mExecutor = new FakeExecutor(new FakeSystemClock());
private TestableLooper mTestableLooper;
@@ -182,7 +183,10 @@
new FakeKeyguardRepository(),
mHeadsUpManager,
PowerInteractorFactory.create().getPowerInteractor(),
- mActiveNotificationsInteractor);
+ mActiveNotificationsInteractor,
+ mKosmos.getFakeSceneContainerFlags(),
+ () -> mKosmos.getSceneInteractor()
+ );
mGutsManager = new NotificationGutsManager(
mContext,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerWithScenesTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerWithScenesTest.kt
new file mode 100644
index 0000000..446b9d0
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerWithScenesTest.kt
@@ -0,0 +1,631 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.systemui.statusbar.notification.row
+
+import android.R
+import android.app.AppOpsManager
+import android.app.INotificationManager
+import android.app.Notification
+import android.app.NotificationChannel
+import android.app.NotificationManager
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.content.pm.ShortcutManager
+import android.content.pm.launcherApps
+import android.graphics.Color
+import android.os.Binder
+import android.os.Handler
+import android.os.userManager
+import android.provider.Settings
+import android.service.notification.NotificationListenerService.Ranking
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import android.testing.TestableLooper.RunWithLooper
+import android.util.ArraySet
+import android.view.View
+import android.view.accessibility.accessibilityManager
+import androidx.test.filters.SmallTest
+import com.android.internal.logging.MetricsLogger
+import com.android.internal.logging.UiEventLogger
+import com.android.internal.logging.metricsLogger
+import com.android.internal.logging.testing.UiEventLoggerFake
+import com.android.internal.statusbar.statusBarService
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.people.widget.PeopleSpaceWidgetManager
+import com.android.systemui.plugins.activityStarter
+import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin
+import com.android.systemui.plugins.statusbar.statusBarStateController
+import com.android.systemui.power.domain.interactor.PowerInteractorFactory.create
+import com.android.systemui.scene.data.repository.WindowRootViewVisibilityRepository
+import com.android.systemui.scene.domain.interactor.WindowRootViewVisibilityInteractor
+import com.android.systemui.scene.domain.interactor.sceneInteractor
+import com.android.systemui.scene.shared.flag.fakeSceneContainerFlags
+import com.android.systemui.scene.shared.model.ObservableTransitionState
+import com.android.systemui.scene.shared.model.SceneKey
+import com.android.systemui.scene.shared.model.SceneModel
+import com.android.systemui.settings.UserContextProvider
+import com.android.systemui.shade.shadeControllerSceneImpl
+import com.android.systemui.statusbar.NotificationEntryHelper
+import com.android.systemui.statusbar.NotificationPresenter
+import com.android.systemui.statusbar.notification.AssistantFeedbackController
+import com.android.systemui.statusbar.notification.NotificationActivityStarter
+import com.android.systemui.statusbar.notification.collection.provider.HighPriorityProvider
+import com.android.systemui.statusbar.notification.domain.interactor.activeNotificationsInteractor
+import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier
+import com.android.systemui.statusbar.notification.stack.NotificationListContainer
+import com.android.systemui.statusbar.notificationLockscreenUserManager
+import com.android.systemui.statusbar.policy.deviceProvisionedController
+import com.android.systemui.statusbar.policy.headsUpManager
+import com.android.systemui.testKosmos
+import com.android.systemui.util.concurrency.FakeExecutor
+import com.android.systemui.util.kotlin.JavaAdapter
+import com.android.systemui.util.time.FakeSystemClock
+import com.android.systemui.wmshell.BubblesManager
+import java.util.Optional
+import junit.framework.Assert
+import kotlin.test.assertEquals
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.test.runCurrent
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.ArgumentMatchers
+import org.mockito.Mock
+import org.mockito.Mockito
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+import org.mockito.invocation.InvocationOnMock
+
+/** Tests for [NotificationGutsManager] with the scene container enabled. */
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@RunWithLooper
+class NotificationGutsManagerWithScenesTest : SysuiTestCase() {
+ private val testNotificationChannel =
+ NotificationChannel(
+ TEST_CHANNEL_ID,
+ TEST_CHANNEL_ID,
+ NotificationManager.IMPORTANCE_DEFAULT
+ )
+
+ private val kosmos = testKosmos()
+ private val testScope = kosmos.testScope
+ private val javaAdapter = JavaAdapter(testScope.backgroundScope)
+ private val executor = FakeExecutor(FakeSystemClock())
+ private lateinit var testableLooper: TestableLooper
+ private lateinit var handler: Handler
+ private lateinit var helper: NotificationTestHelper
+ private lateinit var gutsManager: NotificationGutsManager
+ private lateinit var windowRootViewVisibilityInteractor: WindowRootViewVisibilityInteractor
+
+ private val metricsLogger = kosmos.metricsLogger
+ private val deviceProvisionedController = kosmos.deviceProvisionedController
+ private val accessibilityManager = kosmos.accessibilityManager
+ private val mBarService = kosmos.statusBarService
+ private val launcherApps = kosmos.launcherApps
+ private val shadeController = kosmos.shadeControllerSceneImpl
+ private val notificationLockscreenUserManager = kosmos.notificationLockscreenUserManager
+ private val statusBarStateController = kosmos.statusBarStateController
+ private val headsUpManager = kosmos.headsUpManager
+ private val activityStarter = kosmos.activityStarter
+ private val userManager = kosmos.userManager
+ private val activeNotificationsInteractor = kosmos.activeNotificationsInteractor
+ private val sceneInteractor = kosmos.sceneInteractor
+
+ @Mock private lateinit var onUserInteractionCallback: OnUserInteractionCallback
+ @Mock private lateinit var presenter: NotificationPresenter
+ @Mock private lateinit var notificationActivityStarter: NotificationActivityStarter
+ @Mock private lateinit var notificationListContainer: NotificationListContainer
+ @Mock
+ private lateinit var onSettingsClickListener: NotificationGutsManager.OnSettingsClickListener
+ @Mock private lateinit var highPriorityProvider: HighPriorityProvider
+ @Mock private lateinit var notificationManager: INotificationManager
+ @Mock private lateinit var shortcutManager: ShortcutManager
+ @Mock private lateinit var channelEditorDialogController: ChannelEditorDialogController
+ @Mock private lateinit var peopleNotificationIdentifier: PeopleNotificationIdentifier
+ @Mock private lateinit var contextTracker: UserContextProvider
+ @Mock private lateinit var bubblesManager: BubblesManager
+ @Mock private lateinit var peopleSpaceWidgetManager: PeopleSpaceWidgetManager
+ @Mock private lateinit var assistantFeedbackController: AssistantFeedbackController
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+ val sceneContainerFlags = kosmos.fakeSceneContainerFlags
+ sceneContainerFlags.enabled = true
+ testableLooper = TestableLooper.get(this)
+ allowTestableLooperAsMainThread()
+ handler = Handler.createAsync(testableLooper.getLooper())
+ helper = NotificationTestHelper(mContext, mDependency, TestableLooper.get(this))
+ Mockito.`when`(accessibilityManager.isTouchExplorationEnabled).thenReturn(false)
+ windowRootViewVisibilityInteractor =
+ WindowRootViewVisibilityInteractor(
+ testScope.backgroundScope,
+ WindowRootViewVisibilityRepository(mBarService, executor),
+ FakeKeyguardRepository(),
+ headsUpManager,
+ create().powerInteractor,
+ activeNotificationsInteractor,
+ sceneContainerFlags,
+ { sceneInteractor },
+ )
+ gutsManager =
+ NotificationGutsManager(
+ mContext,
+ handler,
+ handler,
+ javaAdapter,
+ accessibilityManager,
+ highPriorityProvider,
+ notificationManager,
+ userManager,
+ peopleSpaceWidgetManager,
+ launcherApps,
+ shortcutManager,
+ channelEditorDialogController,
+ contextTracker,
+ assistantFeedbackController,
+ Optional.of(bubblesManager),
+ UiEventLoggerFake(),
+ onUserInteractionCallback,
+ shadeController,
+ windowRootViewVisibilityInteractor,
+ notificationLockscreenUserManager,
+ statusBarStateController,
+ mBarService,
+ deviceProvisionedController,
+ metricsLogger,
+ headsUpManager,
+ activityStarter
+ )
+ gutsManager.setUpWithPresenter(
+ presenter,
+ notificationListContainer,
+ onSettingsClickListener
+ )
+ gutsManager.setNotificationActivityStarter(notificationActivityStarter)
+ gutsManager.start()
+ }
+
+ @Test
+ fun testOpenAndCloseGuts() {
+ val guts = Mockito.spy(NotificationGuts(mContext))
+ Mockito.`when`(guts.post(ArgumentMatchers.any())).thenAnswer { invocation: InvocationOnMock
+ ->
+ handler.post((invocation.arguments[0] as Runnable))
+ null
+ }
+
+ // Test doesn't support animation since the guts view is not attached.
+ Mockito.doNothing()
+ .`when`(guts)
+ .openControls(
+ ArgumentMatchers.anyInt(),
+ ArgumentMatchers.anyInt(),
+ ArgumentMatchers.anyBoolean(),
+ ArgumentMatchers.any(Runnable::class.java)
+ )
+ val realRow = createTestNotificationRow()
+ val menuItem = createTestMenuItem(realRow)
+ val row = Mockito.spy(realRow)
+ Mockito.`when`(row!!.windowToken).thenReturn(Binder())
+ Mockito.`when`(row.guts).thenReturn(guts)
+ Assert.assertTrue(gutsManager.openGutsInternal(row, 0, 0, menuItem))
+ assertEquals(View.INVISIBLE.toLong(), guts.visibility.toLong())
+ testableLooper.processAllMessages()
+ verify(guts)
+ .openControls(
+ ArgumentMatchers.anyInt(),
+ ArgumentMatchers.anyInt(),
+ ArgumentMatchers.anyBoolean(),
+ ArgumentMatchers.any(Runnable::class.java)
+ )
+ verify(headsUpManager).setGutsShown(realRow!!.entry, true)
+ assertEquals(View.VISIBLE.toLong(), guts.visibility.toLong())
+ gutsManager.closeAndSaveGuts(false, false, true, 0, 0, false)
+ verify(guts)
+ .closeControls(
+ ArgumentMatchers.anyBoolean(),
+ ArgumentMatchers.anyBoolean(),
+ ArgumentMatchers.anyInt(),
+ ArgumentMatchers.anyInt(),
+ ArgumentMatchers.anyBoolean()
+ )
+ verify(row, Mockito.times(1)).setGutsView(ArgumentMatchers.any())
+ testableLooper.processAllMessages()
+ verify(headsUpManager).setGutsShown(realRow.entry, false)
+ }
+
+ @Test
+ fun testLockscreenShadeVisible_visible_gutsNotClosed() {
+ // First, start out lockscreen or shade as not visible
+ setIsLockscreenOrShadeVisible(false)
+ testScope.testScheduler.runCurrent()
+ val guts = Mockito.mock(NotificationGuts::class.java)
+ gutsManager.exposedGuts = guts
+
+ // WHEN the lockscreen or shade becomes visible
+ setIsLockscreenOrShadeVisible(true)
+ testScope.testScheduler.runCurrent()
+
+ // THEN the guts are not closed
+ verify(guts, Mockito.never()).removeCallbacks(ArgumentMatchers.any())
+ verify(guts, Mockito.never())
+ .closeControls(
+ ArgumentMatchers.anyBoolean(),
+ ArgumentMatchers.anyBoolean(),
+ ArgumentMatchers.anyInt(),
+ ArgumentMatchers.anyInt(),
+ ArgumentMatchers.anyBoolean()
+ )
+ }
+
+ @Test
+ fun testLockscreenShadeVisible_notVisible_gutsClosed() {
+ // First, start out lockscreen or shade as visible
+ setIsLockscreenOrShadeVisible(true)
+ testScope.testScheduler.runCurrent()
+ val guts = Mockito.mock(NotificationGuts::class.java)
+ gutsManager.exposedGuts = guts
+
+ // WHEN the lockscreen or shade is no longer visible
+ setIsLockscreenOrShadeVisible(false)
+ testScope.testScheduler.runCurrent()
+
+ // THEN the guts are closed
+ verify(guts).removeCallbacks(ArgumentMatchers.any())
+ verify(guts)
+ .closeControls(
+ /* leavebehinds= */ ArgumentMatchers.eq(true),
+ /* controls= */ ArgumentMatchers.eq(true),
+ /* x= */ ArgumentMatchers.anyInt(),
+ /* y= */ ArgumentMatchers.anyInt(),
+ /* force= */ ArgumentMatchers.eq(true)
+ )
+ }
+
+ @Test
+ fun testLockscreenShadeVisible_notVisible_listContainerReset() {
+ // First, start out lockscreen or shade as visible
+ setIsLockscreenOrShadeVisible(true)
+ testScope.testScheduler.runCurrent()
+
+ // WHEN the lockscreen or shade is no longer visible
+ setIsLockscreenOrShadeVisible(false)
+ testScope.testScheduler.runCurrent()
+
+ // THEN the list container is reset
+ verify(notificationListContainer)
+ .resetExposedMenuView(ArgumentMatchers.anyBoolean(), ArgumentMatchers.anyBoolean())
+ }
+
+ @Test
+ fun testChangeDensityOrFontScale() {
+ val guts = Mockito.spy(NotificationGuts(mContext))
+ Mockito.`when`(guts.post(ArgumentMatchers.any())).thenAnswer { invocation: InvocationOnMock
+ ->
+ handler.post((invocation.arguments[0] as Runnable))
+ null
+ }
+
+ // Test doesn't support animation since the guts view is not attached.
+ Mockito.doNothing()
+ .`when`(guts)
+ .openControls(
+ ArgumentMatchers.anyInt(),
+ ArgumentMatchers.anyInt(),
+ ArgumentMatchers.anyBoolean(),
+ ArgumentMatchers.any(Runnable::class.java)
+ )
+ val realRow = createTestNotificationRow()
+ val menuItem = createTestMenuItem(realRow)
+ val row = Mockito.spy(realRow)
+ Mockito.`when`(row!!.windowToken).thenReturn(Binder())
+ Mockito.`when`(row.guts).thenReturn(guts)
+ Mockito.doNothing().`when`(row).ensureGutsInflated()
+ val realEntry = realRow!!.entry
+ val entry = Mockito.spy(realEntry)
+ Mockito.`when`(entry.row).thenReturn(row)
+ Mockito.`when`(entry.getGuts()).thenReturn(guts)
+ Assert.assertTrue(gutsManager.openGutsInternal(row, 0, 0, menuItem))
+ testableLooper.processAllMessages()
+ verify(guts)
+ .openControls(
+ ArgumentMatchers.anyInt(),
+ ArgumentMatchers.anyInt(),
+ ArgumentMatchers.anyBoolean(),
+ ArgumentMatchers.any(Runnable::class.java)
+ )
+
+ // called once by mGutsManager.bindGuts() in mGutsManager.openGuts()
+ verify(row).setGutsView(ArgumentMatchers.any())
+ row.onDensityOrFontScaleChanged()
+ gutsManager.onDensityOrFontScaleChanged(entry)
+ testableLooper.processAllMessages()
+ gutsManager.closeAndSaveGuts(false, false, false, 0, 0, false)
+ verify(guts)
+ .closeControls(
+ ArgumentMatchers.anyBoolean(),
+ ArgumentMatchers.anyBoolean(),
+ ArgumentMatchers.anyInt(),
+ ArgumentMatchers.anyInt(),
+ ArgumentMatchers.anyBoolean()
+ )
+
+ // called again by mGutsManager.bindGuts(), in mGutsManager.onDensityOrFontScaleChanged()
+ verify(row, Mockito.times(2)).setGutsView(ArgumentMatchers.any())
+ }
+
+ @Test
+ fun testAppOpsSettingsIntent_camera() {
+ val ops = ArraySet<Int>()
+ ops.add(AppOpsManager.OP_CAMERA)
+ gutsManager.startAppOpsSettingsActivity("", 0, ops, null)
+ val captor = ArgumentCaptor.forClass(Intent::class.java)
+ verify(notificationActivityStarter, Mockito.times(1))
+ .startNotificationGutsIntent(
+ captor.capture(),
+ ArgumentMatchers.anyInt(),
+ ArgumentMatchers.any()
+ )
+ assertEquals(Intent.ACTION_MANAGE_APP_PERMISSIONS, captor.value.action)
+ }
+
+ @Test
+ fun testAppOpsSettingsIntent_mic() {
+ val ops = ArraySet<Int>()
+ ops.add(AppOpsManager.OP_RECORD_AUDIO)
+ gutsManager.startAppOpsSettingsActivity("", 0, ops, null)
+ val captor = ArgumentCaptor.forClass(Intent::class.java)
+ verify(notificationActivityStarter, Mockito.times(1))
+ .startNotificationGutsIntent(
+ captor.capture(),
+ ArgumentMatchers.anyInt(),
+ ArgumentMatchers.any()
+ )
+ assertEquals(Intent.ACTION_MANAGE_APP_PERMISSIONS, captor.value.action)
+ }
+
+ @Test
+ fun testAppOpsSettingsIntent_camera_mic() {
+ val ops = ArraySet<Int>()
+ ops.add(AppOpsManager.OP_CAMERA)
+ ops.add(AppOpsManager.OP_RECORD_AUDIO)
+ gutsManager.startAppOpsSettingsActivity("", 0, ops, null)
+ val captor = ArgumentCaptor.forClass(Intent::class.java)
+ verify(notificationActivityStarter, Mockito.times(1))
+ .startNotificationGutsIntent(
+ captor.capture(),
+ ArgumentMatchers.anyInt(),
+ ArgumentMatchers.any()
+ )
+ assertEquals(Intent.ACTION_MANAGE_APP_PERMISSIONS, captor.value.action)
+ }
+
+ @Test
+ fun testAppOpsSettingsIntent_overlay() {
+ val ops = ArraySet<Int>()
+ ops.add(AppOpsManager.OP_SYSTEM_ALERT_WINDOW)
+ gutsManager.startAppOpsSettingsActivity("", 0, ops, null)
+ val captor = ArgumentCaptor.forClass(Intent::class.java)
+ verify(notificationActivityStarter, Mockito.times(1))
+ .startNotificationGutsIntent(
+ captor.capture(),
+ ArgumentMatchers.anyInt(),
+ ArgumentMatchers.any()
+ )
+ assertEquals(Settings.ACTION_MANAGE_APP_OVERLAY_PERMISSION, captor.value.action)
+ }
+
+ @Test
+ fun testAppOpsSettingsIntent_camera_mic_overlay() {
+ val ops = ArraySet<Int>()
+ ops.add(AppOpsManager.OP_CAMERA)
+ ops.add(AppOpsManager.OP_RECORD_AUDIO)
+ ops.add(AppOpsManager.OP_SYSTEM_ALERT_WINDOW)
+ gutsManager.startAppOpsSettingsActivity("", 0, ops, null)
+ val captor = ArgumentCaptor.forClass(Intent::class.java)
+ verify(notificationActivityStarter, Mockito.times(1))
+ .startNotificationGutsIntent(
+ captor.capture(),
+ ArgumentMatchers.anyInt(),
+ ArgumentMatchers.any()
+ )
+ assertEquals(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, captor.value.action)
+ }
+
+ @Test
+ fun testAppOpsSettingsIntent_camera_overlay() {
+ val ops = ArraySet<Int>()
+ ops.add(AppOpsManager.OP_CAMERA)
+ ops.add(AppOpsManager.OP_SYSTEM_ALERT_WINDOW)
+ gutsManager.startAppOpsSettingsActivity("", 0, ops, null)
+ val captor = ArgumentCaptor.forClass(Intent::class.java)
+ verify(notificationActivityStarter, Mockito.times(1))
+ .startNotificationGutsIntent(
+ captor.capture(),
+ ArgumentMatchers.anyInt(),
+ ArgumentMatchers.any()
+ )
+ assertEquals(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, captor.value.action)
+ }
+
+ @Test
+ fun testAppOpsSettingsIntent_mic_overlay() {
+ val ops = ArraySet<Int>()
+ ops.add(AppOpsManager.OP_RECORD_AUDIO)
+ ops.add(AppOpsManager.OP_SYSTEM_ALERT_WINDOW)
+ gutsManager.startAppOpsSettingsActivity("", 0, ops, null)
+ val captor = ArgumentCaptor.forClass(Intent::class.java)
+ verify(notificationActivityStarter, Mockito.times(1))
+ .startNotificationGutsIntent(
+ captor.capture(),
+ ArgumentMatchers.anyInt(),
+ ArgumentMatchers.any()
+ )
+ assertEquals(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, captor.value.action)
+ }
+
+ @Test
+ @Throws(Exception::class)
+ fun testInitializeNotificationInfoView_highPriority() {
+ val notificationInfoView = Mockito.mock(NotificationInfo::class.java)
+ val row = Mockito.spy(helper.createRow())
+ val entry = row.entry
+ NotificationEntryHelper.modifyRanking(entry)
+ .setUserSentiment(Ranking.USER_SENTIMENT_NEGATIVE)
+ .setImportance(NotificationManager.IMPORTANCE_HIGH)
+ .build()
+ Mockito.`when`(row.getIsNonblockable()).thenReturn(false)
+ Mockito.`when`(highPriorityProvider.isHighPriority(entry)).thenReturn(true)
+ val statusBarNotification = entry.sbn
+ gutsManager.initializeNotificationInfo(row, notificationInfoView)
+ verify(notificationInfoView)
+ .bindNotification(
+ ArgumentMatchers.any(PackageManager::class.java),
+ ArgumentMatchers.any(INotificationManager::class.java),
+ ArgumentMatchers.eq(onUserInteractionCallback),
+ ArgumentMatchers.eq(channelEditorDialogController),
+ ArgumentMatchers.eq(statusBarNotification.packageName),
+ ArgumentMatchers.any(NotificationChannel::class.java),
+ ArgumentMatchers.eq(entry),
+ ArgumentMatchers.any(NotificationInfo.OnSettingsClickListener::class.java),
+ ArgumentMatchers.any(NotificationInfo.OnAppSettingsClickListener::class.java),
+ ArgumentMatchers.any(UiEventLogger::class.java),
+ ArgumentMatchers.eq(true),
+ ArgumentMatchers.eq(false),
+ ArgumentMatchers.eq(true), /* wasShownHighPriority */
+ ArgumentMatchers.eq(assistantFeedbackController),
+ ArgumentMatchers.any(MetricsLogger::class.java)
+ )
+ }
+
+ @Test
+ @Throws(Exception::class)
+ fun testInitializeNotificationInfoView_PassesAlongProvisionedState() {
+ val notificationInfoView = Mockito.mock(NotificationInfo::class.java)
+ val row = Mockito.spy(helper.createRow())
+ NotificationEntryHelper.modifyRanking(row.entry)
+ .setUserSentiment(Ranking.USER_SENTIMENT_NEGATIVE)
+ .build()
+ Mockito.`when`(row.getIsNonblockable()).thenReturn(false)
+ val statusBarNotification = row.entry.sbn
+ val entry = row.entry
+ gutsManager.initializeNotificationInfo(row, notificationInfoView)
+ verify(notificationInfoView)
+ .bindNotification(
+ ArgumentMatchers.any(PackageManager::class.java),
+ ArgumentMatchers.any(INotificationManager::class.java),
+ ArgumentMatchers.eq(onUserInteractionCallback),
+ ArgumentMatchers.eq(channelEditorDialogController),
+ ArgumentMatchers.eq(statusBarNotification.packageName),
+ ArgumentMatchers.any(NotificationChannel::class.java),
+ ArgumentMatchers.eq(entry),
+ ArgumentMatchers.any(NotificationInfo.OnSettingsClickListener::class.java),
+ ArgumentMatchers.any(NotificationInfo.OnAppSettingsClickListener::class.java),
+ ArgumentMatchers.any(UiEventLogger::class.java),
+ ArgumentMatchers.eq(true),
+ ArgumentMatchers.eq(false),
+ ArgumentMatchers.eq(false), /* wasShownHighPriority */
+ ArgumentMatchers.eq(assistantFeedbackController),
+ ArgumentMatchers.any(MetricsLogger::class.java)
+ )
+ }
+
+ @Test
+ @Throws(Exception::class)
+ fun testInitializeNotificationInfoView_withInitialAction() {
+ val notificationInfoView = Mockito.mock(NotificationInfo::class.java)
+ val row = Mockito.spy(helper.createRow())
+ NotificationEntryHelper.modifyRanking(row.entry)
+ .setUserSentiment(Ranking.USER_SENTIMENT_NEGATIVE)
+ .build()
+ Mockito.`when`(row.getIsNonblockable()).thenReturn(false)
+ val statusBarNotification = row.entry.sbn
+ val entry = row.entry
+ gutsManager.initializeNotificationInfo(row, notificationInfoView)
+ verify(notificationInfoView)
+ .bindNotification(
+ ArgumentMatchers.any(PackageManager::class.java),
+ ArgumentMatchers.any(INotificationManager::class.java),
+ ArgumentMatchers.eq(onUserInteractionCallback),
+ ArgumentMatchers.eq(channelEditorDialogController),
+ ArgumentMatchers.eq(statusBarNotification.packageName),
+ ArgumentMatchers.any(NotificationChannel::class.java),
+ ArgumentMatchers.eq(entry),
+ ArgumentMatchers.any(NotificationInfo.OnSettingsClickListener::class.java),
+ ArgumentMatchers.any(NotificationInfo.OnAppSettingsClickListener::class.java),
+ ArgumentMatchers.any(UiEventLogger::class.java),
+ ArgumentMatchers.eq(true),
+ ArgumentMatchers.eq(false),
+ ArgumentMatchers.eq(false), /* wasShownHighPriority */
+ ArgumentMatchers.eq(assistantFeedbackController),
+ ArgumentMatchers.any(MetricsLogger::class.java)
+ )
+ }
+
+ private fun createTestNotificationRow(): ExpandableNotificationRow? {
+ val nb =
+ Notification.Builder(mContext, testNotificationChannel.id)
+ .setContentTitle("foo")
+ .setColorized(true)
+ .setColor(Color.RED)
+ .setFlag(Notification.FLAG_CAN_COLORIZE, true)
+ .setSmallIcon(R.drawable.sym_def_app_icon)
+ return try {
+ val row = helper.createRow(nb.build())
+ NotificationEntryHelper.modifyRanking(row.entry)
+ .setChannel(testNotificationChannel)
+ .build()
+ row
+ } catch (e: Exception) {
+ org.junit.Assert.fail()
+ null
+ }
+ }
+
+ private fun setIsLockscreenOrShadeVisible(isVisible: Boolean) {
+ val key =
+ if (isVisible) {
+ SceneKey.Lockscreen
+ } else {
+ SceneKey.Bouncer
+ }
+ sceneInteractor.changeScene(SceneModel(key), "test")
+ sceneInteractor.setTransitionState(
+ MutableStateFlow<ObservableTransitionState>(ObservableTransitionState.Idle(key))
+ )
+ testScope.runCurrent()
+ }
+
+ private fun createTestMenuItem(
+ row: ExpandableNotificationRow?
+ ): NotificationMenuRowPlugin.MenuItem {
+ val menuRow: NotificationMenuRowPlugin =
+ NotificationMenuRow(mContext, peopleNotificationIdentifier)
+ menuRow.createMenu(row, row!!.entry.sbn)
+ val menuItem = menuRow.getLongpressMenuItem(mContext)
+ Assert.assertNotNull(menuItem)
+ return menuItem
+ }
+
+ companion object {
+ private const val TEST_CHANNEL_ID = "NotificationManagerServiceTestChannelId"
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/SingleLineConversationViewBinderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/SingleLineConversationViewBinderTest.kt
new file mode 100644
index 0000000..1c959af
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/SingleLineConversationViewBinderTest.kt
@@ -0,0 +1,117 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.systemui.statusbar.notification.row
+
+import android.app.Notification
+import android.app.Person
+import android.platform.test.annotations.EnableFlags
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import androidx.test.filters.SmallTest
+import com.android.internal.R
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_SINGLE_LINE
+import com.android.systemui.statusbar.notification.row.SingleLineViewInflater.inflateSingleLineViewHolder
+import com.android.systemui.statusbar.notification.row.shared.AsyncHybridViewInflation
+import com.android.systemui.statusbar.notification.row.ui.viewbinder.SingleLineConversationViewBinder
+import com.android.systemui.util.mockito.mock
+import kotlin.test.assertEquals
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@TestableLooper.RunWithLooper
+class SingleLineConversationViewBinderTest : SysuiTestCase() {
+ private lateinit var notificationBuilder: Notification.Builder
+ private lateinit var helper: NotificationTestHelper
+
+ @Before
+ fun setUp() {
+ allowTestableLooperAsMainThread()
+ helper = NotificationTestHelper(context, mDependency, TestableLooper.get(this))
+ notificationBuilder = Notification.Builder(context, CHANNEL_ID)
+ notificationBuilder
+ .setSmallIcon(R.drawable.ic_corp_icon)
+ .setContentTitle(CONTENT_TITLE)
+ .setContentText(CONTENT_TEXT)
+ }
+
+ @Test
+ @EnableFlags(AsyncHybridViewInflation.FLAG_NAME)
+ fun bindGroupConversationSingleLineView() {
+ // GIVEN a row with a group conversation notification
+ val user =
+ Person.Builder()
+ // .setIcon(Icon.createWithResource(mContext,
+ // R.drawable.ic_account_circle))
+ .setName(USER_NAME)
+ .build()
+ val style =
+ Notification.MessagingStyle(user)
+ .addMessage(MESSAGE_TEXT, System.currentTimeMillis(), user)
+ .addMessage(
+ "How about lunch?",
+ System.currentTimeMillis(),
+ Person.Builder().setName("user2").build()
+ )
+ .setGroupConversation(true)
+ notificationBuilder.setStyle(style).setShortcutId(SHORTCUT_ID)
+ val notification = notificationBuilder.build()
+ val row = helper.createRow(notification)
+
+ val viewHolder =
+ inflateSingleLineViewHolder(
+ isConversation = true,
+ reinflateFlags = FLAG_CONTENT_VIEW_SINGLE_LINE,
+ entry = row.entry,
+ context = context,
+ logger = mock()
+ )
+ as HybridConversationNotificationView
+ val viewModel =
+ SingleLineViewInflater.inflateSingleLineViewModel(
+ notification = notification,
+ messagingStyle = style,
+ builder = notificationBuilder,
+ systemUiContext = context,
+ )
+ // WHEN: binds the viewHolder
+ SingleLineConversationViewBinder.bind(
+ viewModel,
+ viewHolder,
+ )
+
+ // THEN: the single-line conversation view should be bind with view model's corresponding
+ // fields
+ assertEquals(viewModel.titleText, viewHolder.titleView.text)
+ assertEquals(viewModel.contentText, viewHolder.textView.text)
+ assertEquals(
+ viewModel.conversationData?.conversationSenderName,
+ viewHolder.conversationSenderNameView.text
+ )
+ }
+
+ private companion object {
+ const val CHANNEL_ID = "CHANNEL_ID"
+ const val CONTENT_TITLE = "CONTENT_TITLE"
+ const val CONTENT_TEXT = "CONTENT_TEXT"
+ const val USER_NAME = "USER_NAME"
+ const val MESSAGE_TEXT = "MESSAGE_TEXT"
+ const val SHORTCUT_ID = "Shortcut"
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/SingleLineViewBinderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/SingleLineViewBinderTest.kt
new file mode 100644
index 0000000..f0fc349
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/SingleLineViewBinderTest.kt
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.systemui.statusbar.notification.row
+
+import android.app.Notification
+import android.platform.test.annotations.EnableFlags
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import androidx.test.filters.SmallTest
+import com.android.internal.R
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_SINGLE_LINE
+import com.android.systemui.statusbar.notification.row.SingleLineViewInflater.inflateSingleLineViewHolder
+import com.android.systemui.statusbar.notification.row.shared.AsyncHybridViewInflation
+import com.android.systemui.statusbar.notification.row.ui.viewbinder.SingleLineViewBinder
+import com.android.systemui.util.mockito.mock
+import org.junit.Assert
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@TestableLooper.RunWithLooper
+class SingleLineViewBinderTest : SysuiTestCase() {
+ private lateinit var notificationBuilder: Notification.Builder
+ private lateinit var helper: NotificationTestHelper
+
+ @Before
+ fun setUp() {
+ allowTestableLooperAsMainThread()
+ helper = NotificationTestHelper(mContext, mDependency, TestableLooper.get(this))
+ notificationBuilder = Notification.Builder(mContext, CHANNEL_ID)
+ notificationBuilder
+ .setSmallIcon(R.drawable.ic_corp_icon)
+ .setContentTitle(CONTENT_TITLE)
+ .setContentText(CONTENT_TEXT)
+ }
+
+ @Test
+ @EnableFlags(AsyncHybridViewInflation.FLAG_NAME)
+ fun bindNonConversationSingleLineView() {
+ // GIVEN: a row with bigText style notification
+ val style = Notification.BigTextStyle().bigText(CONTENT_TEXT)
+ notificationBuilder.setStyle(style)
+ val notification = notificationBuilder.build()
+ val row: ExpandableNotificationRow = helper.createRow(notification)
+
+ val viewHolder =
+ inflateSingleLineViewHolder(
+ isConversation = false,
+ reinflateFlags = FLAG_CONTENT_VIEW_SINGLE_LINE,
+ entry = row.entry,
+ context = context,
+ logger = mock()
+ )
+ val viewModel =
+ SingleLineViewInflater.inflateSingleLineViewModel(
+ notification = notification,
+ messagingStyle = null,
+ builder = notificationBuilder,
+ systemUiContext = context,
+ )
+
+ // WHEN: binds the viewHolder
+ SingleLineViewBinder.bind(viewModel, viewHolder)
+
+ // THEN: the single-line view should be bind with viewModel's title and content text
+ Assert.assertEquals(viewModel.titleText, viewHolder?.titleView?.text)
+ Assert.assertEquals(viewModel.contentText, viewHolder?.textView?.text)
+ }
+
+ private companion object {
+ const val CHANNEL_ID = "CHANNEL_ID"
+ const val CONTENT_TITLE = "A Cool New Feature"
+ const val CONTENT_TEXT = "Checkout out new feature!"
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/SingleLineViewInflaterTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/SingleLineViewInflaterTest.kt
new file mode 100644
index 0000000..b67153a
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/SingleLineViewInflaterTest.kt
@@ -0,0 +1,463 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.row
+
+import android.app.Notification
+import android.app.Person
+import android.content.Context
+import android.graphics.Bitmap
+import android.graphics.Canvas
+import android.graphics.Paint
+import android.graphics.PorterDuff
+import android.graphics.PorterDuffXfermode
+import android.graphics.drawable.Drawable
+import android.graphics.drawable.Icon
+import android.platform.test.annotations.EnableFlags
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import androidx.core.graphics.drawable.toBitmap
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.res.R
+import com.android.systemui.statusbar.notification.row.shared.AsyncHybridViewInflation
+import com.android.systemui.statusbar.notification.row.ui.viewmodel.ConversationAvatar
+import com.android.systemui.statusbar.notification.row.ui.viewmodel.FacePile
+import com.android.systemui.statusbar.notification.row.ui.viewmodel.SingleIcon
+import com.android.systemui.statusbar.notification.row.ui.viewmodel.SingleLineViewModel
+import kotlin.test.assertEquals
+import kotlin.test.assertIs
+import kotlin.test.assertIsNot
+import kotlin.test.assertNull
+import kotlin.test.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@TestableLooper.RunWithLooper
+@EnableFlags(AsyncHybridViewInflation.FLAG_NAME)
+class SingleLineViewInflaterTest : SysuiTestCase() {
+ private lateinit var helper: NotificationTestHelper
+ // Non-group MessagingStyles only have firstSender
+ private lateinit var firstSender: Person
+ private lateinit var lastSender: Person
+ private lateinit var firstSenderIcon: Icon
+ private lateinit var lastSenderIcon: Icon
+ private var firstSenderIconDrawable: Drawable? = null
+ private var lastSenderIconDrawable: Drawable? = null
+ private val currentUser: Person? = null
+
+ private companion object {
+ const val FIRST_SENDER_NAME = "First Sender"
+ const val LAST_SENDER_NAME = "Second Sender"
+ const val LAST_MESSAGE = "How about lunch?"
+
+ const val CONVERSATION_TITLE = "The Sender Family"
+ const val CONTENT_TITLE = "A Cool Group"
+ const val CONTENT_TEXT = "This is an amazing group chat"
+
+ const val SHORTCUT_ID = "Shortcut"
+ }
+
+ @Before
+ fun setUp() {
+ helper = NotificationTestHelper(mContext, mDependency, TestableLooper.get(this))
+ firstSenderIcon = Icon.createWithBitmap(getBitmap(context, R.drawable.ic_person))
+ firstSenderIconDrawable = firstSenderIcon.loadDrawable(context)
+ lastSenderIcon =
+ Icon.createWithBitmap(
+ getBitmap(context, com.android.internal.R.drawable.ic_account_circle)
+ )
+ lastSenderIconDrawable = lastSenderIcon.loadDrawable(context)
+ firstSender = Person.Builder().setName(FIRST_SENDER_NAME).setIcon(firstSenderIcon).build()
+ lastSender = Person.Builder().setName(LAST_SENDER_NAME).setIcon(lastSenderIcon).build()
+ }
+
+ @Test
+ fun createViewModelForNonConversationSingleLineView() {
+ // Given: a non-conversation notification
+ val notificationType = NonMessaging()
+ val notification = getNotification(NonMessaging())
+
+ // When: inflate the SingleLineViewModel
+ val singleLineViewModel = notification.makeSingleLineViewModel(notificationType)
+
+ // Then: the inflated SingleLineViewModel should be as expected
+ // conversationData: null, because it's not a conversation notification
+ assertEquals(SingleLineViewModel(CONTENT_TITLE, CONTENT_TEXT, null), singleLineViewModel)
+ }
+
+ @Test
+ fun createViewModelForNonGroupConversationNotification() {
+ // Given: a non-group conversation notification
+ val notificationType = OneToOneConversation()
+ val notification = getNotification(notificationType)
+
+ // When: inflate the SingleLineViewModel
+ val singleLineViewModel = notification.makeSingleLineViewModel(notificationType)
+
+ // Then: the inflated SingleLineViewModel should be as expected
+ // titleText: Notification.ConversationTitle
+ // contentText: the last message text
+ // conversationSenderName: null, because it's not a group conversation
+ // conversationData.avatar: a single icon of the last sender
+ assertEquals(CONVERSATION_TITLE, singleLineViewModel.titleText)
+ assertEquals(LAST_MESSAGE, singleLineViewModel.contentText)
+ assertNull(
+ singleLineViewModel.conversationData?.conversationSenderName,
+ "Sender name should be null for one-on-one conversation"
+ )
+ assertTrue {
+ singleLineViewModel.conversationData
+ ?.avatar
+ ?.equalsTo(SingleIcon(firstSenderIcon.loadDrawable(context))) == true
+ }
+ }
+
+ @Test
+ fun createViewModelForNonGroupLegacyMessagingStyleNotification() {
+ // Given: a non-group legacy messaging style notification
+ val notificationType = LegacyMessaging()
+ val notification = getNotification(notificationType)
+
+ // When: inflate the SingleLineViewModel
+ val singleLineViewModel = notification.makeSingleLineViewModel(notificationType)
+
+ // Then: the inflated SingleLineViewModel should be as expected
+ // titleText: CONVERSATION_TITLE: SENDER_NAME
+ // contentText: the last message text
+ // conversationData: null, because it's not a conversation notification
+ assertEquals("$CONVERSATION_TITLE: $FIRST_SENDER_NAME", singleLineViewModel.titleText)
+ assertEquals(LAST_MESSAGE, singleLineViewModel.contentText)
+ assertNull(
+ singleLineViewModel.conversationData,
+ "conversationData should be null for legacy messaging conversation"
+ )
+ }
+
+ @Test
+ fun createViewModelForGroupLegacyMessagingStyleNotification() {
+ // Given: a non-group legacy messaging style notification
+ val notificationType = LegacyMessagingGroup()
+ val notification = getNotification(notificationType)
+
+ // When: inflate the SingleLineViewModel
+ val singleLineViewModel = notification.makeSingleLineViewModel(notificationType)
+
+ // Then: the inflated SingleLineViewModel should be as expected
+ // titleText: CONVERSATION_TITLE: LAST_SENDER_NAME
+ // contentText: the last message text
+ // conversationData: null, because it's not a conversation notification
+ assertEquals("$CONVERSATION_TITLE: $LAST_SENDER_NAME", singleLineViewModel.titleText)
+ assertEquals(LAST_MESSAGE, singleLineViewModel.contentText)
+ assertNull(
+ singleLineViewModel.conversationData,
+ "conversationData should be null for legacy messaging conversation"
+ )
+ }
+
+ @Test
+ fun createViewModelForNonGroupConversationNotificationWithShortcutIcon() {
+ // Given: a non-group conversation notification with a shortcut icon
+ val shortcutIcon =
+ Icon.createWithResource(context, com.android.internal.R.drawable.ic_account_circle)
+ val notificationType = OneToOneConversation(shortcutIcon = shortcutIcon)
+ val notification = getNotification(notificationType)
+
+ // When: inflate the SingleLineViewModel
+ val singleLineViewModel = notification.makeSingleLineViewModel(notificationType)
+
+ // Then: the inflated SingleLineViewModel should be expected
+ // titleText: Notification.ConversationTitle
+ // contentText: the last message text
+ // conversationSenderName: null, because it's not a group conversation
+ // conversationData.avatar: a single icon of the shortcut icon
+ assertEquals(CONVERSATION_TITLE, singleLineViewModel.titleText)
+ assertEquals(LAST_MESSAGE, singleLineViewModel.contentText)
+ assertNull(
+ singleLineViewModel.conversationData?.conversationSenderName,
+ "Sender name should be null for one-on-one conversation"
+ )
+ assertTrue {
+ singleLineViewModel.conversationData
+ ?.avatar
+ ?.equalsTo(SingleIcon(shortcutIcon.loadDrawable(context))) == true
+ }
+ }
+
+ @Test
+ fun createViewModelForGroupConversationNotificationWithLargeIcon() {
+ // Given: a group conversation notification with a large icon
+ val largeIcon =
+ Icon.createWithResource(context, com.android.internal.R.drawable.ic_account_circle)
+ val notificationType = GroupConversation(largeIcon = largeIcon)
+ val notification = getNotification(notificationType)
+
+ // When: inflate the SingleLineViewModel
+ val singleLineViewModel = notification.makeSingleLineViewModel(notificationType)
+
+ // Then: the inflated SingleLineViewModel should be expected
+ // titleText: Notification.ConversationTitle
+ // contentText: the last message text
+ // conversationSenderName: the last non-user sender's name
+ // conversationData.avatar: a single icon
+ assertEquals(CONVERSATION_TITLE, singleLineViewModel.titleText)
+ assertEquals(LAST_MESSAGE, singleLineViewModel.contentText)
+ assertEquals(
+ context.resources.getString(
+ com.android.internal.R.string.conversation_single_line_name_display,
+ LAST_SENDER_NAME
+ ),
+ singleLineViewModel.conversationData?.conversationSenderName
+ )
+ assertTrue {
+ singleLineViewModel.conversationData
+ ?.avatar
+ ?.equalsTo(SingleIcon(largeIcon.loadDrawable(context))) == true
+ }
+ }
+
+ @Test
+ fun createViewModelForGroupConversationWithNoIcon() {
+ // Given: a group conversation notification
+ val notificationType = GroupConversation()
+ val notification = getNotification(notificationType)
+
+ // When: inflate the SingleLineViewModel
+ val singleLineViewModel = notification.makeSingleLineViewModel(notificationType)
+
+ // Then: the inflated SingleLineViewModel should be expected
+ // titleText: Notification.ConversationTitle
+ // contentText: the last message text
+ // conversationSenderName: the last non-user sender's name
+ // conversationData.avatar: a face-pile consists the last sender's icon
+ assertEquals(CONVERSATION_TITLE, singleLineViewModel.titleText)
+ assertEquals(LAST_MESSAGE, singleLineViewModel.contentText)
+ assertEquals(
+ context.resources.getString(
+ com.android.internal.R.string.conversation_single_line_name_display,
+ LAST_SENDER_NAME
+ ),
+ singleLineViewModel.conversationData?.conversationSenderName
+ )
+
+ val backgroundColor =
+ Notification.Builder.recoverBuilder(context, notification)
+ .getBackgroundColor(/* isHeader = */ false)
+ assertTrue {
+ singleLineViewModel.conversationData
+ ?.avatar
+ ?.equalsTo(
+ FacePile(
+ firstSenderIconDrawable,
+ lastSenderIconDrawable,
+ backgroundColor,
+ )
+ ) == true
+ }
+ }
+
+ sealed class NotificationType(val largeIcon: Icon? = null)
+
+ class NonMessaging(largeIcon: Icon? = null) : NotificationType(largeIcon)
+
+ class LegacyMessaging(largeIcon: Icon? = null) : NotificationType(largeIcon)
+
+ class LegacyMessagingGroup(largeIcon: Icon? = null) : NotificationType(largeIcon)
+
+ class OneToOneConversation(largeIcon: Icon? = null, val shortcutIcon: Icon? = null) :
+ NotificationType(largeIcon)
+
+ class GroupConversation(largeIcon: Icon? = null) : NotificationType(largeIcon)
+
+ private fun getNotification(type: NotificationType): Notification {
+ val notificationBuilder: Notification.Builder =
+ Notification.Builder(mContext, "channelId")
+ .setSmallIcon(R.drawable.ic_person)
+ .setContentTitle(CONTENT_TITLE)
+ .setContentText(CONTENT_TEXT)
+ .setLargeIcon(type.largeIcon)
+
+ val user = Person.Builder().setName("User").build()
+
+ val buildMessagingStyle =
+ Notification.MessagingStyle(user)
+ .setConversationTitle(CONVERSATION_TITLE)
+ .addMessage("Hi", 0, currentUser)
+
+ return when (type) {
+ is NonMessaging ->
+ notificationBuilder
+ .setStyle(Notification.BigTextStyle().bigText("Big Text"))
+ .build()
+ is LegacyMessaging -> {
+ buildMessagingStyle
+ .addMessage("What's up?", 0, firstSender)
+ .addMessage("Not much", 0, currentUser)
+ .addMessage(LAST_MESSAGE, 0, firstSender)
+
+ val notification = notificationBuilder.setStyle(buildMessagingStyle).build()
+
+ assertNull(notification.shortcutId)
+ notification
+ }
+ is LegacyMessagingGroup -> {
+ buildMessagingStyle
+ .addMessage("What's up?", 0, firstSender)
+ .addMessage("Check out my new hover board!", 0, lastSender)
+ .setGroupConversation(true)
+ .addMessage(LAST_MESSAGE, 0, lastSender)
+
+ val notification = notificationBuilder.setStyle(buildMessagingStyle).build()
+
+ assertNull(notification.shortcutId)
+ notification
+ }
+ is OneToOneConversation -> {
+ buildMessagingStyle
+ .addMessage("What's up?", 0, firstSender)
+ .addMessage("Not much", 0, currentUser)
+ .addMessage(LAST_MESSAGE, 0, firstSender)
+ .setShortcutIcon(type.shortcutIcon)
+ notificationBuilder.setShortcutId(SHORTCUT_ID).setStyle(buildMessagingStyle).build()
+ }
+ is GroupConversation -> {
+ buildMessagingStyle
+ .addMessage("What's up?", 0, firstSender)
+ .addMessage("Check out my new hover board!", 0, lastSender)
+ .setGroupConversation(true)
+ .addMessage(LAST_MESSAGE, 0, lastSender)
+ notificationBuilder.setShortcutId(SHORTCUT_ID).setStyle(buildMessagingStyle).build()
+ }
+ }
+ }
+
+ private fun Notification.makeSingleLineViewModel(type: NotificationType): SingleLineViewModel {
+ val builder = Notification.Builder.recoverBuilder(context, this)
+
+ // Validate the recovered builder has the right type of style
+ val expectMessagingStyle =
+ when (type) {
+ is LegacyMessaging,
+ is LegacyMessagingGroup,
+ is OneToOneConversation,
+ is GroupConversation -> true
+ else -> false
+ }
+ if (expectMessagingStyle) {
+ assertIs<Notification.MessagingStyle>(
+ builder.style,
+ "Notification style should be MessagingStyle"
+ )
+ } else {
+ assertIsNot<Notification.MessagingStyle>(
+ builder.style,
+ message = "Notification style should not be MessagingStyle"
+ )
+ }
+
+ // Inflate the SingleLineViewModel
+ // Mock the behavior of NotificationContentInflater.doInBackground
+ val messagingStyle = builder.getMessagingStyle()
+ val isConversation = type is OneToOneConversation || type is GroupConversation
+ return SingleLineViewInflater.inflateSingleLineViewModel(
+ this,
+ if (isConversation) messagingStyle else null,
+ builder,
+ context
+ )
+ }
+
+ private fun Notification.Builder.getMessagingStyle(): Notification.MessagingStyle? {
+ return style as? Notification.MessagingStyle
+ }
+
+ private fun getBitmap(context: Context, resId: Int): Bitmap {
+ val largeIconDimension =
+ context.resources.getDimension(R.dimen.conversation_single_line_avatar_size)
+ val d = context.resources.getDrawable(resId)
+ val b =
+ Bitmap.createBitmap(
+ largeIconDimension.toInt(),
+ largeIconDimension.toInt(),
+ Bitmap.Config.ARGB_8888
+ )
+ val c = Canvas(b)
+ val paint = Paint()
+ c.drawCircle(
+ largeIconDimension / 2,
+ largeIconDimension / 2,
+ largeIconDimension.coerceAtMost(largeIconDimension) / 2,
+ paint
+ )
+ d.setBounds(0, 0, largeIconDimension.toInt(), largeIconDimension.toInt())
+ paint.setXfermode(PorterDuffXfermode(PorterDuff.Mode.SRC_IN))
+ c.saveLayer(0F, 0F, largeIconDimension, largeIconDimension, paint, Canvas.ALL_SAVE_FLAG)
+ d.draw(c)
+ c.restore()
+ return b
+ }
+
+ fun ConversationAvatar.equalsTo(other: ConversationAvatar?): Boolean =
+ when {
+ this === other -> true
+ this is SingleIcon && other is SingleIcon -> equalsTo(other)
+ this is FacePile && other is FacePile -> equalsTo(other)
+ else -> false
+ }
+
+ private fun SingleIcon.equalsTo(other: SingleIcon): Boolean =
+ iconDrawable?.equalsTo(other.iconDrawable) == true
+
+ private fun FacePile.equalsTo(other: FacePile): Boolean =
+ when {
+ bottomBackgroundColor != other.bottomBackgroundColor -> false
+ topIconDrawable?.equalsTo(other.topIconDrawable) != true -> false
+ bottomIconDrawable?.equalsTo(other.bottomIconDrawable) != true -> false
+ else -> true
+ }
+
+ fun Drawable.equalsTo(other: Drawable?): Boolean =
+ when {
+ this === other -> true
+ this.pixelsEqualTo(other) -> true
+ else -> false
+ }
+
+ private fun <T : Drawable> T.pixelsEqualTo(t: T?) =
+ toBitmap().pixelsEqualTo(t?.toBitmap(), false)
+
+ private fun Bitmap.pixelsEqualTo(otherBitmap: Bitmap?, shouldRecycle: Boolean = false) =
+ otherBitmap?.let { other ->
+ if (width == other.width && height == other.height) {
+ val res = toPixels().contentEquals(other.toPixels())
+ if (shouldRecycle) {
+ doRecycle().also { otherBitmap.doRecycle() }
+ }
+ res
+ } else false
+ }
+ ?: kotlin.run { false }
+
+ private fun Bitmap.toPixels() =
+ IntArray(width * height).apply { getPixels(this, 0, width, 0, 0, width, height) }
+
+ fun Bitmap.doRecycle() {
+ if (!isRecycled) recycle()
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java
index 89f826b..dbe63f2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java
@@ -16,6 +16,7 @@
package com.android.systemui.statusbar.notification.stack;
+import static com.android.systemui.Flags.FLAG_SCREENSHARE_NOTIFICATION_HIDING;
import static com.android.systemui.log.LogBufferHelperKt.logcatLogBuffer;
import static com.android.systemui.statusbar.StatusBarState.KEYGUARD;
import static com.android.systemui.statusbar.StatusBarState.SHADE;
@@ -32,6 +33,7 @@
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyZeroInteractions;
import static org.mockito.Mockito.when;
import static kotlinx.coroutines.flow.FlowKt.emptyFlow;
@@ -39,6 +41,7 @@
import android.metrics.LogMaker;
import android.platform.test.annotations.DisableFlags;
+import android.platform.test.annotations.EnableFlags;
import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper;
import android.view.View;
@@ -101,6 +104,7 @@
import com.android.systemui.statusbar.policy.DeviceProvisionedController;
import com.android.systemui.statusbar.policy.HeadsUpManager;
import com.android.systemui.statusbar.policy.ResourcesSplitShadeStateController;
+import com.android.systemui.statusbar.policy.SensitiveNotificationProtectionController;
import com.android.systemui.statusbar.policy.ZenModeController;
import com.android.systemui.tuner.TunerService;
import com.android.systemui.util.settings.SecureSettings;
@@ -172,10 +176,16 @@
@Mock private ActivityStarter mActivityStarter;
@Mock private KeyguardTransitionRepository mKeyguardTransitionRepo;
@Mock private NotificationListViewBinder mViewBinder;
+ @Mock
+ private SensitiveNotificationProtectionController mSensitiveNotificationProtectionController;
+
+ @Captor
+ private ArgumentCaptor<Runnable> mSensitiveStateListenerArgumentCaptor;
@Captor
private ArgumentCaptor<StatusBarStateController.StateListener> mStateListenerArgumentCaptor;
+
private final ActiveNotificationListRepository mActiveNotificationsRepository =
new ActiveNotificationListRepository();
@@ -386,6 +396,23 @@
}
@Test
+ public void testOnUserChange_verifyNotSensitive() {
+ when(mNotificationLockscreenUserManager.isAnyProfilePublicMode()).thenReturn(false);
+ initController(/* viewIsAttached= */ true);
+
+ ArgumentCaptor<UserChangedListener> userChangedCaptor = ArgumentCaptor
+ .forClass(UserChangedListener.class);
+
+ verify(mNotificationLockscreenUserManager)
+ .addUserChangedListener(userChangedCaptor.capture());
+ reset(mNotificationStackScrollLayout);
+
+ UserChangedListener changedListener = userChangedCaptor.getValue();
+ changedListener.onUserChanged(0);
+ verify(mNotificationStackScrollLayout).updateSensitiveness(false, false);
+ }
+
+ @Test
public void testOnUserChange_verifySensitiveProfile() {
when(mNotificationLockscreenUserManager.isAnyProfilePublicMode()).thenReturn(true);
initController(/* viewIsAttached= */ true);
@@ -403,6 +430,80 @@
}
@Test
+ @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING)
+ public void testOnUserChange_verifyNotSensitive_screenshareNotificationHidingEnabled() {
+ when(mNotificationLockscreenUserManager.isAnyProfilePublicMode()).thenReturn(false);
+ when(mSensitiveNotificationProtectionController.isSensitiveStateActive()).thenReturn(false);
+
+ initController(/* viewIsAttached= */ true);
+
+ ArgumentCaptor<UserChangedListener> userChangedCaptor = ArgumentCaptor
+ .forClass(UserChangedListener.class);
+
+ verify(mNotificationLockscreenUserManager)
+ .addUserChangedListener(userChangedCaptor.capture());
+ reset(mNotificationStackScrollLayout);
+
+ UserChangedListener changedListener = userChangedCaptor.getValue();
+ changedListener.onUserChanged(0);
+ verify(mNotificationStackScrollLayout).updateSensitiveness(false, false);
+ }
+
+ @Test
+ @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING)
+ public void testOnUserChange_verifySensitiveProfile_screenshareNotificationHidingEnabled() {
+ when(mNotificationLockscreenUserManager.isAnyProfilePublicMode()).thenReturn(true);
+ when(mSensitiveNotificationProtectionController.isSensitiveStateActive()).thenReturn(false);
+
+ initController(/* viewIsAttached= */ true);
+
+ ArgumentCaptor<UserChangedListener> userChangedCaptor = ArgumentCaptor
+ .forClass(UserChangedListener.class);
+
+ verify(mNotificationLockscreenUserManager)
+ .addUserChangedListener(userChangedCaptor.capture());
+ reset(mNotificationStackScrollLayout);
+
+ UserChangedListener changedListener = userChangedCaptor.getValue();
+ changedListener.onUserChanged(0);
+ verify(mNotificationStackScrollLayout).updateSensitiveness(false, true);
+ }
+
+ @Test
+ @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING)
+ public void testOnUserChange_verifySensitiveActive_screenshareNotificationHidingEnabled() {
+ when(mNotificationLockscreenUserManager.isAnyProfilePublicMode()).thenReturn(false);
+ when(mSensitiveNotificationProtectionController.isSensitiveStateActive()).thenReturn(true);
+ initController(/* viewIsAttached= */ true);
+
+ ArgumentCaptor<UserChangedListener> userChangedCaptor = ArgumentCaptor
+ .forClass(UserChangedListener.class);
+
+ verify(mNotificationLockscreenUserManager)
+ .addUserChangedListener(userChangedCaptor.capture());
+ reset(mNotificationStackScrollLayout);
+
+ UserChangedListener changedListener = userChangedCaptor.getValue();
+ changedListener.onUserChanged(0);
+ verify(mNotificationStackScrollLayout).updateSensitiveness(false, true);
+ }
+
+ @Test
+ public void testOnStatePostChange_verifyNotSensitive() {
+ when(mNotificationLockscreenUserManager.isAnyProfilePublicMode()).thenReturn(false);
+
+ initController(/* viewIsAttached= */ true);
+ verify(mSysuiStatusBarStateController).addCallback(
+ mStateListenerArgumentCaptor.capture(), anyInt());
+
+ StatusBarStateController.StateListener stateListener =
+ mStateListenerArgumentCaptor.getValue();
+
+ stateListener.onStatePostChange();
+ verify(mNotificationStackScrollLayout).updateSensitiveness(false, false);
+ }
+
+ @Test
public void testOnStatePostChange_verifyIfProfileIsPublic() {
when(mNotificationLockscreenUserManager.isAnyProfilePublicMode()).thenReturn(true);
@@ -418,6 +519,194 @@
}
@Test
+ @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING)
+ public void testOnStatePostChange_verifyNotSensitive_screenshareNotificationHidingEnabled() {
+ when(mNotificationLockscreenUserManager.isAnyProfilePublicMode()).thenReturn(false);
+ when(mSensitiveNotificationProtectionController.isSensitiveStateActive()).thenReturn(false);
+
+ initController(/* viewIsAttached= */ true);
+ verify(mSysuiStatusBarStateController).addCallback(
+ mStateListenerArgumentCaptor.capture(), anyInt());
+
+ StatusBarStateController.StateListener stateListener =
+ mStateListenerArgumentCaptor.getValue();
+
+ stateListener.onStatePostChange();
+ verify(mNotificationStackScrollLayout).updateSensitiveness(false, false);
+ }
+
+ @Test
+ @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING)
+ public void testOnStatePostChange_verifyIfProfileIsPublic_screenshareNotificationHidingEnabled(
+ ) {
+ when(mNotificationLockscreenUserManager.isAnyProfilePublicMode()).thenReturn(true);
+ when(mSensitiveNotificationProtectionController.isSensitiveStateActive()).thenReturn(false);
+
+ initController(/* viewIsAttached= */ true);
+ verify(mSysuiStatusBarStateController).addCallback(
+ mStateListenerArgumentCaptor.capture(), anyInt());
+
+ StatusBarStateController.StateListener stateListener =
+ mStateListenerArgumentCaptor.getValue();
+
+ stateListener.onStatePostChange();
+ verify(mNotificationStackScrollLayout).updateSensitiveness(false, true);
+ }
+
+ @Test
+ @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING)
+ public void testOnStatePostChange_verifyIfSensitiveActive_screenshareNotificationHidingEnabled(
+ ) {
+ when(mNotificationLockscreenUserManager.isAnyProfilePublicMode()).thenReturn(false);
+ when(mSensitiveNotificationProtectionController.isSensitiveStateActive()).thenReturn(true);
+
+ initController(/* viewIsAttached= */ true);
+ verify(mSysuiStatusBarStateController).addCallback(
+ mStateListenerArgumentCaptor.capture(), anyInt());
+
+ StatusBarStateController.StateListener stateListener =
+ mStateListenerArgumentCaptor.getValue();
+
+ stateListener.onStatePostChange();
+ verify(mNotificationStackScrollLayout).updateSensitiveness(false, true);
+ }
+
+ @Test
+ public void testOnStatePostChange_goingFullShade_verifyNotSensitive() {
+ when(mNotificationLockscreenUserManager.isAnyProfilePublicMode()).thenReturn(false);
+ when(mSysuiStatusBarStateController.goingToFullShade()).thenReturn(true);
+
+ initController(/* viewIsAttached= */ true);
+ verify(mSysuiStatusBarStateController).addCallback(
+ mStateListenerArgumentCaptor.capture(), anyInt());
+
+ StatusBarStateController.StateListener stateListener =
+ mStateListenerArgumentCaptor.getValue();
+
+ stateListener.onStatePostChange();
+ verify(mNotificationStackScrollLayout).updateSensitiveness(true, false);
+ }
+
+ @Test
+ public void testOnStatePostChange_goingFullShade_verifyIfProfileIsPublic() {
+ when(mNotificationLockscreenUserManager.isAnyProfilePublicMode()).thenReturn(true);
+ when(mSysuiStatusBarStateController.goingToFullShade()).thenReturn(true);
+
+ initController(/* viewIsAttached= */ true);
+ verify(mSysuiStatusBarStateController).addCallback(
+ mStateListenerArgumentCaptor.capture(), anyInt());
+
+ StatusBarStateController.StateListener stateListener =
+ mStateListenerArgumentCaptor.getValue();
+
+ stateListener.onStatePostChange();
+ verify(mNotificationStackScrollLayout).updateSensitiveness(true, true);
+ }
+
+ @Test
+ @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING)
+ public void testOnStatePostChange_goingFullShade_verifyNotSensitive_screenshareHideEnabled(
+ ) {
+ when(mNotificationLockscreenUserManager.isAnyProfilePublicMode()).thenReturn(false);
+ when(mSysuiStatusBarStateController.goingToFullShade()).thenReturn(true);
+ when(mSensitiveNotificationProtectionController.isSensitiveStateActive()).thenReturn(false);
+
+ initController(/* viewIsAttached= */ true);
+ verify(mSysuiStatusBarStateController).addCallback(
+ mStateListenerArgumentCaptor.capture(), anyInt());
+
+ StatusBarStateController.StateListener stateListener =
+ mStateListenerArgumentCaptor.getValue();
+
+ stateListener.onStatePostChange();
+ verify(mNotificationStackScrollLayout).updateSensitiveness(true, false);
+ }
+
+ @Test
+ @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING)
+ public void testOnStatePostChange_goingFullShade_verifyProfileIsPublic_screenshareHideEnabled(
+ ) {
+ when(mNotificationLockscreenUserManager.isAnyProfilePublicMode()).thenReturn(true);
+ when(mSysuiStatusBarStateController.goingToFullShade()).thenReturn(true);
+ when(mSensitiveNotificationProtectionController.isSensitiveStateActive()).thenReturn(false);
+
+ initController(/* viewIsAttached= */ true);
+ verify(mSysuiStatusBarStateController).addCallback(
+ mStateListenerArgumentCaptor.capture(), anyInt());
+
+ StatusBarStateController.StateListener stateListener =
+ mStateListenerArgumentCaptor.getValue();
+
+ stateListener.onStatePostChange();
+ verify(mNotificationStackScrollLayout).updateSensitiveness(true, true);
+ }
+
+ @Test
+ @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING)
+ public void testOnStatePostChange_goingFullShade_verifySensitiveActive_screenshareHideEnabled(
+ ) {
+ when(mNotificationLockscreenUserManager.isAnyProfilePublicMode()).thenReturn(false);
+ when(mSysuiStatusBarStateController.goingToFullShade()).thenReturn(true);
+ when(mSensitiveNotificationProtectionController.isSensitiveStateActive()).thenReturn(true);
+
+ initController(/* viewIsAttached= */ true);
+ verify(mSysuiStatusBarStateController).addCallback(
+ mStateListenerArgumentCaptor.capture(), anyInt());
+
+ StatusBarStateController.StateListener stateListener =
+ mStateListenerArgumentCaptor.getValue();
+
+ stateListener.onStatePostChange();
+ verify(mNotificationStackScrollLayout).updateSensitiveness(false, true);
+ }
+
+ @Test
+ @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING)
+ public void testOnProjectionStateChanged_verifyNotSensitive() {
+ when(mNotificationLockscreenUserManager.isAnyProfilePublicMode()).thenReturn(false);
+ when(mSensitiveNotificationProtectionController.isSensitiveStateActive())
+ .thenReturn(false);
+
+ initController(/* viewIsAttached= */ true);
+ verify(mSensitiveNotificationProtectionController)
+ .registerSensitiveStateListener(mSensitiveStateListenerArgumentCaptor.capture());
+
+ mSensitiveStateListenerArgumentCaptor.getValue().run();
+
+ verify(mNotificationStackScrollLayout).updateSensitiveness(false, false);
+ }
+
+ @Test
+ @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING)
+ public void testOnProjectionStateChanged_verifyIfProfileIsPublic() {
+ when(mNotificationLockscreenUserManager.isAnyProfilePublicMode()).thenReturn(true);
+ when(mSensitiveNotificationProtectionController.isSensitiveStateActive()).thenReturn(false);
+
+ initController(/* viewIsAttached= */ true);
+ verify(mSensitiveNotificationProtectionController)
+ .registerSensitiveStateListener(mSensitiveStateListenerArgumentCaptor.capture());
+
+ mSensitiveStateListenerArgumentCaptor.getValue().run();
+
+ verify(mNotificationStackScrollLayout).updateSensitiveness(false, true);
+ }
+
+ @Test
+ @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING)
+ public void testOnProjectionStateChanged_verifyIfSensitiveActive() {
+ when(mNotificationLockscreenUserManager.isAnyProfilePublicMode()).thenReturn(false);
+ when(mSensitiveNotificationProtectionController.isSensitiveStateActive()).thenReturn(true);
+
+ initController(/* viewIsAttached= */ true);
+ verify(mSensitiveNotificationProtectionController)
+ .registerSensitiveStateListener(mSensitiveStateListenerArgumentCaptor.capture());
+
+ mSensitiveStateListenerArgumentCaptor.getValue().run();
+
+ verify(mNotificationStackScrollLayout).updateSensitiveness(false, true);
+ }
+
+ @Test
public void testOnMenuShownLogging() {
ExpandableNotificationRow row = mock(ExpandableNotificationRow.class, RETURNS_DEEP_STUBS);
when(row.getEntry().getSbn().getLogMaker()).thenReturn(new LogMaker(
@@ -571,9 +860,6 @@
when(mNotificationStackScrollLayout.onKeyguard()).thenReturn(true);
mController.getNotifStackController().setNotifStats(NotifStats.getEmpty());
- // WHEN: call updateImportantForAccessibility
- mController.updateImportantForAccessibility();
-
// THEN: mNotificationStackScrollLayout should not be important for A11y
verify(mNotificationStackScrollLayout)
.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
@@ -595,9 +881,6 @@
/* hasClearableSilentNotifs = */ false)
);
- // WHEN: call updateImportantForAccessibility
- mController.updateImportantForAccessibility();
-
// THEN: mNotificationStackScrollLayout should be important for A11y
verify(mNotificationStackScrollLayout)
.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES);
@@ -619,9 +902,6 @@
/* hasClearableSilentNotifs = */ false)
);
- // WHEN: call updateImportantForAccessibility
- mController.updateImportantForAccessibility();
-
// THEN: mNotificationStackScrollLayout should be important for A11y
verify(mNotificationStackScrollLayout)
.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES);
@@ -636,9 +916,6 @@
when(mNotificationStackScrollLayout.onKeyguard()).thenReturn(false);
mController.getNotifStackController().setNotifStats(NotifStats.getEmpty());
- // WHEN: call updateImportantForAccessibility
- mController.updateImportantForAccessibility();
-
// THEN: mNotificationStackScrollLayout should be important for A11y
verify(mNotificationStackScrollLayout)
.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES);
@@ -666,6 +943,20 @@
verify(mNotificationStackScrollLayout).updateEmptyShadeView(eq(false), anyBoolean());
}
+ @Test
+ @DisableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING)
+ public void sensitiveNotificationProtectionControllerListenerNotRegistered() {
+ initController(/* viewIsAttached= */ true);
+ verifyZeroInteractions(mSensitiveNotificationProtectionController);
+ }
+
+ @Test
+ @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING)
+ public void sensitiveNotificationProtectionControllerListenerRegistered() {
+ initController(/* viewIsAttached= */ true);
+ verify(mSensitiveNotificationProtectionController).registerSensitiveStateListener(any());
+ }
+
private LogMaker logMatcher(int category, int type) {
return argThat(new LogMatcher(category, type));
}
@@ -744,7 +1035,8 @@
mSecureSettings,
mock(NotificationDismissibilityProvider.class),
mActivityStarter,
- new ResourcesSplitShadeStateController());
+ new ResourcesSplitShadeStateController(),
+ mSensitiveNotificationProtectionController);
}
static class LogMatcher implements ArgumentMatcher<LogMaker> {
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 a824bc4..32c727c 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
@@ -25,6 +25,9 @@
import com.android.systemui.SysuiTestCase
import com.android.systemui.common.shared.model.NotificationContainerBounds
import com.android.systemui.common.ui.data.repository.fakeConfigurationRepository
+import com.android.systemui.communal.domain.interactor.communalInteractor
+import com.android.systemui.communal.shared.model.CommunalSceneKey
+import com.android.systemui.communal.shared.model.ObservableCommunalTransitionState
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.flags.Flags
import com.android.systemui.flags.fakeFeatureFlagsClassic
@@ -33,50 +36,64 @@
import com.android.systemui.keyguard.domain.interactor.keyguardInteractor
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.keyguard.shared.model.StatusBarState
-import com.android.systemui.keyguard.shared.model.StatusBarState.SHADE_LOCKED
import com.android.systemui.keyguard.shared.model.TransitionState
import com.android.systemui.keyguard.shared.model.TransitionStep
+import com.android.systemui.keyguard.ui.viewmodel.AodBurnInViewModel
+import com.android.systemui.keyguard.ui.viewmodel.BurnInParameters
+import com.android.systemui.keyguard.ui.viewmodel.aodBurnInViewModel
import com.android.systemui.keyguard.ui.viewmodel.keyguardRootViewModel
import com.android.systemui.kosmos.testScope
import com.android.systemui.res.R
import com.android.systemui.shade.data.repository.shadeRepository
-import com.android.systemui.shade.largeScreenHeaderHelper
import com.android.systemui.shade.mockLargeScreenHeaderHelper
import com.android.systemui.statusbar.notification.stack.domain.interactor.sharedNotificationContainerInteractor
import com.android.systemui.testKosmos
+import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.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.Mockito.mock
@SmallTest
@RunWith(AndroidJUnit4::class)
class SharedNotificationContainerViewModelTest : SysuiTestCase() {
+ val aodBurnInViewModel = mock(AodBurnInViewModel::class.java)
+ lateinit var translationYFlow: MutableStateFlow<Float>
val kosmos =
testKosmos().apply {
fakeFeatureFlagsClassic.apply { set(Flags.FULL_SCREEN_USER_SWITCHER, false) }
}
+
+ init {
+ kosmos.aodBurnInViewModel = aodBurnInViewModel
+ }
val testScope = kosmos.testScope
val configurationRepository = kosmos.fakeConfigurationRepository
val keyguardRepository = kosmos.fakeKeyguardRepository
val keyguardInteractor = kosmos.keyguardInteractor
val keyguardRootViewModel = kosmos.keyguardRootViewModel
val keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository
+ val communalInteractor = kosmos.communalInteractor
val shadeRepository = kosmos.shadeRepository
val sharedNotificationContainerInteractor = kosmos.sharedNotificationContainerInteractor
val largeScreenHeaderHelper = kosmos.mockLargeScreenHeaderHelper
- val underTest = kosmos.sharedNotificationContainerViewModel
+ lateinit var underTest: SharedNotificationContainerViewModel
@Before
fun setUp() {
overrideResource(R.bool.config_use_split_notification_shade, false)
+ translationYFlow = MutableStateFlow(0f)
+ whenever(aodBurnInViewModel.translationY(any())).thenReturn(translationYFlow)
+ underTest = kosmos.sharedNotificationContainerViewModel
}
@Test
@@ -214,6 +231,61 @@
}
@Test
+ fun glanceableHubAlpha() =
+ testScope.runTest {
+ val alpha by collectLastValue(underTest.glanceableHubAlpha)
+
+ // Start on lockscreen
+ showLockscreen()
+ assertThat(alpha).isEqualTo(1f)
+
+ // Start transitioning to glanceable hub
+ val progress = 0.6f
+ keyguardTransitionRepository.sendTransitionStep(
+ TransitionStep(
+ transitionState = TransitionState.STARTED,
+ from = KeyguardState.LOCKSCREEN,
+ to = KeyguardState.GLANCEABLE_HUB,
+ value = 0f,
+ )
+ )
+ runCurrent()
+ keyguardTransitionRepository.sendTransitionStep(
+ TransitionStep(
+ transitionState = TransitionState.RUNNING,
+ from = KeyguardState.LOCKSCREEN,
+ to = KeyguardState.GLANCEABLE_HUB,
+ value = progress,
+ )
+ )
+ runCurrent()
+ // Expected alpha is inverse of progress as notifications are fading away
+ assertThat(alpha).isEqualTo(1 - progress)
+
+ // Finish transition to glanceable hub
+ keyguardTransitionRepository.sendTransitionStep(
+ TransitionStep(
+ transitionState = TransitionState.FINISHED,
+ from = KeyguardState.LOCKSCREEN,
+ to = KeyguardState.GLANCEABLE_HUB,
+ value = 1f,
+ )
+ )
+ val idleTransitionState =
+ MutableStateFlow<ObservableCommunalTransitionState>(
+ ObservableCommunalTransitionState.Idle(CommunalSceneKey.Communal)
+ )
+ communalInteractor.setTransitionState(idleTransitionState)
+ runCurrent()
+ assertThat(alpha).isEqualTo(0f)
+
+ // While state is GLANCEABLE_HUB, verify alpha is restored to full if glanceable hub is
+ // not fully visible.
+ shadeRepository.setLockscreenShadeExpansion(0.1f)
+ assertThat(alpha).isEqualTo(1f)
+ }
+
+ @Test
fun validateMarginTop() =
testScope.runTest {
overrideResource(R.bool.config_use_large_screen_shade_header, false)
@@ -302,6 +374,43 @@
}
@Test
+ fun isOnGlanceableHubWithoutShade() =
+ testScope.runTest {
+ val isOnGlanceableHubWithoutShade by
+ collectLastValue(underTest.isOnGlanceableHubWithoutShade)
+
+ // Start on lockscreen
+ showLockscreen()
+ assertThat(isOnGlanceableHubWithoutShade).isFalse()
+
+ // Move to glanceable hub
+ val idleTransitionState =
+ MutableStateFlow<ObservableCommunalTransitionState>(
+ ObservableCommunalTransitionState.Idle(CommunalSceneKey.Communal)
+ )
+ communalInteractor.setTransitionState(idleTransitionState)
+ runCurrent()
+ assertThat(isOnGlanceableHubWithoutShade).isTrue()
+
+ // While state is GLANCEABLE_HUB, validate variations of both shade and qs expansion
+ shadeRepository.setLockscreenShadeExpansion(0.1f)
+ shadeRepository.setQsExpansion(0f)
+ assertThat(isOnGlanceableHubWithoutShade).isFalse()
+
+ shadeRepository.setLockscreenShadeExpansion(0.1f)
+ shadeRepository.setQsExpansion(0.1f)
+ assertThat(isOnGlanceableHubWithoutShade).isFalse()
+
+ shadeRepository.setLockscreenShadeExpansion(0f)
+ shadeRepository.setQsExpansion(0.1f)
+ assertThat(isOnGlanceableHubWithoutShade).isFalse()
+
+ shadeRepository.setLockscreenShadeExpansion(0f)
+ shadeRepository.setQsExpansion(0f)
+ assertThat(isOnGlanceableHubWithoutShade).isTrue()
+ }
+
+ @Test
fun boundsOnLockscreenNotInSplitShade() =
testScope.runTest {
val bounds by collectLastValue(underTest.bounds)
@@ -484,9 +593,21 @@
}
@Test
+ fun translationYUpdatesOnKeyguardForBurnIn() =
+ testScope.runTest {
+ val translationY by collectLastValue(underTest.translationY(BurnInParameters()))
+
+ showLockscreen()
+ assertThat(translationY).isEqualTo(0)
+
+ translationYFlow.value = 150f
+ assertThat(translationY).isEqualTo(150f)
+ }
+
+ @Test
fun translationYUpdatesOnKeyguard() =
testScope.runTest {
- val translationY by collectLastValue(underTest.translationY)
+ val translationY by collectLastValue(underTest.translationY(BurnInParameters()))
configurationRepository.setDimensionPixelSize(
R.dimen.keyguard_translate_distance_on_swipe_up,
@@ -506,7 +627,7 @@
@Test
fun translationYDoesNotUpdateWhenShadeIsExpanded() =
testScope.runTest {
- val translationY by collectLastValue(underTest.translationY)
+ val translationY by collectLastValue(underTest.translationY(BurnInParameters()))
configurationRepository.setDimensionPixelSize(
R.dimen.keyguard_translate_distance_on_swipe_up,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java
index 4827c92..b3a47d7 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java
@@ -48,7 +48,6 @@
import android.content.res.ColorStateList;
import android.content.res.TypedArray;
import android.graphics.Color;
-import android.os.Handler;
import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper;
import android.util.MathUtils;
@@ -66,6 +65,7 @@
import com.android.systemui.bouncer.shared.constants.KeyguardBouncerConstants;
import com.android.systemui.dock.DockManager;
import com.android.systemui.keyguard.KeyguardUnlockAnimationController;
+import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor;
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor;
import com.android.systemui.keyguard.shared.model.KeyguardState;
import com.android.systemui.keyguard.shared.model.TransitionState;
@@ -134,7 +134,7 @@
@Mock private AlarmManager mAlarmManager;
@Mock private DozeParameters mDozeParameters;
@Mock private LightBarController mLightBarController;
- @Mock private DelayedWakeLock.Builder mDelayedWakeLockBuilder;
+ @Mock private DelayedWakeLock.Factory mDelayedWakeLockFactory;
@Mock private DelayedWakeLock mWakeLock;
@Mock private KeyguardStateController mKeyguardStateController;
@Mock private KeyguardUpdateMonitor mKeyguardUpdateMonitor;
@@ -145,6 +145,7 @@
@Mock private AlternateBouncerToGoneTransitionViewModel
mAlternateBouncerToGoneTransitionViewModel;
@Mock private KeyguardTransitionInteractor mKeyguardTransitionInteractor;
+ @Mock private KeyguardInteractor mKeyguardInteractor;
private final FakeWallpaperRepository mWallpaperRepository = new FakeWallpaperRepository();
@Mock private CoroutineDispatcher mMainDispatcher;
@Mock private TypedArray mMockTypedArray;
@@ -260,11 +261,7 @@
}).when(mLightBarController).setScrimState(
any(ScrimState.class), anyFloat(), any(GradientColors.class));
- when(mDelayedWakeLockBuilder.setHandler(any(Handler.class)))
- .thenReturn(mDelayedWakeLockBuilder);
- when(mDelayedWakeLockBuilder.setTag(any(String.class)))
- .thenReturn(mDelayedWakeLockBuilder);
- when(mDelayedWakeLockBuilder.build()).thenReturn(mWakeLock);
+ when(mDelayedWakeLockFactory.create(any(String.class))).thenReturn(mWakeLock);
when(mDockManager.isDocked()).thenReturn(false);
when(mKeyguardTransitionInteractor.transition(any(), any()))
@@ -279,7 +276,7 @@
mDozeParameters,
mAlarmManager,
mKeyguardStateController,
- mDelayedWakeLockBuilder,
+ mDelayedWakeLockFactory,
new FakeHandler(mLooper.getLooper()),
mKeyguardUpdateMonitor,
mDockManager,
@@ -292,6 +289,7 @@
mPrimaryBouncerToGoneTransitionViewModel,
mAlternateBouncerToGoneTransitionViewModel,
mKeyguardTransitionInteractor,
+ mKeyguardInteractor,
mWallpaperRepository,
mMainDispatcher,
mLinearLargeScreenShadeInterpolator);
@@ -987,7 +985,7 @@
mDozeParameters,
mAlarmManager,
mKeyguardStateController,
- mDelayedWakeLockBuilder,
+ mDelayedWakeLockFactory,
new FakeHandler(mLooper.getLooper()),
mKeyguardUpdateMonitor,
mDockManager,
@@ -1000,6 +998,7 @@
mPrimaryBouncerToGoneTransitionViewModel,
mAlternateBouncerToGoneTransitionViewModel,
mKeyguardTransitionInteractor,
+ mKeyguardInteractor,
mWallpaperRepository,
mMainDispatcher,
mLinearLargeScreenShadeInterpolator);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt
index 20d5c5d..49953a1 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt
@@ -16,6 +16,7 @@
package com.android.systemui.statusbar.pipeline.mobile.domain.interactor
+import android.platform.test.annotations.EnableFlags
import android.telephony.CellSignalStrength
import android.telephony.SubscriptionManager.PROFILE_CLASS_UNSET
import android.telephony.TelephonyManager.NETWORK_TYPE_UNKNOWN
@@ -159,10 +160,13 @@
}
@Test
- fun numberOfLevels_comesFromRepo() =
+ fun numberOfLevels_comesFromRepo_whenApplicable() =
testScope.runTest {
var latest: Int? = null
- val job = underTest.signalLevelIcon.onEach { latest = it.numberOfLevels }.launchIn(this)
+ val job =
+ underTest.signalLevelIcon
+ .onEach { latest = (it as? SignalIconModel.Cellular)?.numberOfLevels }
+ .launchIn(this)
connectionRepository.numberOfLevels.value = 5
assertThat(latest).isEqualTo(5)
@@ -491,14 +495,19 @@
}
@Test
- fun iconId_correctLevel_notCutout() =
+ fun cellBasedIconId_correctLevel_notCutout() =
testScope.runTest {
+ connectionRepository.isNonTerrestrial.value = false
connectionRepository.isInService.value = true
connectionRepository.primaryLevel.value = 1
connectionRepository.setDataEnabled(false)
+ connectionRepository.isNonTerrestrial.value = false
- var latest: SignalIconModel? = null
- val job = underTest.signalLevelIcon.onEach { latest = it }.launchIn(this)
+ var latest: SignalIconModel.Cellular? = null
+ val job =
+ underTest.signalLevelIcon
+ .onEach { latest = it as? SignalIconModel.Cellular }
+ .launchIn(this)
assertThat(latest?.level).isEqualTo(1)
assertThat(latest?.showExclamationMark).isFalse()
@@ -509,6 +518,7 @@
@Test
fun icon_usesLevelFromInteractor() =
testScope.runTest {
+ connectionRepository.isNonTerrestrial.value = false
connectionRepository.isInService.value = true
var latest: SignalIconModel? = null
@@ -524,10 +534,15 @@
}
@Test
- fun icon_usesNumberOfLevelsFromInteractor() =
+ fun cellBasedIcon_usesNumberOfLevelsFromInteractor() =
testScope.runTest {
- var latest: SignalIconModel? = null
- val job = underTest.signalLevelIcon.onEach { latest = it }.launchIn(this)
+ connectionRepository.isNonTerrestrial.value = false
+
+ var latest: SignalIconModel.Cellular? = null
+ val job =
+ underTest.signalLevelIcon
+ .onEach { latest = it as? SignalIconModel.Cellular }
+ .launchIn(this)
connectionRepository.numberOfLevels.value = 5
assertThat(latest!!.numberOfLevels).isEqualTo(5)
@@ -539,12 +554,16 @@
}
@Test
- fun icon_defaultDataDisabled_showExclamationTrue() =
+ fun cellBasedIcon_defaultDataDisabled_showExclamationTrue() =
testScope.runTest {
+ connectionRepository.isNonTerrestrial.value = false
mobileIconsInteractor.activeDataConnectionHasDataEnabled.value = false
- var latest: SignalIconModel? = null
- val job = underTest.signalLevelIcon.onEach { latest = it }.launchIn(this)
+ var latest: SignalIconModel.Cellular? = null
+ val job =
+ underTest.signalLevelIcon
+ .onEach { latest = it as? SignalIconModel.Cellular }
+ .launchIn(this)
assertThat(latest!!.showExclamationMark).isTrue()
@@ -552,12 +571,16 @@
}
@Test
- fun icon_defaultConnectionFailed_showExclamationTrue() =
+ fun cellBasedIcon_defaultConnectionFailed_showExclamationTrue() =
testScope.runTest {
+ connectionRepository.isNonTerrestrial.value = false
mobileIconsInteractor.isDefaultConnectionFailed.value = true
- var latest: SignalIconModel? = null
- val job = underTest.signalLevelIcon.onEach { latest = it }.launchIn(this)
+ var latest: SignalIconModel.Cellular? = null
+ val job =
+ underTest.signalLevelIcon
+ .onEach { latest = it as? SignalIconModel.Cellular }
+ .launchIn(this)
assertThat(latest!!.showExclamationMark).isTrue()
@@ -565,14 +588,18 @@
}
@Test
- fun icon_enabledAndNotFailed_showExclamationFalse() =
+ fun cellBasedIcon_enabledAndNotFailed_showExclamationFalse() =
testScope.runTest {
+ connectionRepository.isNonTerrestrial.value = false
connectionRepository.isInService.value = true
mobileIconsInteractor.activeDataConnectionHasDataEnabled.value = true
mobileIconsInteractor.isDefaultConnectionFailed.value = false
- var latest: SignalIconModel? = null
- val job = underTest.signalLevelIcon.onEach { latest = it }.launchIn(this)
+ var latest: SignalIconModel.Cellular? = null
+ val job =
+ underTest.signalLevelIcon
+ .onEach { latest = it as? SignalIconModel.Cellular }
+ .launchIn(this)
assertThat(latest!!.showExclamationMark).isFalse()
@@ -580,11 +607,15 @@
}
@Test
- fun icon_usesEmptyState_whenNotInService() =
+ fun cellBasedIcon_usesEmptyState_whenNotInService() =
testScope.runTest {
- var latest: SignalIconModel? = null
- val job = underTest.signalLevelIcon.onEach { latest = it }.launchIn(this)
+ var latest: SignalIconModel.Cellular? = null
+ val job =
+ underTest.signalLevelIcon
+ .onEach { latest = it as? SignalIconModel.Cellular }
+ .launchIn(this)
+ connectionRepository.isNonTerrestrial.value = false
connectionRepository.isInService.value = false
assertThat(latest?.level).isEqualTo(0)
@@ -604,11 +635,15 @@
}
@Test
- fun icon_usesCarrierNetworkState_whenInCarrierNetworkChangeMode() =
+ fun cellBasedIcon_usesCarrierNetworkState_whenInCarrierNetworkChangeMode() =
testScope.runTest {
- var latest: SignalIconModel? = null
- val job = underTest.signalLevelIcon.onEach { latest = it }.launchIn(this)
+ var latest: SignalIconModel.Cellular? = null
+ val job =
+ underTest.signalLevelIcon
+ .onEach { latest = it as? SignalIconModel.Cellular? }
+ .launchIn(this)
+ connectionRepository.isNonTerrestrial.value = false
connectionRepository.isInService.value = true
connectionRepository.carrierNetworkChangeActive.value = true
connectionRepository.primaryLevel.value = 1
@@ -626,6 +661,20 @@
job.cancel()
}
+ @EnableFlags(com.android.internal.telephony.flags.Flags.FLAG_CARRIER_ENABLED_SATELLITE_FLAG)
+ @Test
+ fun satBasedIcon_isUsedWhenNonTerrestrial() =
+ testScope.runTest {
+ val latest by collectLastValue(underTest.signalLevelIcon)
+
+ // Start off using cellular
+ assertThat(latest).isInstanceOf(SignalIconModel.Cellular::class.java)
+
+ connectionRepository.isNonTerrestrial.value = true
+
+ assertThat(latest).isInstanceOf(SignalIconModel.Satellite::class.java)
+ }
+
private fun createInteractor(
overrides: MobileIconCarrierIdOverrides = MobileIconCarrierIdOverridesImpl()
) =
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/model/SignalIconModelParameterizedTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/model/SignalIconModelParameterizedTest.kt
index 90a8946..ebec003 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/model/SignalIconModelParameterizedTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/model/SignalIconModelParameterizedTest.kt
@@ -32,7 +32,7 @@
@Test
fun drawableFromModel_level0_numLevels4_noExclamation_notCarrierNetworkChange() {
val model =
- SignalIconModel(
+ SignalIconModel.Cellular(
level = 0,
numberOfLevels = 4,
showExclamationMark = false,
@@ -59,7 +59,7 @@
val expected: Int,
) {
fun toSignalIconModel() =
- SignalIconModel(
+ SignalIconModel.Cellular(
level = level,
numberOfLevels = numberOfLevels,
showExclamationMark = showExclamation,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/LocationBasedMobileIconViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/LocationBasedMobileIconViewModelTest.kt
index 889130f..deb9fcf 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/LocationBasedMobileIconViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/LocationBasedMobileIconViewModelTest.kt
@@ -190,7 +190,7 @@
/** Convenience constructor for these tests */
fun defaultSignal(level: Int = 1): SignalIconModel {
- return SignalIconModel(
+ return SignalIconModel.Cellular(
level,
NUM_LEVELS,
showExclamationMark = false,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt
index 147efcb..83d0fe8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt
@@ -55,6 +55,7 @@
import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.test.TestScope
@@ -709,6 +710,87 @@
.isEqualTo(Icon.Resource(R.drawable.mobile_network_type_background, null))
}
+ @EnableFlags(com.android.internal.telephony.flags.Flags.FLAG_CARRIER_ENABLED_SATELLITE_FLAG)
+ @Test
+ fun nonTerrestrial_defaultProperties() =
+ testScope.runTest {
+ repository.isNonTerrestrial.value = true
+
+ val roaming by collectLastValue(underTest.roaming)
+ val networkTypeIcon by collectLastValue(underTest.networkTypeIcon)
+ val networkTypeBackground by collectLastValue(underTest.networkTypeBackground)
+ val activityInVisible by collectLastValue(underTest.activityInVisible)
+ val activityOutVisible by collectLastValue(underTest.activityOutVisible)
+ val activityContainerVisible by collectLastValue(underTest.activityContainerVisible)
+
+ assertThat(roaming).isFalse()
+ assertThat(networkTypeIcon).isNull()
+ assertThat(networkTypeBackground).isNull()
+ assertThat(activityInVisible).isFalse()
+ assertThat(activityOutVisible).isFalse()
+ assertThat(activityContainerVisible).isFalse()
+ }
+
+ @EnableFlags(com.android.internal.telephony.flags.Flags.FLAG_CARRIER_ENABLED_SATELLITE_FLAG)
+ @Test
+ fun nonTerrestrial_ignoresDefaultProperties() =
+ testScope.runTest {
+ repository.isNonTerrestrial.value = true
+
+ val roaming by collectLastValue(underTest.roaming)
+ val networkTypeIcon by collectLastValue(underTest.networkTypeIcon)
+ val networkTypeBackground by collectLastValue(underTest.networkTypeBackground)
+ val activityInVisible by collectLastValue(underTest.activityInVisible)
+ val activityOutVisible by collectLastValue(underTest.activityOutVisible)
+ val activityContainerVisible by collectLastValue(underTest.activityContainerVisible)
+
+ repository.setAllRoaming(true)
+ repository.setNetworkTypeKey(connectionsRepository.LTE_KEY)
+ // sets the background on cellular
+ repository.hasPrioritizedNetworkCapabilities.value = true
+ repository.dataActivityDirection.value =
+ DataActivityModel(
+ hasActivityIn = true,
+ hasActivityOut = true,
+ )
+
+ assertThat(roaming).isFalse()
+ assertThat(networkTypeIcon).isNull()
+ assertThat(networkTypeBackground).isNull()
+ assertThat(activityInVisible).isFalse()
+ assertThat(activityOutVisible).isFalse()
+ assertThat(activityContainerVisible).isFalse()
+ }
+
+ @EnableFlags(com.android.internal.telephony.flags.Flags.FLAG_CARRIER_ENABLED_SATELLITE_FLAG)
+ @Test
+ fun nonTerrestrial_usesSatelliteIcon() =
+ testScope.runTest {
+ repository.isNonTerrestrial.value = true
+ repository.setAllLevels(0)
+
+ val latest by
+ collectLastValue(underTest.icon.filterIsInstance(SignalIconModel.Satellite::class))
+
+ // Level 0 -> no connection
+ assertThat(latest).isNotNull()
+ assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_0)
+
+ // 1-2 -> 1 bar
+ repository.setAllLevels(1)
+ assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_1)
+
+ repository.setAllLevels(2)
+ assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_1)
+
+ // 3-4 -> 2 bars
+ repository.setAllLevels(3)
+ assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_2)
+
+ repository.setAllLevels(4)
+ assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_2)
+ }
+
private fun createAndSetViewModel() {
underTest =
MobileIconViewModel(
@@ -723,24 +805,5 @@
companion object {
private const val SUB_1_ID = 1
- private const val NUM_LEVELS = 4
-
- /** Convenience constructor for these tests */
- fun defaultSignal(level: Int = 1): SignalIconModel {
- return SignalIconModel(
- level,
- NUM_LEVELS,
- showExclamationMark = false,
- carrierNetworkChange = false,
- )
- }
-
- fun emptySignal(): SignalIconModel =
- SignalIconModel(
- level = 0,
- numberOfLevels = NUM_LEVELS,
- showExclamationMark = true,
- carrierNetworkChange = false,
- )
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImplTest.kt
index 0e0d489..5b5819d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImplTest.kt
@@ -125,22 +125,6 @@
}
@Test
- fun satelliteManagerThrows_doesNotCrash() =
- testScope.runTest {
- setupDefaultRepo()
-
- whenever(satelliteManager.registerForNtnSignalStrengthChanged(any(), any()))
- .thenThrow(SatelliteException(13))
-
- val conn by collectLastValue(underTest.connectionState)
- val strength by collectLastValue(underTest.signalStrength)
-
- // Flows have not emitted, we haven't crashed
- assertThat(conn).isNull()
- assertThat(strength).isNull()
- }
-
- @Test
fun connectionState_mapsFromSatelliteModemState() =
testScope.runTest {
setupDefaultRepo()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerTest.kt
new file mode 100644
index 0000000..cd5d5ed
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerTest.kt
@@ -0,0 +1,231 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.policy
+
+import android.app.Notification
+import android.media.projection.MediaProjectionInfo
+import android.media.projection.MediaProjectionManager
+import android.os.Handler
+import android.service.notification.StatusBarNotification
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.systemui.Flags
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.statusbar.notification.collection.NotificationEntry
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.ArgumentMatchers.any
+import org.mockito.Captor
+import org.mockito.Mock
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.reset
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.verifyNoMoreInteractions
+import org.mockito.Mockito.verifyZeroInteractions
+import org.mockito.Mockito.`when`
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class SensitiveNotificationProtectionControllerTest : SysuiTestCase() {
+ @Mock private lateinit var handler: Handler
+
+ @Mock private lateinit var mediaProjectionManager: MediaProjectionManager
+
+ @Mock private lateinit var mediaProjectionInfo: MediaProjectionInfo
+
+ @Mock private lateinit var listener1: Runnable
+ @Mock private lateinit var listener2: Runnable
+ @Mock private lateinit var listener3: Runnable
+
+ @Captor
+ private lateinit var mediaProjectionCallbackCaptor:
+ ArgumentCaptor<MediaProjectionManager.Callback>
+
+ private lateinit var controller: SensitiveNotificationProtectionControllerImpl
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+ mSetFlagsRule.enableFlags(Flags.FLAG_SCREENSHARE_NOTIFICATION_HIDING)
+
+ controller = SensitiveNotificationProtectionControllerImpl(mediaProjectionManager, handler)
+
+ // Obtain useful MediaProjectionCallback
+ verify(mediaProjectionManager).addCallback(mediaProjectionCallbackCaptor.capture(), any())
+ }
+
+ @Test
+ fun init_flagEnabled_registerMediaProjectionManagerCallback() {
+ assertNotNull(mediaProjectionCallbackCaptor.value)
+ }
+
+ @Test
+ fun init_flagDisabled_noRegisterMediaProjectionManagerCallback() {
+ mSetFlagsRule.disableFlags(Flags.FLAG_SCREENSHARE_NOTIFICATION_HIDING)
+ reset(mediaProjectionManager)
+
+ controller = SensitiveNotificationProtectionControllerImpl(mediaProjectionManager, handler)
+
+ verifyZeroInteractions(mediaProjectionManager)
+ }
+
+ @Test
+ fun registerSensitiveStateListener_singleListener() {
+ controller.registerSensitiveStateListener(listener1)
+
+ mediaProjectionCallbackCaptor.value.onStart(mediaProjectionInfo)
+ mediaProjectionCallbackCaptor.value.onStop(mediaProjectionInfo)
+
+ verify(listener1, times(2)).run()
+ }
+
+ @Test
+ fun registerSensitiveStateListener_multipleListeners() {
+ controller.registerSensitiveStateListener(listener1)
+ controller.registerSensitiveStateListener(listener2)
+
+ mediaProjectionCallbackCaptor.value.onStart(mediaProjectionInfo)
+ mediaProjectionCallbackCaptor.value.onStop(mediaProjectionInfo)
+
+ verify(listener1, times(2)).run()
+ verify(listener2, times(2)).run()
+ }
+
+ @Test
+ fun registerSensitiveStateListener_afterProjectionActive() {
+ mediaProjectionCallbackCaptor.value.onStart(mediaProjectionInfo)
+
+ controller.registerSensitiveStateListener(listener1)
+ verifyZeroInteractions(listener1)
+
+ mediaProjectionCallbackCaptor.value.onStop(mediaProjectionInfo)
+
+ verify(listener1).run()
+ }
+
+ @Test
+ fun unregisterSensitiveStateListener_singleListener() {
+ controller.registerSensitiveStateListener(listener1)
+
+ mediaProjectionCallbackCaptor.value.onStart(mediaProjectionInfo)
+ mediaProjectionCallbackCaptor.value.onStop(mediaProjectionInfo)
+
+ verify(listener1, times(2)).run()
+
+ controller.unregisterSensitiveStateListener(listener1)
+
+ mediaProjectionCallbackCaptor.value.onStart(mediaProjectionInfo)
+ mediaProjectionCallbackCaptor.value.onStop(mediaProjectionInfo)
+
+ verifyNoMoreInteractions(listener1)
+ }
+
+ @Test
+ fun unregisterSensitiveStateListener_multipleListeners() {
+ controller.registerSensitiveStateListener(listener1)
+ controller.registerSensitiveStateListener(listener2)
+ controller.registerSensitiveStateListener(listener3)
+
+ mediaProjectionCallbackCaptor.value.onStart(mediaProjectionInfo)
+ mediaProjectionCallbackCaptor.value.onStop(mediaProjectionInfo)
+
+ verify(listener1, times(2)).run()
+ verify(listener2, times(2)).run()
+ verify(listener3, times(2)).run()
+
+ controller.unregisterSensitiveStateListener(listener1)
+ controller.unregisterSensitiveStateListener(listener2)
+
+ mediaProjectionCallbackCaptor.value.onStart(mediaProjectionInfo)
+ mediaProjectionCallbackCaptor.value.onStop(mediaProjectionInfo)
+
+ verifyNoMoreInteractions(listener1)
+ verifyNoMoreInteractions(listener2)
+ verify(listener3, times(4)).run()
+ }
+
+ @Test
+ fun isSensitiveStateActive_projectionInactive_false() {
+ assertFalse(controller.isSensitiveStateActive)
+ }
+
+ @Test
+ fun isSensitiveStateActive_projectionActive_true() {
+ mediaProjectionCallbackCaptor.value.onStart(mediaProjectionInfo)
+
+ assertTrue(controller.isSensitiveStateActive)
+ }
+
+ @Test
+ fun isSensitiveStateActive_projectionInactiveAfterActive_false() {
+ mediaProjectionCallbackCaptor.value.onStart(mediaProjectionInfo)
+ mediaProjectionCallbackCaptor.value.onStop(mediaProjectionInfo)
+
+ assertFalse(controller.isSensitiveStateActive)
+ }
+
+ @Test
+ fun isSensitiveStateActive_projectionActiveAfterInactive_true() {
+ mediaProjectionCallbackCaptor.value.onStart(mediaProjectionInfo)
+ mediaProjectionCallbackCaptor.value.onStop(mediaProjectionInfo)
+ mediaProjectionCallbackCaptor.value.onStart(mediaProjectionInfo)
+
+ assertTrue(controller.isSensitiveStateActive)
+ }
+
+ @Test
+ fun shouldProtectNotification_projectionInactive_false() {
+ val notificationEntry = mock(NotificationEntry::class.java)
+
+ assertFalse(controller.shouldProtectNotification(notificationEntry))
+ }
+
+ @Test
+ fun shouldProtectNotification_projectionActive_fgsNotification_false() {
+ mediaProjectionCallbackCaptor.value.onStart(mediaProjectionInfo)
+
+ val notificationEntry = mock(NotificationEntry::class.java)
+ val sbn = mock(StatusBarNotification::class.java)
+ val notification = mock(Notification::class.java)
+ `when`(notificationEntry.sbn).thenReturn(sbn)
+ `when`(sbn.notification).thenReturn(notification)
+ `when`(notification.isFgsOrUij).thenReturn(true)
+
+ assertFalse(controller.shouldProtectNotification(notificationEntry))
+ }
+
+ @Test
+ fun shouldProtectNotification_projectionActive_notFgsNotification_true() {
+ mediaProjectionCallbackCaptor.value.onStart(mediaProjectionInfo)
+
+ val notificationEntry = mock(NotificationEntry::class.java)
+ val sbn = mock(StatusBarNotification::class.java)
+ val notification = mock(Notification::class.java)
+ `when`(notificationEntry.sbn).thenReturn(sbn)
+ `when`(sbn.notification).thenReturn(notification)
+ `when`(notification.isFgsOrUij).thenReturn(false)
+
+ assertTrue(controller.shouldProtectNotification(notificationEntry))
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/theme/ThemeOverlayControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/theme/ThemeOverlayControllerTest.java
index b58a41c..457acd2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/theme/ThemeOverlayControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/theme/ThemeOverlayControllerTest.java
@@ -190,7 +190,7 @@
mWakefulnessLifecycle.dispatchFinishedWakingUp();
mThemeOverlayController.start();
- verify(mUserTracker).addCallback(mUserTrackerCallback.capture(), eq(mMainExecutor));
+ verify(mUserTracker).addCallback(mUserTrackerCallback.capture(), eq(mBgExecutor));
verify(mWallpaperManager).addOnColorsChangedListener(mColorsListener.capture(), eq(null),
eq(UserHandle.USER_ALL));
verify(mBroadcastDispatcher).registerReceiver(mBroadcastReceiver.capture(), any(),
diff --git a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java
index 8c823b2..7a8dce8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java
@@ -32,6 +32,7 @@
import static org.junit.Assume.assumeNotNull;
import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
@@ -63,15 +64,16 @@
import com.android.internal.jank.InteractionJankMonitor;
import com.android.internal.logging.testing.UiEventLoggerFake;
+import com.android.keyguard.TestScopeProvider;
import com.android.systemui.Prefs;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.animation.AnimatorTestRule;
import com.android.systemui.dump.DumpManager;
import com.android.systemui.media.dialog.MediaOutputDialogFactory;
-import com.android.systemui.plugins.ActivityStarter;
import com.android.systemui.plugins.VolumeDialogController;
import com.android.systemui.plugins.VolumeDialogController.State;
import com.android.systemui.res.R;
+import com.android.systemui.statusbar.VibratorHelper;
import com.android.systemui.statusbar.policy.AccessibilityManagerWrapper;
import com.android.systemui.statusbar.policy.ConfigurationController;
import com.android.systemui.statusbar.policy.DevicePostureController;
@@ -79,6 +81,9 @@
import com.android.systemui.statusbar.policy.FakeConfigurationController;
import com.android.systemui.util.settings.FakeSettings;
import com.android.systemui.util.settings.SecureSettings;
+import com.android.systemui.util.time.FakeSystemClock;
+import com.android.systemui.volume.domain.interactor.VolumePanelNavigationInteractor;
+import com.android.systemui.volume.ui.navigation.VolumeNavigator;
import dagger.Lazy;
@@ -97,6 +102,8 @@
import java.util.Arrays;
import java.util.function.Predicate;
+import kotlinx.coroutines.Dispatchers;
+
@SmallTest
@RunWith(AndroidTestingRunner.class)
@TestableLooper.RunWithLooper(setAsMainLooper = true)
@@ -126,10 +133,6 @@
@Mock
MediaOutputDialogFactory mMediaOutputDialogFactory;
@Mock
- VolumePanelFactory mVolumePanelFactory;
- @Mock
- ActivityStarter mActivityStarter;
- @Mock
InteractionJankMonitor mInteractionJankMonitor;
@Mock
private DumpManager mDumpManager;
@@ -138,6 +141,10 @@
DevicePostureController mPostureController;
@Mock
private Lazy<SecureSettings> mLazySecureSettings;
+ @Mock
+ private VolumePanelNavigationInteractor mVolumePanelNavigationInteractor;
+ @Mock
+ private VolumeNavigator mVolumeNavigator;
private final CsdWarningDialog.Factory mCsdWarningDialogFactory =
new CsdWarningDialog.Factory() {
@@ -146,6 +153,8 @@
return mCsdWarningDialog;
}
};
+ @Mock
+ private VibratorHelper mVibratorHelper;
private int mLongestHideShowAnimationDuration = 250;
private FakeSettings mSecureSettings;
@@ -180,6 +189,8 @@
when(mLazySecureSettings.get()).thenReturn(mSecureSettings);
+ when(mVibratorHelper.getPrimitiveDurations(anyInt())).thenReturn(new int[]{0});
+
mDialog = new VolumeDialogImpl(
getContext(),
mVolumeDialogController,
@@ -187,15 +198,19 @@
mDeviceProvisionedController,
mConfigurationController,
mMediaOutputDialogFactory,
- mVolumePanelFactory,
- mActivityStarter,
mInteractionJankMonitor,
+ mVolumePanelNavigationInteractor,
+ mVolumeNavigator,
false,
mCsdWarningDialogFactory,
mPostureController,
mTestableLooper.getLooper(),
mDumpManager,
- mLazySecureSettings);
+ mLazySecureSettings,
+ mVibratorHelper,
+ Dispatchers.getUnconfined(),
+ TestScopeProvider.getTestScope(),
+ new FakeSystemClock());
mDialog.init(0, null);
State state = createShellState();
mDialog.onStateChangedH(state);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
index 744f424..98f3ede 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
@@ -456,6 +456,7 @@
powerInteractor,
new GlanceableHubTransitions(
mTestScope,
+ mKosmos.getTestDispatcher(),
keyguardTransitionInteractor,
keyguardTransitionRepository,
communalInteractor
@@ -477,6 +478,7 @@
mKosmos.getTestDispatcher(),
mKosmos.getTestDispatcher(),
keyguardInteractor,
+ communalInteractor,
featureFlags,
mock(KeyguardSecurityModel.class),
mSelectedUserInteractor,
@@ -534,7 +536,8 @@
mShadeWindowLogger,
() -> mSelectedUserInteractor,
mUserTracker,
- mSceneContainerFlags
+ mSceneContainerFlags,
+ mKosmos::getCommunalInteractor
);
mNotificationShadeWindowController.fetchWindowRootView();
mNotificationShadeWindowController.attach();
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/VolumePanelComponentKey.kt b/packages/SystemUI/tests/utils/src/android/app/KeyguardManagerKosmos.kt
similarity index 76%
copy from packages/SystemUI/src/com/android/systemui/volume/panel/VolumePanelComponentKey.kt
copy to packages/SystemUI/tests/utils/src/android/app/KeyguardManagerKosmos.kt
index 22a74d2..e5121d5 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/VolumePanelComponentKey.kt
+++ b/packages/SystemUI/tests/utils/src/android/app/KeyguardManagerKosmos.kt
@@ -14,7 +14,9 @@
* limitations under the License.
*/
-package com.android.systemui.volume.panel
+package android.app
-/** Uniquely identifies the [com.android.systemui.volume.panel.ui.VolumePanelComponent]. */
-typealias VolumePanelComponentKey = String
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.util.mockito.mock
+
+val Kosmos.keyguardManager by Kosmos.Fixture { mock<KeyguardManager>() }
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/VolumePanelComponentKey.kt b/packages/SystemUI/tests/utils/src/android/os/HandlerKosmos.kt
similarity index 69%
copy from packages/SystemUI/src/com/android/systemui/volume/panel/VolumePanelComponentKey.kt
copy to packages/SystemUI/tests/utils/src/android/os/HandlerKosmos.kt
index 22a74d2..4e2683b 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/VolumePanelComponentKey.kt
+++ b/packages/SystemUI/tests/utils/src/android/os/HandlerKosmos.kt
@@ -14,7 +14,10 @@
* limitations under the License.
*/
-package com.android.systemui.volume.panel
+package android.os
-/** Uniquely identifies the [com.android.systemui.volume.panel.ui.VolumePanelComponent]. */
-typealias VolumePanelComponentKey = String
+import com.android.systemui.concurrency.fakeExecutor
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.util.concurrency.mockExecutorHandler
+
+val Kosmos.fakeExecutorHandler by Kosmos.Fixture { mockExecutorHandler(fakeExecutor) }
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/VolumePanelComponentKey.kt b/packages/SystemUI/tests/utils/src/android/service/dream/DreamManagerKosmos.kt
similarity index 72%
copy from packages/SystemUI/src/com/android/systemui/volume/panel/VolumePanelComponentKey.kt
copy to packages/SystemUI/tests/utils/src/android/service/dream/DreamManagerKosmos.kt
index 22a74d2..fb51f0f 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/VolumePanelComponentKey.kt
+++ b/packages/SystemUI/tests/utils/src/android/service/dream/DreamManagerKosmos.kt
@@ -14,7 +14,10 @@
* limitations under the License.
*/
-package com.android.systemui.volume.panel
+package android.service.dream
-/** Uniquely identifies the [com.android.systemui.volume.panel.ui.VolumePanelComponent]. */
-typealias VolumePanelComponentKey = String
+import android.service.dreams.IDreamManager
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.util.mockito.mock
+
+var Kosmos.dreamManager by Kosmos.Fixture { mock<IDreamManager>() }
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/VolumePanelComponentKey.kt b/packages/SystemUI/tests/utils/src/android/view/WindowManagerKosmos.kt
similarity index 69%
copy from packages/SystemUI/src/com/android/systemui/volume/panel/VolumePanelComponentKey.kt
copy to packages/SystemUI/tests/utils/src/android/view/WindowManagerKosmos.kt
index 22a74d2..2a05598 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/VolumePanelComponentKey.kt
+++ b/packages/SystemUI/tests/utils/src/android/view/WindowManagerKosmos.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2023 The Android Open Source Project
+ * Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,7 +14,9 @@
* limitations under the License.
*/
-package com.android.systemui.volume.panel
+package android.view
-/** Uniquely identifies the [com.android.systemui.volume.panel.ui.VolumePanelComponent]. */
-typealias VolumePanelComponentKey = String
+import com.android.systemui.kosmos.Kosmos
+import org.mockito.Mockito.mock
+
+val Kosmos.windowManager by Kosmos.Fixture<WindowManager> { mock(WindowManager::class.java) }
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/VolumePanelComponentKey.kt b/packages/SystemUI/tests/utils/src/com/android/internal/widget/LockPatternUtilsKosmos.kt
similarity index 75%
copy from packages/SystemUI/src/com/android/systemui/volume/panel/VolumePanelComponentKey.kt
copy to packages/SystemUI/tests/utils/src/com/android/internal/widget/LockPatternUtilsKosmos.kt
index 22a74d2..d9ea5e9 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/VolumePanelComponentKey.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/internal/widget/LockPatternUtilsKosmos.kt
@@ -14,7 +14,9 @@
* limitations under the License.
*/
-package com.android.systemui.volume.panel
+package com.android.internal.widget
-/** Uniquely identifies the [com.android.systemui.volume.panel.ui.VolumePanelComponent]. */
-typealias VolumePanelComponentKey = String
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.util.mockito.mock
+
+var Kosmos.lockPatternUtils by Kosmos.Fixture { mock<LockPatternUtils>() }
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/VolumePanelComponentKey.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/ActivityIntentHelperKosmos.kt
similarity index 74%
copy from packages/SystemUI/src/com/android/systemui/volume/panel/VolumePanelComponentKey.kt
copy to packages/SystemUI/tests/utils/src/com/android/systemui/ActivityIntentHelperKosmos.kt
index 22a74d2..7185b7c 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/VolumePanelComponentKey.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/ActivityIntentHelperKosmos.kt
@@ -14,7 +14,9 @@
* limitations under the License.
*/
-package com.android.systemui.volume.panel
+package com.android.systemui
-/** Uniquely identifies the [com.android.systemui.volume.panel.ui.VolumePanelComponent]. */
-typealias VolumePanelComponentKey = String
+import android.content.applicationContext
+import com.android.systemui.kosmos.Kosmos
+
+val Kosmos.activityIntentHelper by Kosmos.Fixture { ActivityIntentHelper(applicationContext) }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/accessibility/data/repository/FakeAccessibilityQsShortcutsRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/accessibility/data/repository/FakeAccessibilityQsShortcutsRepository.kt
new file mode 100644
index 0000000..e547da1
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/accessibility/data/repository/FakeAccessibilityQsShortcutsRepository.kt
@@ -0,0 +1,41 @@
+/*
+ * 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.accessibility.data.repository
+
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.SharedFlow
+import kotlinx.coroutines.flow.asSharedFlow
+
+class FakeAccessibilityQsShortcutsRepository : AccessibilityQsShortcutsRepository {
+
+ private val targetsPerUser = mutableMapOf<Int, MutableSharedFlow<Set<String>>>()
+
+ override fun a11yQsShortcutTargets(userId: Int): SharedFlow<Set<String>> {
+ return getFlow(userId).asSharedFlow()
+ }
+
+ /**
+ * Set the a11y qs shortcut targets. In real world, the A11y QS Shortcut targets are set by the
+ * Settings app not in SysUi
+ */
+ suspend fun setA11yQsShortcutTargets(userId: Int, targets: Set<String>) {
+ getFlow(userId).emit(targets)
+ }
+
+ private fun getFlow(userId: Int): MutableSharedFlow<Set<String>> =
+ targetsPerUser.getOrPut(userId) { MutableSharedFlow(replay = 1) }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/VolumePanelComponentKey.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/animation/ActivityLaunchAnimatorKosmos.kt
similarity index 77%
copy from packages/SystemUI/src/com/android/systemui/volume/panel/VolumePanelComponentKey.kt
copy to packages/SystemUI/tests/utils/src/com/android/systemui/animation/ActivityLaunchAnimatorKosmos.kt
index 22a74d2..128f58b 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/VolumePanelComponentKey.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/animation/ActivityLaunchAnimatorKosmos.kt
@@ -14,7 +14,8 @@
* limitations under the License.
*/
-package com.android.systemui.volume.panel
+package com.android.systemui.animation
-/** Uniquely identifies the [com.android.systemui.volume.panel.ui.VolumePanelComponent]. */
-typealias VolumePanelComponentKey = String
+import com.android.systemui.kosmos.Kosmos
+
+val Kosmos.activityLaunchAnimator by Kosmos.Fixture { ActivityLaunchAnimator() }
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/VolumePanelComponentKey.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/assist/AssistManagerKosmos.kt
similarity index 75%
copy from packages/SystemUI/src/com/android/systemui/volume/panel/VolumePanelComponentKey.kt
copy to packages/SystemUI/tests/utils/src/com/android/systemui/assist/AssistManagerKosmos.kt
index 22a74d2..b7d6f3a 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/VolumePanelComponentKey.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/assist/AssistManagerKosmos.kt
@@ -14,7 +14,9 @@
* limitations under the License.
*/
-package com.android.systemui.volume.panel
+package com.android.systemui.assist
-/** Uniquely identifies the [com.android.systemui.volume.panel.ui.VolumePanelComponent]. */
-typealias VolumePanelComponentKey = String
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.util.mockito.mock
+
+var Kosmos.assistManager by Kosmos.Fixture { mock<AssistManager>() }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/FakeFingerprintInteractiveToAuthProvider.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/FakeFingerprintInteractiveToAuthProvider.kt
new file mode 100644
index 0000000..8fcb60c
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/FakeFingerprintInteractiveToAuthProvider.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.biometrics
+
+import android.hardware.biometrics.common.AuthenticateReason
+import kotlinx.coroutines.flow.MutableStateFlow
+
+class FakeFingerprintInteractiveToAuthProvider : FingerprintInteractiveToAuthProvider {
+ override val enabledForCurrentUser: MutableStateFlow<Boolean> = MutableStateFlow(false)
+
+ private val userIdToExtension = mutableMapOf<Int, AuthenticateReason.Vendor>()
+ override fun getVendorExtension(userId: Int): AuthenticateReason.Vendor? =
+ userIdToExtension[userId]
+
+ fun setVendorExtension(userId: Int, extension: AuthenticateReason.Vendor) {
+ userIdToExtension[userId] = extension
+ }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/FingerprintInteractiveToAuthProviderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/FingerprintInteractiveToAuthProviderKosmos.kt
new file mode 100644
index 0000000..57dc37e3
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/FingerprintInteractiveToAuthProviderKosmos.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.biometrics
+
+import com.android.systemui.kosmos.Kosmos
+
+val Kosmos.fingerprintInteractiveToAuthProvider by
+ Kosmos.Fixture { fakeFingerprintInteractiveToAuthProvider }
+
+val Kosmos.fakeFingerprintInteractiveToAuthProvider by
+ Kosmos.Fixture { FakeFingerprintInteractiveToAuthProvider() }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FingerprintPropertyRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FingerprintPropertyRepositoryKosmos.kt
index 8702e00..b5515c4 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FingerprintPropertyRepositoryKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FingerprintPropertyRepositoryKosmos.kt
@@ -19,4 +19,6 @@
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.Kosmos.Fixture
-val Kosmos.fingerprintPropertyRepository by Fixture { FakeFingerprintPropertyRepository() }
+val Kosmos.fingerprintPropertyRepository by Fixture { fakeFingerprintPropertyRepository }
+
+val Kosmos.fakeFingerprintPropertyRepository by Fixture { FakeFingerprintPropertyRepository() }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/domain/interactor/SideFpsSensorInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/domain/interactor/SideFpsSensorInteractorKosmos.kt
new file mode 100644
index 0000000..979a49b7
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/domain/interactor/SideFpsSensorInteractorKosmos.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.biometrics.domain.interactor
+
+import android.content.applicationContext
+import android.view.windowManager
+import com.android.systemui.biometrics.data.repository.fingerprintPropertyRepository
+import com.android.systemui.biometrics.fingerprintInteractiveToAuthProvider
+import com.android.systemui.keyguard.data.repository.biometricSettingsRepository
+import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.log.SideFpsLogger
+import java.util.Optional
+import org.mockito.Mockito.mock
+
+val Kosmos.sideFpsSensorInteractor by
+ Kosmos.Fixture {
+ SideFpsSensorInteractor(
+ applicationContext,
+ fingerprintPropertyRepository,
+ windowManager,
+ displayStateInteractor,
+ Optional.of(fingerprintInteractiveToAuthProvider),
+ biometricSettingsRepository,
+ keyguardTransitionInteractor,
+ mock(SideFpsLogger::class.java),
+ )
+ }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalRepository.kt
index 20fa545..cccd908 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalRepository.kt
@@ -50,4 +50,11 @@
fun setIsCommunalHubShowing(isCommunalHubShowing: Boolean) {
_isCommunalHubShowing.value = isCommunalHubShowing
}
+
+ private val _communalEnabledState: MutableStateFlow<Boolean> = MutableStateFlow(false)
+ override val communalEnabledState: StateFlow<Boolean> = _communalEnabledState
+
+ fun setCommunalEnabledState(enabled: Boolean) {
+ _communalEnabledState.value = enabled
+ }
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalWidgetRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalWidgetRepository.kt
index e25e8c0..bc7e7af 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalWidgetRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalWidgetRepository.kt
@@ -39,13 +39,4 @@
private fun onConfigured(id: Int, providerInfo: AppWidgetProviderInfo, priority: Int) {
_communalWidgets.value += listOf(CommunalWidgetContentModel(id, providerInfo, priority))
}
-
- private var isHostActive = false
- override fun updateAppWidgetHostActive(active: Boolean) {
- isHostActive = active
- }
-
- fun isHostActive(): Boolean {
- return isHostActive
- }
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorKosmos.kt
index 7e3c10b..1abf71f 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorKosmos.kt
@@ -26,6 +26,7 @@
import com.android.systemui.kosmos.Kosmos.Fixture
import com.android.systemui.kosmos.applicationCoroutineScope
import com.android.systemui.smartspace.data.repository.smartspaceRepository
+import com.android.systemui.user.data.repository.userRepository
import com.android.systemui.util.mockito.mock
val Kosmos.communalInteractor by Fixture {
@@ -36,6 +37,7 @@
mediaRepository = communalMediaRepository,
communalPrefsRepository = communalPrefsRepository,
smartspaceRepository = smartspaceRepository,
+ userRepository = userRepository,
appWidgetHost = mock(),
keyguardInteractor = keyguardInteractor,
editWidgetsActivityStarter = editWidgetsActivityStarter,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalTutorialInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalTutorialInteractorKosmos.kt
index baba88d..adaea7c 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalTutorialInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalTutorialInteractorKosmos.kt
@@ -29,5 +29,6 @@
communalTutorialRepository = communalTutorialRepository,
keyguardInteractor = keyguardInteractor,
communalRepository = communalRepository,
+ communalInteractor = communalInteractor,
)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/data/repository/FakeStickyKeysRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/data/repository/FakeStickyKeysRepository.kt
new file mode 100644
index 0000000..68e1457
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/data/repository/FakeStickyKeysRepository.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyboard.data.repository
+
+import com.android.systemui.keyboard.stickykeys.data.repository.StickyKeysRepository
+import com.android.systemui.keyboard.stickykeys.shared.model.Locked
+import com.android.systemui.keyboard.stickykeys.shared.model.ModifierKey
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+
+class FakeStickyKeysRepository : StickyKeysRepository {
+ override val settingEnabled: Flow<Boolean> = MutableStateFlow(true)
+ private val _stickyKeys: MutableStateFlow<LinkedHashMap<ModifierKey, Locked>> =
+ MutableStateFlow(LinkedHashMap())
+
+ override val stickyKeys: Flow<LinkedHashMap<ModifierKey, Locked>> = _stickyKeys
+
+ fun setStickyKeys(keys: LinkedHashMap<ModifierKey, Locked>) {
+ _stickyKeys.value = keys
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/VolumePanelComponentKey.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/data/repository/KeyboardRepositoryKosmos.kt
similarity index 70%
copy from packages/SystemUI/src/com/android/systemui/volume/panel/VolumePanelComponentKey.kt
copy to packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/data/repository/KeyboardRepositoryKosmos.kt
index 22a74d2..46f7355 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/VolumePanelComponentKey.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/data/repository/KeyboardRepositoryKosmos.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2023 The Android Open Source Project
+ * Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,7 +14,8 @@
* limitations under the License.
*/
-package com.android.systemui.volume.panel
+package com.android.systemui.keyboard.data.repository
-/** Uniquely identifies the [com.android.systemui.volume.panel.ui.VolumePanelComponent]. */
-typealias VolumePanelComponentKey = String
+import com.android.systemui.kosmos.Kosmos
+
+val Kosmos.keyboardRepository by Kosmos.Fixture { FakeKeyboardRepository() }
\ No newline at end of file
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepositoryKosmos.kt
index 45d39b0..cf8f812 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepositoryKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepositoryKosmos.kt
@@ -19,4 +19,5 @@
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.Kosmos.Fixture
-val Kosmos.biometricSettingsRepository by Fixture { FakeBiometricSettingsRepository() }
+val Kosmos.fakeBiometricSettingsRepository by Fixture { FakeBiometricSettingsRepository() }
+val Kosmos.biometricSettingsRepository by Fixture { fakeBiometricSettingsRepository }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/DeviceEntryFingerprintAuthRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/DeviceEntryFingerprintAuthRepositoryKosmos.kt
index 6437ef3..0d20939 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/DeviceEntryFingerprintAuthRepositoryKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/DeviceEntryFingerprintAuthRepositoryKosmos.kt
@@ -19,6 +19,10 @@
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.Kosmos.Fixture
-val Kosmos.deviceEntryFingerprintAuthRepository by Fixture {
+val Kosmos.fakeDeviceEntryFingerprintAuthRepository by Fixture {
FakeDeviceEntryFingerprintAuthRepository()
}
+
+val Kosmos.deviceEntryFingerprintAuthRepository by Fixture {
+ fakeDeviceEntryFingerprintAuthRepository
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt
index 59f56dd..5766f7a 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt
@@ -130,6 +130,8 @@
private val _isEncryptedOrLockdown = MutableStateFlow(true)
override val isEncryptedOrLockdown: Flow<Boolean> = _isEncryptedOrLockdown
+ override val topClippingBounds = MutableStateFlow<Int?>(null)
+
override fun setQuickSettingsVisible(isVisible: Boolean) {
_isQuickSettingsVisible.value = isVisible
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractorKosmos.kt
index 97536e2..719686e 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractorKosmos.kt
@@ -17,6 +17,7 @@
package com.android.systemui.keyguard.domain.interactor
import com.android.keyguard.keyguardSecurityModel
+import com.android.systemui.communal.domain.interactor.communalInteractor
import com.android.systemui.flags.featureFlagsClassic
import com.android.systemui.keyguard.data.repository.keyguardTransitionRepository
import com.android.systemui.kosmos.Kosmos
@@ -34,6 +35,7 @@
bgDispatcher = testDispatcher,
mainDispatcher = testDispatcher,
keyguardInteractor = keyguardInteractor,
+ communalInteractor = communalInteractor,
flags = featureFlagsClassic,
keyguardSecurityModel = keyguardSecurityModel,
selectedUserInteractor = selectedUserInteractor,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/GlanceableHubTransitionsKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/GlanceableHubTransitionsKosmos.kt
index ec17c48..55885bf 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/GlanceableHubTransitionsKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/GlanceableHubTransitionsKosmos.kt
@@ -20,11 +20,13 @@
import com.android.systemui.keyguard.data.repository.keyguardTransitionRepository
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.kosmos.testDispatcher
val Kosmos.glanceableHubTransitions by
Kosmos.Fixture {
GlanceableHubTransitions(
scope = applicationCoroutineScope,
+ bgDispatcher = testDispatcher,
transitionRepository = keyguardTransitionRepository,
transitionInteractor = keyguardTransitionInteractor,
communalInteractor = communalInteractor,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModelKosmos.kt
index 35cfa89..a8f45b0 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModelKosmos.kt
@@ -26,7 +26,7 @@
import com.android.systemui.kosmos.Kosmos.Fixture
import kotlinx.coroutines.ExperimentalCoroutinesApi
-val Kosmos.aodBurnInViewModel by Fixture {
+var Kosmos.aodBurnInViewModel by Fixture {
AodBurnInViewModel(
burnInInteractor = burnInInteractor,
configurationInteractor = configurationInteractor,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModelKosmos.kt
index 5ca0439..4a85909 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModelKosmos.kt
@@ -20,6 +20,7 @@
import com.android.systemui.keyguard.domain.interactor.keyguardInteractor
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.statusbar.notification.stack.domain.interactor.notificationsKeyguardInteractor
import com.android.systemui.statusbar.policy.splitShadeStateController
val Kosmos.keyguardClockViewModel by
@@ -29,5 +30,6 @@
keyguardClockInteractor = keyguardClockInteractor,
applicationScope = applicationCoroutineScope,
splitShadeStateController = splitShadeStateController,
+ notifsKeyguardInteractor = notificationsKeyguardInteractor,
)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt
index 5564767..d376f12 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt
@@ -18,6 +18,7 @@
package com.android.systemui.keyguard.ui.viewmodel
+import com.android.systemui.communal.domain.interactor.communalInteractor
import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor
import com.android.systemui.keyguard.domain.interactor.keyguardInteractor
import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
@@ -33,6 +34,7 @@
deviceEntryInteractor = deviceEntryInteractor,
dozeParameters = dozeParameters,
keyguardInteractor = keyguardInteractor,
+ communalInteractor = communalInteractor,
keyguardTransitionInteractor = keyguardTransitionInteractor,
notificationsKeyguardInteractor = notificationsKeyguardInteractor,
aodToLockscreenTransitionViewModel = aodToLockscreenTransitionViewModel,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeControllerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeControllerKosmos.kt
new file mode 100644
index 0000000..e13fa52
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeControllerKosmos.kt
@@ -0,0 +1,79 @@
+/*
+ * 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.
+ */
+
+@file:OptIn(ExperimentalCoroutinesApi::class)
+
+package com.android.systemui.shade
+
+import android.view.WindowManager
+import com.android.systemui.assist.AssistManager
+import com.android.systemui.concurrency.fakeExecutor
+import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.log.LogBuffer
+import com.android.systemui.plugins.statusbar.statusBarStateController
+import com.android.systemui.scene.domain.interactor.sceneInteractor
+import com.android.systemui.shade.domain.interactor.shadeInteractor
+import com.android.systemui.statusbar.CommandQueue
+import com.android.systemui.statusbar.NotificationShadeWindowController
+import com.android.systemui.statusbar.notification.row.NotificationGutsManager
+import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout
+import com.android.systemui.statusbar.notification.stack.ui.viewmodel.windowRootViewVisibilityInteractor
+import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager
+import com.android.systemui.statusbar.phone.statusBarKeyguardViewManager
+import com.android.systemui.statusbar.policy.KeyguardStateController
+import com.android.systemui.statusbar.policy.deviceProvisionedController
+import com.android.systemui.statusbar.window.StatusBarWindowController
+import com.android.systemui.util.mockito.mock
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+
+val Kosmos.shadeControllerSceneImpl by
+ Kosmos.Fixture {
+ ShadeControllerSceneImpl(
+ scope = applicationCoroutineScope,
+ shadeInteractor = shadeInteractor,
+ sceneInteractor = sceneInteractor,
+ notificationStackScrollLayout = mock<NotificationStackScrollLayout>(),
+ deviceEntryInteractor = deviceEntryInteractor,
+ touchLog = mock<LogBuffer>(),
+ commandQueue = mock<CommandQueue>(),
+ statusBarKeyguardViewManager = mock<StatusBarKeyguardViewManager>(),
+ notificationShadeWindowController = mock<NotificationShadeWindowController>(),
+ assistManagerLazy = { mock<AssistManager>() },
+ )
+ }
+
+val Kosmos.shadeControllerImpl by
+ Kosmos.Fixture {
+ ShadeControllerImpl(
+ mock<CommandQueue>(),
+ fakeExecutor,
+ mock<LogBuffer>(),
+ windowRootViewVisibilityInteractor,
+ mock<KeyguardStateController>(),
+ statusBarStateController,
+ statusBarKeyguardViewManager,
+ mock<StatusBarWindowController>(),
+ deviceProvisionedController,
+ mock<NotificationShadeWindowController>(),
+ mock<WindowManager>(),
+ { mock<ShadeViewController>() },
+ { mock<AssistManager>() },
+ { mock<NotificationGutsManager>() },
+ )
+ }
+var Kosmos.shadeController: ShadeController by Kosmos.Fixture { shadeControllerImpl }
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/VolumePanelComponentKey.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeViewControllerKosmos.kt
similarity index 74%
copy from packages/SystemUI/src/com/android/systemui/volume/panel/VolumePanelComponentKey.kt
copy to packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeViewControllerKosmos.kt
index 22a74d2..1ceab68 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/VolumePanelComponentKey.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeViewControllerKosmos.kt
@@ -14,7 +14,9 @@
* limitations under the License.
*/
-package com.android.systemui.volume.panel
+package com.android.systemui.shade
-/** Uniquely identifies the [com.android.systemui.volume.panel.ui.VolumePanelComponent]. */
-typealias VolumePanelComponentKey = String
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.util.mockito.mock
+
+var Kosmos.shadeViewController by Kosmos.Fixture { mock<ShadeViewController>() }
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/VolumePanelComponentKey.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/data/repository/ShadeAnimationRepositoryKosmos.kt
similarity index 76%
copy from packages/SystemUI/src/com/android/systemui/volume/panel/VolumePanelComponentKey.kt
copy to packages/SystemUI/tests/utils/src/com/android/systemui/shade/data/repository/ShadeAnimationRepositoryKosmos.kt
index 22a74d2..4dcd220 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/VolumePanelComponentKey.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/data/repository/ShadeAnimationRepositoryKosmos.kt
@@ -14,7 +14,8 @@
* limitations under the License.
*/
-package com.android.systemui.volume.panel
+package com.android.systemui.shade.data.repository
-/** Uniquely identifies the [com.android.systemui.volume.panel.ui.VolumePanelComponent]. */
-typealias VolumePanelComponentKey = String
+import com.android.systemui.kosmos.Kosmos
+
+val Kosmos.shadeAnimationRepository by Kosmos.Fixture { ShadeAnimationRepository() }
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/VolumePanelComponentKey.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeAnimationInteractorKosmos.kt
similarity index 65%
copy from packages/SystemUI/src/com/android/systemui/volume/panel/VolumePanelComponentKey.kt
copy to packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeAnimationInteractorKosmos.kt
index 22a74d2..57b272f 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/VolumePanelComponentKey.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeAnimationInteractorKosmos.kt
@@ -14,7 +14,10 @@
* limitations under the License.
*/
-package com.android.systemui.volume.panel
+package com.android.systemui.shade.domain.interactor
-/** Uniquely identifies the [com.android.systemui.volume.panel.ui.VolumePanelComponent]. */
-typealias VolumePanelComponentKey = String
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.shade.data.repository.shadeAnimationRepository
+
+var Kosmos.shadeAnimationInteractor: ShadeAnimationInteractor by
+ Kosmos.Fixture { ShadeAnimationInteractorEmptyImpl(shadeAnimationRepository) }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shared/notifications/data/repository/NotificationSettingsRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shared/notifications/data/repository/NotificationSettingsRepositoryKosmos.kt
new file mode 100644
index 0000000..a75d2bc
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shared/notifications/data/repository/NotificationSettingsRepositoryKosmos.kt
@@ -0,0 +1,31 @@
+/*
+ * 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.shared.notifications.data.repository
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.shared.settings.data.repository.secureSettingsRepository
+
+val Kosmos.notificationSettingsRepository by
+ Kosmos.Fixture {
+ NotificationSettingsRepository(
+ scope = testScope.backgroundScope,
+ backgroundDispatcher = testDispatcher,
+ secureSettingsRepository = secureSettingsRepository,
+ )
+ }
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/VolumePanelComponentKey.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shared/notifications/domain/interactor/NotificationSettingsInteractorKosmos.kt
similarity index 64%
copy from packages/SystemUI/src/com/android/systemui/volume/panel/VolumePanelComponentKey.kt
copy to packages/SystemUI/tests/utils/src/com/android/systemui/shared/notifications/domain/interactor/NotificationSettingsInteractorKosmos.kt
index 22a74d2..17b4603 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/VolumePanelComponentKey.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shared/notifications/domain/interactor/NotificationSettingsInteractorKosmos.kt
@@ -14,7 +14,10 @@
* limitations under the License.
*/
-package com.android.systemui.volume.panel
+package com.android.systemui.shared.notifications.domain.interactor
-/** Uniquely identifies the [com.android.systemui.volume.panel.ui.VolumePanelComponent]. */
-typealias VolumePanelComponentKey = String
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.shared.notifications.data.repository.notificationSettingsRepository
+
+val Kosmos.notificationSettingsInteractor by
+ Kosmos.Fixture { NotificationSettingsInteractor(notificationSettingsRepository) }
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/VolumePanelComponentKey.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shared/settings/data/repository/SecureSettingsRepositoryKosmos.kt
similarity index 65%
copy from packages/SystemUI/src/com/android/systemui/volume/panel/VolumePanelComponentKey.kt
copy to packages/SystemUI/tests/utils/src/com/android/systemui/shared/settings/data/repository/SecureSettingsRepositoryKosmos.kt
index 22a74d2..552b09e 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/VolumePanelComponentKey.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shared/settings/data/repository/SecureSettingsRepositoryKosmos.kt
@@ -14,7 +14,10 @@
* limitations under the License.
*/
-package com.android.systemui.volume.panel
+package com.android.systemui.shared.settings.data.repository
-/** Uniquely identifies the [com.android.systemui.volume.panel.ui.VolumePanelComponent]. */
-typealias VolumePanelComponentKey = String
+import com.android.systemui.kosmos.Kosmos
+
+var Kosmos.secureSettingsRepository: SecureSettingsRepository by
+ Kosmos.Fixture { fakeSecureSettingsRepository }
+val Kosmos.fakeSecureSettingsRepository by Kosmos.Fixture { FakeSecureSettingsRepository() }
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/VolumePanelComponentKey.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/NotificationClickNotifierKosmos.kt
similarity index 73%
copy from packages/SystemUI/src/com/android/systemui/volume/panel/VolumePanelComponentKey.kt
copy to packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/NotificationClickNotifierKosmos.kt
index 22a74d2..7b912ae 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/VolumePanelComponentKey.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/NotificationClickNotifierKosmos.kt
@@ -14,7 +14,9 @@
* limitations under the License.
*/
-package com.android.systemui.volume.panel
+package com.android.systemui.statusbar
-/** Uniquely identifies the [com.android.systemui.volume.panel.ui.VolumePanelComponent]. */
-typealias VolumePanelComponentKey = String
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.util.mockito.mock
+
+var Kosmos.notificationClickNotifier by Kosmos.Fixture { mock<NotificationClickNotifier>() }
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/VolumePanelComponentKey.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/NotificationPresenterKosmos.kt
similarity index 73%
copy from packages/SystemUI/src/com/android/systemui/volume/panel/VolumePanelComponentKey.kt
copy to packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/NotificationPresenterKosmos.kt
index 22a74d2..8d30049 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/VolumePanelComponentKey.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/NotificationPresenterKosmos.kt
@@ -14,7 +14,9 @@
* limitations under the License.
*/
-package com.android.systemui.volume.panel
+package com.android.systemui.statusbar
-/** Uniquely identifies the [com.android.systemui.volume.panel.ui.VolumePanelComponent]. */
-typealias VolumePanelComponentKey = String
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.util.mockito.mock
+
+var Kosmos.notificationPresenter by Kosmos.Fixture { mock<NotificationPresenter>() }
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/VolumePanelComponentKey.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/NotificationRemoteInputManagerKosmos.kt
similarity index 71%
copy from packages/SystemUI/src/com/android/systemui/volume/panel/VolumePanelComponentKey.kt
copy to packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/NotificationRemoteInputManagerKosmos.kt
index 22a74d2..554bdbe 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/VolumePanelComponentKey.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/NotificationRemoteInputManagerKosmos.kt
@@ -14,7 +14,10 @@
* limitations under the License.
*/
-package com.android.systemui.volume.panel
+package com.android.systemui.statusbar
-/** Uniquely identifies the [com.android.systemui.volume.panel.ui.VolumePanelComponent]. */
-typealias VolumePanelComponentKey = String
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.util.mockito.mock
+
+var Kosmos.notificationRemoteInputManager by
+ Kosmos.Fixture { mock<NotificationRemoteInputManager>() }
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/VolumePanelComponentKey.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/NotificationShadeWindowControllerKosmos.kt
similarity index 71%
copy from packages/SystemUI/src/com/android/systemui/volume/panel/VolumePanelComponentKey.kt
copy to packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/NotificationShadeWindowControllerKosmos.kt
index 22a74d2..e8ca3b8 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/VolumePanelComponentKey.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/NotificationShadeWindowControllerKosmos.kt
@@ -14,7 +14,10 @@
* limitations under the License.
*/
-package com.android.systemui.volume.panel
+package com.android.systemui.statusbar
-/** Uniquely identifies the [com.android.systemui.volume.panel.ui.VolumePanelComponent]. */
-typealias VolumePanelComponentKey = String
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.util.mockito.mock
+
+var Kosmos.notificationShadeWindowController by
+ Kosmos.Fixture { mock<NotificationShadeWindowController>() }
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/VolumePanelComponentKey.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/NotificationActivityStarterKosmos.kt
similarity index 66%
copy from packages/SystemUI/src/com/android/systemui/volume/panel/VolumePanelComponentKey.kt
copy to packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/NotificationActivityStarterKosmos.kt
index 22a74d2..c337ac2 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/VolumePanelComponentKey.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/NotificationActivityStarterKosmos.kt
@@ -14,7 +14,10 @@
* limitations under the License.
*/
-package com.android.systemui.volume.panel
+package com.android.systemui.statusbar.notification
-/** Uniquely identifies the [com.android.systemui.volume.panel.ui.VolumePanelComponent]. */
-typealias VolumePanelComponentKey = String
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.statusbar.phone.statusBarNotificationActivityStarter
+
+var Kosmos.notificationActivityStarter: NotificationActivityStarter by
+ Kosmos.Fixture { statusBarNotificationActivityStarter }
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/VolumePanelComponentKey.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/NotificationLaunchAnimatorControllerProviderKosmos.kt
similarity index 68%
copy from packages/SystemUI/src/com/android/systemui/volume/panel/VolumePanelComponentKey.kt
copy to packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/NotificationLaunchAnimatorControllerProviderKosmos.kt
index 22a74d2..c3db34b 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/VolumePanelComponentKey.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/NotificationLaunchAnimatorControllerProviderKosmos.kt
@@ -14,7 +14,10 @@
* limitations under the License.
*/
-package com.android.systemui.volume.panel
+package com.android.systemui.statusbar.notification
-/** Uniquely identifies the [com.android.systemui.volume.panel.ui.VolumePanelComponent]. */
-typealias VolumePanelComponentKey = String
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.util.mockito.mock
+
+var Kosmos.notificationLaunchAnimatorControllerProvider by
+ Kosmos.Fixture { mock<NotificationLaunchAnimatorControllerProvider>() }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/EntryUtil.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/EntryUtil.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/EntryUtil.kt
rename to packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/EntryUtil.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/GroupEntryBuilder.java b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/GroupEntryBuilder.java
similarity index 90%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/GroupEntryBuilder.java
rename to packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/GroupEntryBuilder.java
index dda7fad..4efcada 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/GroupEntryBuilder.java
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/GroupEntryBuilder.java
@@ -52,32 +52,38 @@
return ge;
}
+ /** Sets the group key. */
public GroupEntryBuilder setKey(String key) {
mKey = key;
return this;
}
+ /** Sets the creation time. */
public GroupEntryBuilder setCreationTime(long creationTime) {
mCreationTime = creationTime;
return this;
}
+ /** Sets the parent entry of the group. */
public GroupEntryBuilder setParent(@Nullable GroupEntry entry) {
mParent = entry;
return this;
}
+ /** Sets the section the group belongs to. */
public GroupEntryBuilder setSection(@Nullable NotifSection section) {
mNotifSection = section;
return this;
}
+ /** Sets the group summary. */
public GroupEntryBuilder setSummary(
NotificationEntry summary) {
mSummary = summary;
return this;
}
+ /** Sets the group children. */
public GroupEntryBuilder setChildren(List<NotificationEntry> children) {
mChildren.clear();
mChildren.addAll(children);
@@ -90,6 +96,7 @@
return this;
}
+ /** Get the group's internal children list. */
public static List<NotificationEntry> getRawChildren(GroupEntry groupEntry) {
return groupEntry.getRawChildren();
}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/VolumePanelComponentKey.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/NotifLiveDataStoreKosmos.kt
similarity index 68%
copy from packages/SystemUI/src/com/android/systemui/volume/panel/VolumePanelComponentKey.kt
copy to packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/NotifLiveDataStoreKosmos.kt
index 22a74d2..1f45fbb 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/VolumePanelComponentKey.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/NotifLiveDataStoreKosmos.kt
@@ -14,7 +14,10 @@
* limitations under the License.
*/
-package com.android.systemui.volume.panel
+package com.android.systemui.statusbar.notification.collection
-/** Uniquely identifies the [com.android.systemui.volume.panel.ui.VolumePanelComponent]. */
-typealias VolumePanelComponentKey = String
+import com.android.systemui.concurrency.fakeExecutor
+import com.android.systemui.kosmos.Kosmos
+
+var Kosmos.notifLiveDataStore: NotifLiveDataStore by
+ Kosmos.Fixture { NotifLiveDataStoreImpl(fakeExecutor) }
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/VolumePanelComponentKey.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinatorKosmos.kt
similarity index 69%
copy from packages/SystemUI/src/com/android/systemui/volume/panel/VolumePanelComponentKey.kt
copy to packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinatorKosmos.kt
index 22a74d2..358d251 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/VolumePanelComponentKey.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinatorKosmos.kt
@@ -14,7 +14,9 @@
* limitations under the License.
*/
-package com.android.systemui.volume.panel
+package com.android.systemui.statusbar.notification.collection.coordinator
-/** Uniquely identifies the [com.android.systemui.volume.panel.ui.VolumePanelComponent]. */
-typealias VolumePanelComponentKey = String
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.util.mockito.mock
+
+var Kosmos.visualStabilityCoordinator by Kosmos.Fixture { mock<VisualStabilityCoordinator>() }
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/VolumePanelComponentKey.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/provider/LaunchFullScreenIntentProviderKosmos.kt
similarity index 73%
copy from packages/SystemUI/src/com/android/systemui/volume/panel/VolumePanelComponentKey.kt
copy to packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/provider/LaunchFullScreenIntentProviderKosmos.kt
index 22a74d2..a5c9561 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/VolumePanelComponentKey.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/provider/LaunchFullScreenIntentProviderKosmos.kt
@@ -14,7 +14,8 @@
* limitations under the License.
*/
-package com.android.systemui.volume.panel
+package com.android.systemui.statusbar.notification.collection.provider
-/** Uniquely identifies the [com.android.systemui.volume.panel.ui.VolumePanelComponent]. */
-typealias VolumePanelComponentKey = String
+import com.android.systemui.kosmos.Kosmos
+
+val Kosmos.launchFullScreenIntentProvider by Kosmos.Fixture { LaunchFullScreenIntentProvider() }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/render/NotificationVisibilityProviderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/render/NotificationVisibilityProviderKosmos.kt
new file mode 100644
index 0000000..edce5d58
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/render/NotificationVisibilityProviderKosmos.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.collection.render
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.statusbar.notification.collection.notifLiveDataStore
+import com.android.systemui.statusbar.notification.collection.notifPipeline
+import com.android.systemui.statusbar.notification.collection.provider.NotificationVisibilityProviderImpl
+import com.android.systemui.statusbar.notification.domain.interactor.activeNotificationsInteractor
+
+var Kosmos.notificationVisibilityProvider: NotificationVisibilityProvider by
+ Kosmos.Fixture {
+ NotificationVisibilityProviderImpl(
+ activeNotificationsInteractor,
+ notifLiveDataStore,
+ notifPipeline,
+ )
+ }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/OnUserInteractionCallbackKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/OnUserInteractionCallbackKosmos.kt
new file mode 100644
index 0000000..1e3897b
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/OnUserInteractionCallbackKosmos.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.row
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.plugins.statusbar.statusBarStateController
+import com.android.systemui.statusbar.notification.collection.coordinator.visualStabilityCoordinator
+import com.android.systemui.statusbar.notification.collection.inflation.OnUserInteractionCallbackImpl
+import com.android.systemui.statusbar.notification.collection.notifCollection
+import com.android.systemui.statusbar.notification.collection.render.notificationVisibilityProvider
+import com.android.systemui.statusbar.policy.headsUpManager
+
+var Kosmos.onUserInteractionCallback: OnUserInteractionCallback by
+ Kosmos.Fixture {
+ OnUserInteractionCallbackImpl(
+ notificationVisibilityProvider,
+ notifCollection,
+ headsUpManager,
+ statusBarStateController,
+ visualStabilityCoordinator,
+ )
+ }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelKosmos.kt
index 5ef9a8e..7c398cd 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelKosmos.kt
@@ -16,8 +16,12 @@
package com.android.systemui.statusbar.notification.stack.ui.viewmodel
+import com.android.systemui.communal.domain.interactor.communalInteractor
import com.android.systemui.keyguard.domain.interactor.keyguardInteractor
import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
+import com.android.systemui.keyguard.ui.viewmodel.aodBurnInViewModel
+import com.android.systemui.keyguard.ui.viewmodel.glanceableHubToLockscreenTransitionViewModel
+import com.android.systemui.keyguard.ui.viewmodel.lockscreenToGlanceableHubTransitionViewModel
import com.android.systemui.keyguard.ui.viewmodel.lockscreenToOccludedTransitionViewModel
import com.android.systemui.keyguard.ui.viewmodel.occludedToLockscreenTransitionViewModel
import com.android.systemui.kosmos.Kosmos
@@ -33,7 +37,11 @@
keyguardInteractor = keyguardInteractor,
keyguardTransitionInteractor = keyguardTransitionInteractor,
shadeInteractor = shadeInteractor,
+ communalInteractor = communalInteractor,
occludedToLockscreenTransitionViewModel = occludedToLockscreenTransitionViewModel,
lockscreenToOccludedTransitionViewModel = lockscreenToOccludedTransitionViewModel,
+ glanceableHubToLockscreenTransitionViewModel = glanceableHubToLockscreenTransitionViewModel,
+ lockscreenToGlanceableHubTransitionViewModel = lockscreenToGlanceableHubTransitionViewModel,
+ aodBurnInViewModel = aodBurnInViewModel,
)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/WindowRootViewVisibilityInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/WindowRootViewVisibilityInteractorKosmos.kt
index d80ee75..cf800d0 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/WindowRootViewVisibilityInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/WindowRootViewVisibilityInteractorKosmos.kt
@@ -23,6 +23,8 @@
import com.android.systemui.power.domain.interactor.powerInteractor
import com.android.systemui.scene.data.repository.windowRootViewVisibilityRepository
import com.android.systemui.scene.domain.interactor.WindowRootViewVisibilityInteractor
+import com.android.systemui.scene.domain.interactor.sceneInteractor
+import com.android.systemui.scene.shared.flag.sceneContainerFlags
import com.android.systemui.statusbar.notification.domain.interactor.activeNotificationsInteractor
import com.android.systemui.statusbar.policy.headsUpManager
@@ -34,5 +36,7 @@
headsUpManager = headsUpManager,
powerInteractor = powerInteractor,
activeNotificationsInteractor = activeNotificationsInteractor,
+ sceneInteractorProvider = { sceneInteractor },
+ sceneContainerFlags = sceneContainerFlags,
)
}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/VolumePanelComponentKey.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/DozeServiceHostKosmos.kt
similarity index 60%
copy from packages/SystemUI/src/com/android/systemui/volume/panel/VolumePanelComponentKey.kt
copy to packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/DozeServiceHostKosmos.kt
index 22a74d2..370b177 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/VolumePanelComponentKey.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/DozeServiceHostKosmos.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2023 The Android Open Source Project
+ * Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,7 +14,11 @@
* limitations under the License.
*/
-package com.android.systemui.volume.panel
+package com.android.systemui.statusbar.phone
-/** Uniquely identifies the [com.android.systemui.volume.panel.ui.VolumePanelComponent]. */
-typealias VolumePanelComponentKey = String
+import com.android.systemui.kosmos.Kosmos
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import org.mockito.Mockito.mock
+
+@OptIn(ExperimentalCoroutinesApi::class)
+val Kosmos.dozeServiceHost: DozeServiceHost by Kosmos.Fixture { mock(DozeServiceHost::class.java) }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterKosmos.kt
new file mode 100644
index 0000000..6ddc9df
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterKosmos.kt
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.phone
+
+import android.app.keyguardManager
+import android.content.applicationContext
+import android.os.fakeExecutorHandler
+import android.service.dream.dreamManager
+import com.android.internal.logging.metricsLogger
+import com.android.internal.widget.lockPatternUtils
+import com.android.systemui.activityIntentHelper
+import com.android.systemui.animation.activityLaunchAnimator
+import com.android.systemui.assist.assistManager
+import com.android.systemui.concurrency.fakeExecutor
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.plugins.activityStarter
+import com.android.systemui.power.domain.interactor.powerInteractor
+import com.android.systemui.settings.userTracker
+import com.android.systemui.shade.domain.interactor.shadeAnimationInteractor
+import com.android.systemui.shade.shadeController
+import com.android.systemui.shade.shadeViewController
+import com.android.systemui.statusbar.notification.collection.provider.launchFullScreenIntentProvider
+import com.android.systemui.statusbar.notification.collection.render.notificationVisibilityProvider
+import com.android.systemui.statusbar.notification.notificationLaunchAnimatorControllerProvider
+import com.android.systemui.statusbar.notification.row.onUserInteractionCallback
+import com.android.systemui.statusbar.notificationClickNotifier
+import com.android.systemui.statusbar.notificationLockscreenUserManager
+import com.android.systemui.statusbar.notificationPresenter
+import com.android.systemui.statusbar.notificationRemoteInputManager
+import com.android.systemui.statusbar.notificationShadeWindowController
+import com.android.systemui.statusbar.policy.headsUpManager
+import com.android.systemui.statusbar.policy.keyguardStateController
+import com.android.systemui.wmshell.bubblesManager
+import java.util.Optional
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+
+@OptIn(ExperimentalCoroutinesApi::class)
+val Kosmos.statusBarNotificationActivityStarter by
+ Kosmos.Fixture {
+ StatusBarNotificationActivityStarter(
+ applicationContext,
+ applicationContext.displayId,
+ fakeExecutorHandler,
+ fakeExecutor,
+ notificationVisibilityProvider,
+ headsUpManager,
+ activityStarter,
+ notificationClickNotifier,
+ statusBarKeyguardViewManager,
+ keyguardManager,
+ dreamManager,
+ Optional.of(bubblesManager),
+ { assistManager },
+ notificationRemoteInputManager,
+ notificationLockscreenUserManager,
+ shadeController,
+ keyguardStateController,
+ lockPatternUtils,
+ statusBarRemoteInputCallback,
+ activityIntentHelper,
+ metricsLogger,
+ statusBarNotificationActivityStarterLogger,
+ onUserInteractionCallback,
+ notificationPresenter,
+ shadeViewController,
+ notificationShadeWindowController,
+ activityLaunchAnimator,
+ shadeAnimationInteractor,
+ notificationLaunchAnimatorControllerProvider,
+ launchFullScreenIntentProvider,
+ powerInteractor,
+ userTracker,
+ )
+ }
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/VolumePanelComponentKey.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterLoggerKosmos.kt
similarity index 68%
copy from packages/SystemUI/src/com/android/systemui/volume/panel/VolumePanelComponentKey.kt
copy to packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterLoggerKosmos.kt
index 22a74d2..31cfc97 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/VolumePanelComponentKey.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterLoggerKosmos.kt
@@ -14,7 +14,10 @@
* limitations under the License.
*/
-package com.android.systemui.volume.panel
+package com.android.systemui.statusbar.phone
-/** Uniquely identifies the [com.android.systemui.volume.panel.ui.VolumePanelComponent]. */
-typealias VolumePanelComponentKey = String
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.log.logcatLogBuffer
+
+val Kosmos.statusBarNotificationActivityStarterLogger by
+ Kosmos.Fixture { StatusBarNotificationActivityStarterLogger(logcatLogBuffer()) }
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/VolumePanelComponentKey.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallbackKosmos.kt
similarity index 72%
copy from packages/SystemUI/src/com/android/systemui/volume/panel/VolumePanelComponentKey.kt
copy to packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallbackKosmos.kt
index 22a74d2..475d7fa 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/VolumePanelComponentKey.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallbackKosmos.kt
@@ -14,7 +14,9 @@
* limitations under the License.
*/
-package com.android.systemui.volume.panel
+package com.android.systemui.statusbar.phone
-/** Uniquely identifies the [com.android.systemui.volume.panel.ui.VolumePanelComponent]. */
-typealias VolumePanelComponentKey = String
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.util.mockito.mock
+
+var Kosmos.statusBarRemoteInputCallback by Kosmos.Fixture { mock<StatusBarRemoteInputCallback>() }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconInteractor.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconInteractor.kt
index 5f4d7bf..c010327 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconInteractor.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconInteractor.kt
@@ -60,6 +60,8 @@
override val isInService = MutableStateFlow(true)
+ override val isNonTerrestrial = MutableStateFlow(false)
+
private val _isDataEnabled = MutableStateFlow(true)
override val isDataEnabled = _isDataEnabled
@@ -69,7 +71,7 @@
override val signalLevelIcon: MutableStateFlow<SignalIconModel> =
MutableStateFlow(
- SignalIconModel(
+ SignalIconModel.Cellular(
level = 0,
numberOfLevels = 4,
showExclamationMark = false,
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/VolumePanelComponentKey.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/KeyguardStateControllerKosmos.kt
similarity index 72%
copy from packages/SystemUI/src/com/android/systemui/volume/panel/VolumePanelComponentKey.kt
copy to packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/KeyguardStateControllerKosmos.kt
index 22a74d2..0e909c4 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/VolumePanelComponentKey.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/KeyguardStateControllerKosmos.kt
@@ -14,7 +14,9 @@
* limitations under the License.
*/
-package com.android.systemui.volume.panel
+package com.android.systemui.statusbar.policy
-/** Uniquely identifies the [com.android.systemui.volume.panel.ui.VolumePanelComponent]. */
-typealias VolumePanelComponentKey = String
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.util.mockito.mock
+
+var Kosmos.keyguardStateController by Kosmos.Fixture { mock<KeyguardStateController>() }
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/VolumePanelComponentKey.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/wmshell/BubblesManagerKosmos.kt
similarity index 75%
copy from packages/SystemUI/src/com/android/systemui/volume/panel/VolumePanelComponentKey.kt
copy to packages/SystemUI/tests/utils/src/com/android/systemui/wmshell/BubblesManagerKosmos.kt
index 22a74d2..1d05d62 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/VolumePanelComponentKey.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/wmshell/BubblesManagerKosmos.kt
@@ -14,7 +14,9 @@
* limitations under the License.
*/
-package com.android.systemui.volume.panel
+package com.android.systemui.wmshell
-/** Uniquely identifies the [com.android.systemui.volume.panel.ui.VolumePanelComponent]. */
-typealias VolumePanelComponentKey = String
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.util.mockito.mock
+
+var Kosmos.bubblesManager by Kosmos.Fixture { mock<BubblesManager>() }
diff --git a/packages/overlays/NavigationBarModeGesturalOverlay/res/values-sw600dp/config.xml b/packages/overlays/NavigationBarModeGesturalOverlay/res/values-sw600dp/config.xml
new file mode 100644
index 0000000..be1f081
--- /dev/null
+++ b/packages/overlays/NavigationBarModeGesturalOverlay/res/values-sw600dp/config.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/**
+ * 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.
+ */
+-->
+<resources>
+ <!-- If true, attach the navigation bar to the app during app transition -->
+ <bool name="config_attachNavBarToAppDuringTransition">false</bool>
+</resources>
diff --git a/packages/overlays/NavigationBarModeGesturalOverlayExtraWideBack/res/values-sw600dp/config.xml b/packages/overlays/NavigationBarModeGesturalOverlayExtraWideBack/res/values-sw600dp/config.xml
new file mode 100644
index 0000000..be1f081
--- /dev/null
+++ b/packages/overlays/NavigationBarModeGesturalOverlayExtraWideBack/res/values-sw600dp/config.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/**
+ * 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.
+ */
+-->
+<resources>
+ <!-- If true, attach the navigation bar to the app during app transition -->
+ <bool name="config_attachNavBarToAppDuringTransition">false</bool>
+</resources>
diff --git a/packages/overlays/NavigationBarModeGesturalOverlayNarrowBack/res/values-sw600dp/config.xml b/packages/overlays/NavigationBarModeGesturalOverlayNarrowBack/res/values-sw600dp/config.xml
new file mode 100644
index 0000000..be1f081
--- /dev/null
+++ b/packages/overlays/NavigationBarModeGesturalOverlayNarrowBack/res/values-sw600dp/config.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/**
+ * 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.
+ */
+-->
+<resources>
+ <!-- If true, attach the navigation bar to the app during app transition -->
+ <bool name="config_attachNavBarToAppDuringTransition">false</bool>
+</resources>
diff --git a/packages/overlays/NavigationBarModeGesturalOverlayWideBack/res/values-sw600dp/config.xml b/packages/overlays/NavigationBarModeGesturalOverlayWideBack/res/values-sw600dp/config.xml
new file mode 100644
index 0000000..be1f081
--- /dev/null
+++ b/packages/overlays/NavigationBarModeGesturalOverlayWideBack/res/values-sw600dp/config.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/**
+ * 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.
+ */
+-->
+<resources>
+ <!-- If true, attach the navigation bar to the app during app transition -->
+ <bool name="config_attachNavBarToAppDuringTransition">false</bool>
+</resources>
diff --git a/ravenwood/framework-minus-apex-ravenwood-policies.txt b/ravenwood/framework-minus-apex-ravenwood-policies.txt
index f5e4af5..16f99e9 100644
--- a/ravenwood/framework-minus-apex-ravenwood-policies.txt
+++ b/ravenwood/framework-minus-apex-ravenwood-policies.txt
@@ -6,6 +6,9 @@
# Keep all feature flag implementations
class :feature_flags stubclass
+# Keep all sysprops generated code implementations
+class :sysprops stubclass
+
# Collections
class android.util.ArrayMap stubclass
class android.util.ArraySet stubclass
@@ -112,6 +115,12 @@
class android.os.PatternMatcher stubclass
class android.os.ParcelUuid stubclass
+# Logging related interfaces from modules-utils
+class com.android.internal.logging.InstanceId stubclass
+class com.android.internal.logging.InstanceIdSequence stubclass
+class com.android.internal.logging.UiEvent stubclass
+class com.android.internal.logging.UiEventLogger stubclass
+
# XML
class com.android.internal.util.XmlPullParserWrapper stubclass
class com.android.internal.util.XmlSerializerWrapper stubclass
@@ -129,6 +138,9 @@
class android.net.Uri stubclass
class android.net.UriCodec stubclass
+# Telephony
+class android.telephony.PinResult stubclass
+
# Just enough to support mocking, no further functionality
class android.content.Context stub
method <init> ()V stub
diff --git a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java
index eacdc2f..91c522e 100644
--- a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java
+++ b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java
@@ -19,8 +19,6 @@
import android.os.HandlerThread;
import android.os.Looper;
-import java.util.Objects;
-
public class RavenwoodRuleImpl {
private static final String MAIN_THREAD_NAME = "RavenwoodMain";
@@ -31,6 +29,10 @@
public static void init(RavenwoodRule rule) {
android.os.Process.init$ravenwood(rule.mUid, rule.mPid);
android.os.Binder.init$ravenwood();
+ android.os.SystemProperties.init$ravenwood(
+ rule.mSystemProperties.getValues(),
+ rule.mSystemProperties.getKeyReadablePredicate(),
+ rule.mSystemProperties.getKeyWritablePredicate());
com.android.server.LocalServices.removeAllServicesForTest();
@@ -49,7 +51,8 @@
com.android.server.LocalServices.removeAllServicesForTest();
- android.os.Process.reset$ravenwood();
+ android.os.SystemProperties.reset$ravenwood();
android.os.Binder.reset$ravenwood();
+ android.os.Process.reset$ravenwood();
}
}
diff --git a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java
index 53da8ba..dd442f0 100644
--- a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java
+++ b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java
@@ -62,6 +62,8 @@
boolean mProvideMainThread = false;
+ final RavenwoodSystemProperties mSystemProperties = new RavenwoodSystemProperties();
+
public RavenwoodRule() {
}
@@ -98,6 +100,40 @@
return this;
}
+ /**
+ * Configure the given system property as immutable for the duration of the test.
+ * Read access to the key is allowed, and write access will fail. When {@code value} is
+ * {@code null}, the value is left as undefined.
+ *
+ * All properties in the {@code debug.*} namespace are automatically mutable, with no
+ * developer action required.
+ *
+ * Has no effect under non-Ravenwood environments.
+ */
+ public Builder setSystemPropertyImmutable(/* @NonNull */ String key,
+ /* @Nullable */ Object value) {
+ mRule.mSystemProperties.setValue(key, value);
+ mRule.mSystemProperties.setAccessReadOnly(key);
+ return this;
+ }
+
+ /**
+ * Configure the given system property as mutable for the duration of the test.
+ * Both read and write access to the key is allowed, and its value will be reset between
+ * each test. When {@code value} is {@code null}, the value is left as undefined.
+ *
+ * All properties in the {@code debug.*} namespace are automatically mutable, with no
+ * developer action required.
+ *
+ * Has no effect under non-Ravenwood environments.
+ */
+ public Builder setSystemPropertyMutable(/* @NonNull */ String key,
+ /* @Nullable */ Object value) {
+ mRule.mSystemProperties.setValue(key, value);
+ mRule.mSystemProperties.setAccessReadWrite(key);
+ return this;
+ }
+
public RavenwoodRule build() {
return mRule;
}
diff --git a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodSystemProperties.java b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodSystemProperties.java
new file mode 100644
index 0000000..85ad4e4
--- /dev/null
+++ b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodSystemProperties.java
@@ -0,0 +1,175 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.platform.test.ravenwood;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Predicate;
+
+class RavenwoodSystemProperties {
+ private final Map<String, String> mValues = new HashMap<>();
+
+ /** Set of additional keys that should be considered readable */
+ private final Set<String> mKeyReadable = new HashSet<>();
+ private final Predicate<String> mKeyReadablePredicate = (key) -> {
+ final String root = getKeyRoot(key);
+
+ if (root.startsWith("debug.")) return true;
+
+ // This set is carefully curated to help identify situations where a test may
+ // accidentally depend on a default value of an obscure property whose owner hasn't
+ // decided how Ravenwood should behave.
+ if (root.startsWith("boot.")) return true;
+ if (root.startsWith("build.")) return true;
+ if (root.startsWith("product.")) return true;
+ if (root.startsWith("soc.")) return true;
+ if (root.startsWith("system.")) return true;
+
+ switch (key) {
+ case "gsm.version.baseband":
+ case "no.such.thing":
+ case "ro.bootloader":
+ case "ro.debuggable":
+ case "ro.hardware":
+ case "ro.hw_timeout_multiplier":
+ case "ro.odm.build.media_performance_class":
+ case "ro.treble.enabled":
+ case "ro.vndk.version":
+ return true;
+ }
+
+ return mKeyReadable.contains(key);
+ };
+
+ /** Set of additional keys that should be considered writable */
+ private final Set<String> mKeyWritable = new HashSet<>();
+ private final Predicate<String> mKeyWritablePredicate = (key) -> {
+ final String root = getKeyRoot(key);
+
+ if (root.startsWith("debug.")) return true;
+
+ return mKeyWritable.contains(key);
+ };
+
+ public RavenwoodSystemProperties() {
+ // TODO: load these values from build.prop generated files
+ setValueForPartitions("product.brand", "Android");
+ setValueForPartitions("product.device", "Ravenwood");
+ setValueForPartitions("product.manufacturer", "Android");
+ setValueForPartitions("product.model", "Ravenwood");
+ setValueForPartitions("product.name", "Ravenwood");
+
+ setValueForPartitions("product.cpu.abilist", "x86_64");
+ setValueForPartitions("product.cpu.abilist32", "");
+ setValueForPartitions("product.cpu.abilist64", "x86_64");
+
+ setValueForPartitions("build.date", "Thu Jan 01 00:00:00 GMT 2024");
+ setValueForPartitions("build.date.utc", "1704092400");
+ setValueForPartitions("build.id", "MAIN");
+ setValueForPartitions("build.tags", "dev-keys");
+ setValueForPartitions("build.type", "userdebug");
+ setValueForPartitions("build.version.all_codenames", "REL");
+ setValueForPartitions("build.version.codename", "REL");
+ setValueForPartitions("build.version.incremental", "userdebug.ravenwood.20240101");
+ setValueForPartitions("build.version.known_codenames", "REL");
+ setValueForPartitions("build.version.release", "14");
+ setValueForPartitions("build.version.release_or_codename", "VanillaIceCream");
+ setValueForPartitions("build.version.sdk", "34");
+
+ setValue("ro.board.first_api_level", "1");
+ setValue("ro.product.first_api_level", "1");
+
+ setValue("ro.soc.manufacturer", "Android");
+ setValue("ro.soc.model", "Ravenwood");
+
+ setValue("ro.debuggable", "1");
+ }
+
+ Map<String, String> getValues() {
+ return new HashMap<>(mValues);
+ }
+
+ Predicate<String> getKeyReadablePredicate() {
+ return mKeyReadablePredicate;
+ }
+
+ Predicate<String> getKeyWritablePredicate() {
+ return mKeyWritablePredicate;
+ }
+
+ private static final String[] PARTITIONS = {
+ "bootimage",
+ "odm",
+ "product",
+ "system",
+ "system_ext",
+ "vendor",
+ "vendor_dlkm",
+ };
+
+ /**
+ * Set the given property for all possible partitions where it could be defined. For
+ * example, the value of {@code ro.build.type} is typically also mirrored under
+ * {@code ro.system.build.type}, etc.
+ */
+ private void setValueForPartitions(String key, String value) {
+ setValue("ro." + key, value);
+ for (String partition : PARTITIONS) {
+ setValue("ro." + partition + "." + key, value);
+ }
+ }
+
+ public void setValue(String key, Object value) {
+ final String valueString = (value == null) ? null : String.valueOf(value);
+ if ((valueString == null) || valueString.isEmpty()) {
+ mValues.remove(key);
+ } else {
+ mValues.put(key, valueString);
+ }
+ }
+
+ public void setAccessNone(String key) {
+ mKeyReadable.remove(key);
+ mKeyWritable.remove(key);
+ }
+
+ public void setAccessReadOnly(String key) {
+ mKeyReadable.add(key);
+ mKeyWritable.remove(key);
+ }
+
+ public void setAccessReadWrite(String key) {
+ mKeyReadable.add(key);
+ mKeyWritable.add(key);
+ }
+
+ /**
+ * Return the "root" of the given property key, stripping away any modifier prefix such as
+ * {@code ro.} or {@code persist.}.
+ */
+ private static String getKeyRoot(String key) {
+ if (key.startsWith("ro.")) {
+ return key.substring(3);
+ } else if (key.startsWith("persist.")) {
+ return key.substring(8);
+ } else {
+ return key;
+ }
+ }
+}
diff --git a/ravenwood/ravenwood-annotation-allowed-classes.txt b/ravenwood/ravenwood-annotation-allowed-classes.txt
index ab2546b..eaf01a3 100644
--- a/ravenwood/ravenwood-annotation-allowed-classes.txt
+++ b/ravenwood/ravenwood-annotation-allowed-classes.txt
@@ -1,6 +1,10 @@
# Only classes listed here can use the Ravenwood annotations.
+com.android.internal.display.BrightnessSynchronizer
com.android.internal.util.ArrayUtils
+com.android.internal.logging.MetricsLogger
+com.android.internal.logging.testing.FakeMetricsLogger
+com.android.internal.logging.testing.UiEventLoggerFake
com.android.internal.os.BatteryStatsHistory
com.android.internal.os.BatteryStatsHistory$TraceDelegate
com.android.internal.os.BatteryStatsHistory$VarintParceler
@@ -28,6 +32,7 @@
android.util.MonthDisplayHelper
android.util.RecurrenceRule
android.util.RotationUtils
+android.util.Singleton
android.util.Slog
android.util.SparseDoubleArray
android.util.SparseSetArray
@@ -47,6 +52,7 @@
android.os.Binder
android.os.Binder$IdentitySupplier
android.os.Broadcaster
+android.os.Build
android.os.BundleMerger
android.os.ConditionVariable
android.os.FileUtils
@@ -65,11 +71,14 @@
android.os.Process
android.os.ServiceSpecificException
android.os.SystemClock
+android.os.SystemProperties
android.os.ThreadLocalWorkSource
android.os.TimestampedValue
+android.os.Trace
android.os.UidBatteryConsumer
android.os.UidBatteryConsumer$Builder
android.os.UserHandle
+android.os.UserManager
android.os.WorkSource
android.content.ClipData
@@ -83,16 +92,22 @@
android.content.IntentFilter
android.content.UriMatcher
-android.content.pm.PackageInfo
-android.content.pm.ApplicationInfo
-android.content.pm.PackageItemInfo
-android.content.pm.ComponentInfo
android.content.pm.ActivityInfo
-android.content.pm.ServiceInfo
+android.content.pm.ApplicationInfo
+android.content.pm.ComponentInfo
+android.content.pm.PackageInfo
+android.content.pm.PackageItemInfo
+android.content.pm.PackageManager$Flags
+android.content.pm.PackageManager$PackageInfoFlags
+android.content.pm.PackageManager$ApplicationInfoFlags
+android.content.pm.PackageManager$ComponentInfoFlags
+android.content.pm.PackageManager$ResolveInfoFlags
android.content.pm.PathPermission
android.content.pm.ProviderInfo
android.content.pm.ResolveInfo
+android.content.pm.ServiceInfo
android.content.pm.Signature
+android.content.pm.UserInfo
android.database.AbstractCursor
android.database.CharArrayBuffer
@@ -126,6 +141,12 @@
android.content.ContentProvider
+android.metrics.LogMaker
+
+android.view.Display$HdrCapabilities
+android.view.Display$Mode
+android.view.DisplayInfo
+
com.android.server.LocalServices
com.android.server.power.stats.BatteryStatsImpl
diff --git a/services/accessibility/accessibility.aconfig b/services/accessibility/accessibility.aconfig
index 993b254..44682e2 100644
--- a/services/accessibility/accessibility.aconfig
+++ b/services/accessibility/accessibility.aconfig
@@ -45,6 +45,13 @@
}
flag {
+ name: "fix_drag_pointer_when_ending_drag"
+ namespace: "accessibility"
+ description: "Send the correct pointer id when transitioning from dragging to delegating states."
+ bug: "300002193"
+}
+
+flag {
name: "pinch_zoom_zero_min_span"
namespace: "accessibility"
description: "Whether to set min span of ScaleGestureDetector to zero."
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
index 3d8d7b7..d656892 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
@@ -4413,25 +4413,50 @@
@Override
public boolean isAccessibilityServiceWarningRequired(AccessibilityServiceInfo info) {
mSecurityPolicy.enforceCallingOrSelfPermission(Manifest.permission.MANAGE_ACCESSIBILITY);
+ final ComponentName componentName = info.getComponentName();
// Warning is not required if the service is already enabled.
synchronized (mLock) {
final AccessibilityUserState userState = getCurrentUserStateLocked();
- if (userState.getEnabledServicesLocked().contains(info.getComponentName())) {
+ if (userState.getEnabledServicesLocked().contains(componentName)) {
return false;
}
}
// Warning is not required if the service is already assigned to a shortcut.
for (int shortcutType : AccessibilityManager.SHORTCUT_TYPES) {
if (getAccessibilityShortcutTargets(shortcutType).contains(
- info.getComponentName().flattenToString())) {
+ componentName.flattenToString())) {
return false;
}
}
+ // Warning is not required if the service is preinstalled and in the
+ // trustedAccessibilityServices allowlist.
+ if (android.view.accessibility.Flags.skipAccessibilityWarningDialogForTrustedServices()
+ && isAccessibilityServicePreinstalledAndTrusted(info)) {
+ return false;
+ }
+
// Warning is required by default.
return true;
}
+ private boolean isAccessibilityServicePreinstalledAndTrusted(AccessibilityServiceInfo info) {
+ final ComponentName componentName = info.getComponentName();
+ final boolean isPreinstalled =
+ info.getResolveInfo().serviceInfo.applicationInfo.isSystemApp();
+ if (isPreinstalled) {
+ final String[] trustedAccessibilityServices =
+ mContext.getResources().getStringArray(
+ R.array.config_trustedAccessibilityServices);
+ if (Arrays.stream(trustedAccessibilityServices)
+ .map(ComponentName::unflattenFromString)
+ .anyMatch(componentName::equals)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
@Override
public void dump(FileDescriptor fd, final PrintWriter pw, String[] args) {
if (!DumpUtils.checkDumpPermission(mContext, LOG_TAG, pw)) return;
diff --git a/services/accessibility/java/com/android/server/accessibility/gestures/TouchExplorer.java b/services/accessibility/java/com/android/server/accessibility/gestures/TouchExplorer.java
index c418485..3086ce1 100644
--- a/services/accessibility/java/com/android/server/accessibility/gestures/TouchExplorer.java
+++ b/services/accessibility/java/com/android/server/accessibility/gestures/TouchExplorer.java
@@ -1466,8 +1466,11 @@
int policyFlags = mState.getLastReceivedPolicyFlags();
if (mState.isDragging()) {
// Send an event to the end of the drag gesture.
- mDispatcher.sendMotionEvent(
- event, ACTION_UP, rawEvent, ALL_POINTER_ID_BITS, policyFlags);
+ int pointerIdBits = ALL_POINTER_ID_BITS;
+ if (Flags.fixDragPointerWhenEndingDrag()) {
+ pointerIdBits = 1 << mDraggingPointerId;
+ }
+ mDispatcher.sendMotionEvent(event, ACTION_UP, rawEvent, pointerIdBits, policyFlags);
}
mState.startDelegating();
// Deliver all pointers to the view hierarchy.
diff --git a/services/autofill/java/com/android/server/autofill/ui/SaveUi.java b/services/autofill/java/com/android/server/autofill/ui/SaveUi.java
index 42ab05f..4d42f15 100644
--- a/services/autofill/java/com/android/server/autofill/ui/SaveUi.java
+++ b/services/autofill/java/com/android/server/autofill/ui/SaveUi.java
@@ -58,11 +58,13 @@
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewGroup.LayoutParams;
+import android.view.ViewTreeObserver;
import android.view.Window;
import android.view.WindowManager;
import android.view.autofill.AutofillManager;
import android.widget.ImageView;
import android.widget.RemoteViews;
+import android.widget.ScrollView;
import android.widget.TextView;
import com.android.internal.R;
@@ -370,9 +372,23 @@
params.windowAnimations = R.style.AutofillSaveAnimation;
params.setTrustedOverlay();
+ ScrollView scrollView = view.findViewById(R.id.autofill_sheet_scroll_view);
+
+ View divider = view.findViewById(R.id.autofill_sheet_divider);
+
+ ViewTreeObserver observer = scrollView.getViewTreeObserver();
+ observer.addOnGlobalLayoutListener(() -> adjustDividerVisibility(scrollView, divider));
+
+ scrollView.getViewTreeObserver()
+ .addOnScrollChangedListener(() -> adjustDividerVisibility(scrollView, divider));
show();
}
+ private void adjustDividerVisibility(ScrollView scrollView, View divider) {
+ boolean canScrollDown = scrollView.canScrollVertically(1); // 1 to check scrolling down
+ divider.setVisibility(canScrollDown ? View.VISIBLE : View.INVISIBLE);
+ }
+
private boolean applyCustomDescription(@NonNull Context context, @NonNull View saveUiView,
@NonNull ValueFinder valueFinder, @NonNull SaveInfo info) {
final CustomDescription customDescription = info.getCustomDescription();
diff --git a/services/backup/flags.aconfig b/services/backup/flags.aconfig
index 549fa36..1416c88 100644
--- a/services/backup/flags.aconfig
+++ b/services/backup/flags.aconfig
@@ -10,6 +10,15 @@
}
flag {
+ name: "enable_metrics_system_backup_agents"
+ namespace: "onboarding"
+ description: "Enable SystemBackupAgent to collect B&R agent metrics by passing an instance of "
+ "the logger to each BackupHelper."
+ bug: "296844513"
+ is_fixed_read_only: true
+}
+
+flag {
name: "enable_max_size_writes_to_pipes"
namespace: "onboarding"
description: "Enables the write buffer to pipes to be of maximum size."
diff --git a/services/companion/TEST_MAPPING b/services/companion/TEST_MAPPING
index 37c47ba..ae6d591 100644
--- a/services/companion/TEST_MAPPING
+++ b/services/companion/TEST_MAPPING
@@ -9,5 +9,10 @@
{
"name": "CtsCompanionDeviceManagerNoCompanionServicesTestCases"
}
+ ],
+ "postsubmit": [
+ {
+ "name": "CtsCompanionDeviceManagerMultiProcessTestCases"
+ }
]
}
diff --git a/services/companion/java/com/android/server/companion/CompanionApplicationController.java b/services/companion/java/com/android/server/companion/CompanionApplicationController.java
index c2d2468..586aa8a 100644
--- a/services/companion/java/com/android/server/companion/CompanionApplicationController.java
+++ b/services/companion/java/com/android/server/companion/CompanionApplicationController.java
@@ -22,9 +22,11 @@
import android.annotation.UserIdInt;
import android.companion.AssociationInfo;
import android.companion.CompanionDeviceService;
+import android.companion.DevicePresenceEvent;
import android.content.ComponentName;
import android.content.Context;
import android.os.Handler;
+import android.os.ParcelUuid;
import android.util.Log;
import android.util.Slog;
import android.util.SparseArray;
@@ -46,7 +48,8 @@
* the services, maintaining the connection (the binding), and invoking callback methods such as
* {@link CompanionDeviceService#onDeviceAppeared(AssociationInfo)},
* {@link CompanionDeviceService#onDeviceDisappeared(AssociationInfo)} and
- * {@link CompanionDeviceService#onDeviceEvent(AssociationInfo, int)} in the application process.
+ * {@link CompanionDeviceService#onDevicePresenceEvent(DevicePresenceEvent)} in the
+ * application process.
*
* <p>
* The following is the list of the APIs provided by {@link CompanionApplicationController} (to be
@@ -54,7 +57,7 @@
* <ul>
* <li> {@link #bindCompanionApplication(int, String, boolean)}
* <li> {@link #unbindCompanionApplication(int, String)}
- * <li> {@link #notifyCompanionApplicationDeviceEvent(AssociationInfo, int)} (AssociationInfo, int)}
+ * <li> {@link #notifyCompanionApplicationDevicePresenceEvent(AssociationInfo, int)}
* <li> {@link #isCompanionApplicationBound(int, String)}
* <li> {@link #isRebindingCompanionApplicationScheduled(int, String)}
* </ul>
@@ -72,6 +75,7 @@
private final @NonNull Context mContext;
private final @NonNull AssociationStore mAssociationStore;
+ private final @NonNull ObservableUuidStore mObservableUuidStore;
private final @NonNull CompanionDevicePresenceMonitor mDevicePresenceMonitor;
private final @NonNull CompanionServicesRegister mCompanionServicesRegister;
@@ -82,9 +86,11 @@
private final @NonNull AndroidPackageMap<Boolean> mScheduledForRebindingCompanionApplications;
CompanionApplicationController(Context context, AssociationStore associationStore,
+ ObservableUuidStore observableUuidStore,
CompanionDevicePresenceMonitor companionDevicePresenceMonitor) {
mContext = context;
mAssociationStore = associationStore;
+ mObservableUuidStore = observableUuidStore;
mDevicePresenceMonitor = companionDevicePresenceMonitor;
mCompanionServicesRegister = new CompanionServicesRegister();
mBoundCompanionApplications = new AndroidPackageMap<>();
@@ -281,25 +287,50 @@
primaryServiceConnector.postOnDeviceDisappeared(association);
}
- void notifyCompanionApplicationDeviceEvent(AssociationInfo association, int event) {
+ void notifyCompanionApplicationDevicePresenceEvent(AssociationInfo association, int event) {
final int userId = association.getUserId();
final String packageName = association.getPackageName();
final CompanionDeviceServiceConnector primaryServiceConnector =
getPrimaryServiceConnector(userId, packageName);
+ final DevicePresenceEvent devicePresenceEvent =
+ new DevicePresenceEvent(association.getId(), event, null);
if (primaryServiceConnector == null) {
- Slog.e(TAG, "notifyCompanionApplicationDeviceEvent(): "
+ Slog.e(TAG, "notifyCompanionApplicationDevicePresenceEvent(): "
+ "u" + userId + "/" + packageName
+ " event=[ " + event + " ] is NOT bound.");
Slog.e(TAG, "Stacktrace", new Throwable());
return;
}
- Slog.i(TAG, "Calling onDeviceEvent() to userId=[" + userId + "] package=["
+ Slog.i(TAG, "Calling onDevicePresenceEvent() to userId=[" + userId + "] package=["
+ packageName + "] associationId=[" + association.getId()
- + "] state=[" + event + "]");
+ + "] event=[" + event + "]");
- primaryServiceConnector.postOnDeviceEvent(association, event);
+ primaryServiceConnector.postOnDevicePresenceEvent(devicePresenceEvent);
+ }
+
+ void notifyApplicationDevicePresenceEvent(ObservableUuid uuid, int event) {
+ final int userId = uuid.getUserId();
+ final ParcelUuid parcelUuid = uuid.getUuid();
+ final String packageName = uuid.getPackageName();
+ final CompanionDeviceServiceConnector primaryServiceConnector =
+ getPrimaryServiceConnector(userId, packageName);
+ final DevicePresenceEvent devicePresenceEvent =
+ new DevicePresenceEvent(DevicePresenceEvent.NO_ASSOCIATION, event, parcelUuid);
+
+ if (primaryServiceConnector == null) {
+ Slog.e(TAG, "notifyApplicationDevicePresenceChanged(): "
+ + "u" + userId + "/" + packageName
+ + " event=[ " + event + " ] is NOT bound.");
+ Slog.e(TAG, "Stacktrace", new Throwable());
+ return;
+ }
+
+ Slog.i(TAG, "Calling onDevicePresenceEvent() to userId=[" + userId + "] package=["
+ + packageName + "]" + "event= [" + event + "]");
+
+ primaryServiceConnector.postOnDevicePresenceEvent(devicePresenceEvent);
}
void dump(@NonNull PrintWriter out) {
@@ -364,6 +395,9 @@
// Make sure to clean up the state for all the associations
// that associate with this package.
boolean shouldScheduleRebind = false;
+ boolean shouldScheduleRebindForUuid = false;
+ final List<ObservableUuid> uuids =
+ mObservableUuidStore.getObservableUuidsForPackage(userId, packageName);
for (AssociationInfo ai :
mAssociationStore.getAssociationsForPackage(userId, packageName)) {
@@ -385,7 +419,14 @@
}
}
- return stillAssociated && shouldScheduleRebind;
+ for (ObservableUuid uuid : uuids) {
+ if (mDevicePresenceMonitor.isDeviceUuidPresent(uuid.getUuid())) {
+ shouldScheduleRebindForUuid = true;
+ break;
+ }
+ }
+
+ return (stillAssociated && shouldScheduleRebind) || shouldScheduleRebindForUuid;
}
private class CompanionServicesRegister extends PerUser<Map<String, List<ComponentName>>> {
diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
index 50e1862..2e01ced 100644
--- a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
+++ b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
@@ -20,17 +20,17 @@
import static android.Manifest.permission.ASSOCIATE_COMPANION_DEVICES;
import static android.Manifest.permission.DELIVER_COMPANION_MESSAGES;
import static android.Manifest.permission.MANAGE_COMPANION_DEVICES;
-import static android.Manifest.permission.REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE;
import static android.Manifest.permission.REQUEST_COMPANION_PROFILE_AUTOMOTIVE_PROJECTION;
+import static android.Manifest.permission.REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE;
import static android.Manifest.permission.USE_COMPANION_TRANSPORTS;
import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_VISIBLE;
import static android.companion.AssociationRequest.DEVICE_PROFILE_AUTOMOTIVE_PROJECTION;
-import static android.companion.CompanionDeviceService.DEVICE_EVENT_BLE_APPEARED;
-import static android.companion.CompanionDeviceService.DEVICE_EVENT_BLE_DISAPPEARED;
-import static android.companion.CompanionDeviceService.DEVICE_EVENT_BT_CONNECTED;
-import static android.companion.CompanionDeviceService.DEVICE_EVENT_BT_DISCONNECTED;
-import static android.companion.CompanionDeviceService.DEVICE_EVENT_SELF_MANAGED_APPEARED;
-import static android.companion.CompanionDeviceService.DEVICE_EVENT_SELF_MANAGED_DISAPPEARED;
+import static android.companion.DevicePresenceEvent.EVENT_BLE_APPEARED;
+import static android.companion.DevicePresenceEvent.EVENT_BLE_DISAPPEARED;
+import static android.companion.DevicePresenceEvent.EVENT_BT_CONNECTED;
+import static android.companion.DevicePresenceEvent.EVENT_BT_DISCONNECTED;
+import static android.companion.DevicePresenceEvent.EVENT_SELF_MANAGED_APPEARED;
+import static android.companion.DevicePresenceEvent.EVENT_SELF_MANAGED_DISAPPEARED;
import static android.content.pm.PackageManager.CERT_INPUT_SHA256;
import static android.content.pm.PackageManager.PERMISSION_GRANTED;
import static android.os.Process.SYSTEM_UID;
@@ -45,6 +45,7 @@
import static com.android.server.companion.PackageUtils.getPackageInfo;
import static com.android.server.companion.PermissionsUtils.checkCallerCanManageCompanionDevice;
import static com.android.server.companion.PermissionsUtils.enforceCallerCanManageAssociationsForPackage;
+import static com.android.server.companion.PermissionsUtils.enforceCallerCanObservingDevicePresenceByUuid;
import static com.android.server.companion.PermissionsUtils.enforceCallerIsSystemOr;
import static com.android.server.companion.PermissionsUtils.enforceCallerIsSystemOrCanInteractWithUserId;
import static com.android.server.companion.PermissionsUtils.sanitizeWithCallerChecks;
@@ -75,6 +76,7 @@
import android.companion.IOnMessageReceivedListener;
import android.companion.IOnTransportsChangedListener;
import android.companion.ISystemDataTransferCallback;
+import android.companion.ObservingDevicePresenceRequest;
import android.companion.datatransfer.PermissionSyncRequest;
import android.content.ComponentName;
import android.content.Context;
@@ -92,6 +94,7 @@
import android.os.Message;
import android.os.Parcel;
import android.os.ParcelFileDescriptor;
+import android.os.ParcelUuid;
import android.os.PowerManagerInternal;
import android.os.PowerWhitelistManager;
import android.os.RemoteCallbackList;
@@ -132,6 +135,7 @@
import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
@@ -221,6 +225,8 @@
private CrossDeviceSyncController mCrossDeviceSyncController;
+ private ObservableUuidStore mObservableUuidStore;
+
public CompanionDeviceManagerService(Context context) {
super(context);
@@ -240,6 +246,7 @@
mOnPackageVisibilityChangeListener =
new OnPackageVisibilityChangeListener(mActivityManager);
mPowerManagerInternal = LocalServices.getService(PowerManagerInternal.class);
+ mObservableUuidStore = new ObservableUuidStore();
}
@Override
@@ -247,24 +254,27 @@
final Context context = getContext();
mPersistentStore = new PersistentDataStore();
+ mAssociationRequestsProcessor = new AssociationRequestsProcessor(
+ /* cdmService */ this, mAssociationStore);
+ mBackupRestoreProcessor = new BackupRestoreProcessor(
+ /* cdmService */ this, mAssociationStore, mPersistentStore,
+ mSystemDataTransferRequestStore, mAssociationRequestsProcessor);
loadAssociationsFromDisk();
+
+ mObservableUuidStore.getObservableUuidsForUser(getContext().getUserId());
+
mAssociationStore.registerListener(mAssociationStoreChangeListener);
mDevicePresenceMonitor = new CompanionDevicePresenceMonitor(mUserManager,
- mAssociationStore, mDevicePresenceCallback);
+ mAssociationStore, mObservableUuidStore, mDevicePresenceCallback);
- mAssociationRequestsProcessor = new AssociationRequestsProcessor(
- /* cdmService */this, mAssociationStore);
mCompanionAppController = new CompanionApplicationController(
- context, mAssociationStore, mDevicePresenceMonitor);
+ context, mAssociationStore, mObservableUuidStore, mDevicePresenceMonitor);
mTransportManager = new CompanionTransportManager(context, mAssociationStore);
mSystemDataTransferProcessor = new SystemDataTransferProcessor(this,
mPackageManagerInternal, mAssociationStore,
mSystemDataTransferRequestStore, mTransportManager);
- mBackupRestoreProcessor = new BackupRestoreProcessor(
- /* cdmService */ this, mAssociationStore, mPersistentStore,
- mSystemDataTransferRequestStore, mAssociationRequestsProcessor);
// TODO(b/279663946): move context sync to a dedicated system service
mCrossDeviceSyncController = new CrossDeviceSyncController(getContext(), mTransportManager);
@@ -352,13 +362,29 @@
final int userId = user.getUserIdentifier();
final Set<BluetoothDevice> blueToothDevices =
mDevicePresenceMonitor.getPendingConnectedDevices().get(userId);
+
+ final List<ObservableUuid> observableUuids =
+ mObservableUuidStore.getObservableUuidsForUser(userId);
+
if (blueToothDevices != null) {
for (BluetoothDevice bluetoothDevice : blueToothDevices) {
+ final List<ParcelUuid> deviceUuids = bluetoothDevice.getUuids() == null
+ ? Collections.emptyList() : Arrays.asList(bluetoothDevice.getUuids());
+
for (AssociationInfo ai:
mAssociationStore.getAssociationsByAddress(bluetoothDevice.getAddress())) {
Slog.i(TAG, "onUserUnlocked, device id( " + ai.getId() + " ) is connected");
mDevicePresenceMonitor.onBluetoothCompanionDeviceConnected(ai.getId());
}
+
+ for (ObservableUuid observableUuid : observableUuids) {
+ if (deviceUuids.contains(observableUuid.getUuid())) {
+ Slog.i(TAG, "onUserUnlocked, UUID( "
+ + observableUuid.getUuid() + " ) is connected");
+ mDevicePresenceMonitor.onDevicePresenceEventByUuid(
+ observableUuid, EVENT_BT_CONNECTED);
+ }
+ }
}
}
}
@@ -423,31 +449,31 @@
}
}
- private void onDeviceEventInternal(int associationId, int event) {
- Slog.i(TAG, "onDeviceEventInternal() id=" + associationId + " event= " + event);
+ private void onDevicePresenceEventInternal(int associationId, int event) {
+ Slog.i(TAG, "onDevicePresenceEventInternal() id=" + associationId + " event= " + event);
final AssociationInfo association = mAssociationStore.getAssociationById(associationId);
final String packageName = association.getPackageName();
final int userId = association.getUserId();
switch (event) {
- case DEVICE_EVENT_BLE_APPEARED:
- case DEVICE_EVENT_BT_CONNECTED:
- case DEVICE_EVENT_SELF_MANAGED_APPEARED:
+ case EVENT_BLE_APPEARED:
+ case EVENT_BT_CONNECTED:
+ case EVENT_SELF_MANAGED_APPEARED:
if (!association.shouldBindWhenPresent()) return;
bindApplicationIfNeeded(association);
- mCompanionAppController.notifyCompanionApplicationDeviceEvent(
+ mCompanionAppController.notifyCompanionApplicationDevicePresenceEvent(
association, event);
break;
- case DEVICE_EVENT_BLE_DISAPPEARED:
- case DEVICE_EVENT_BT_DISCONNECTED:
- case DEVICE_EVENT_SELF_MANAGED_DISAPPEARED:
+ case EVENT_BLE_DISAPPEARED:
+ case EVENT_BT_DISCONNECTED:
+ case EVENT_SELF_MANAGED_DISAPPEARED:
if (!mCompanionAppController.isCompanionApplicationBound(userId, packageName)) {
if (DEBUG) Log.w(TAG, "u" + userId + "\\" + packageName + " is NOT bound");
return;
}
if (association.shouldBindWhenPresent()) {
- mCompanionAppController.notifyCompanionApplicationDeviceEvent(
+ mCompanionAppController.notifyCompanionApplicationDevicePresenceEvent(
association, event);
}
// Check if there are other devices associated to the app that are present.
@@ -460,6 +486,45 @@
}
}
+ private void onDevicePresenceEventByUuidInternal(ObservableUuid uuid, int event) {
+ Slog.i(TAG, "onDevicePresenceEventByUuidInternal() id=" + uuid.getUuid()
+ + "for package=" + uuid.getPackageName() + " event=" + event);
+ final String packageName = uuid.getPackageName();
+ final int userId = uuid.getUserId();
+
+ switch(event) {
+ case EVENT_BT_CONNECTED:
+ if (!mCompanionAppController.isCompanionApplicationBound(userId, packageName)) {
+ mCompanionAppController.bindCompanionApplication(
+ userId, packageName, /*bindImportant*/ false);
+
+ } else if (DEBUG) {
+ Log.i(TAG, "u" + userId + "\\" + packageName + " is already bound");
+ }
+
+ mCompanionAppController.notifyApplicationDevicePresenceEvent(uuid, event);
+
+ break;
+ case EVENT_BT_DISCONNECTED:
+ if (!mCompanionAppController.isCompanionApplicationBound(userId, packageName)) {
+ if (DEBUG) Log.w(TAG, "u" + userId + "\\" + packageName + " is NOT bound");
+ return;
+ }
+
+ mCompanionAppController.notifyApplicationDevicePresenceEvent(uuid, event);
+ // Check if there are other devices associated to the app or the UUID to be
+ // observed are present.
+ if (shouldBindPackage(userId, packageName)) return;
+
+ mCompanionAppController.unbindCompanionApplication(userId, packageName);
+
+ break;
+ default:
+ Slog.e(TAG, "Event: " + event + "is not supported");
+ break;
+ }
+ }
+
private void bindApplicationIfNeeded(AssociationInfo association) {
final String packageName = association.getPackageName();
final int userId = association.getUserId();
@@ -476,15 +541,26 @@
/**
* @return whether the package should be bound (i.e. at least one of the devices associated with
- * the package is currently present).
+ * the package is currently present OR the UUID to be observed by this package is
+ * currently present).
*/
private boolean shouldBindPackage(@UserIdInt int userId, @NonNull String packageName) {
final List<AssociationInfo> packageAssociations =
mAssociationStore.getAssociationsForPackage(userId, packageName);
+ final List<ObservableUuid> observableUuids =
+ mObservableUuidStore.getObservableUuidsForPackage(userId, packageName);
+
for (AssociationInfo association : packageAssociations) {
if (!association.shouldBindWhenPresent()) continue;
if (mDevicePresenceMonitor.isDevicePresent(association.getId())) return true;
}
+
+ for (ObservableUuid uuid : observableUuids) {
+ if (mDevicePresenceMonitor.isDeviceUuidPresent(uuid.getUuid())) {
+ return true;
+ }
+ }
+
return false;
}
@@ -568,6 +644,8 @@
// Clear associations.
final List<AssociationInfo> associationsForPackage =
mAssociationStore.getAssociationsForPackage(userId, packageName);
+ final List<ObservableUuid> uuidsTobeObserved =
+ mObservableUuidStore.getObservableUuidsForPackage(userId, packageName);
for (AssociationInfo association : associationsForPackage) {
mAssociationStore.removeAssociation(association.getId());
}
@@ -575,6 +653,10 @@
for (AssociationInfo association : associationsForPackage) {
maybeRemoveRoleHolderForAssociation(association);
}
+ // Clear the uuids to be observed.
+ for (ObservableUuid uuid : uuidsTobeObserved) {
+ mObservableUuidStore.removeObservableUuid(userId, uuid.getUuid(), packageName);
+ }
mCompanionAppController.onPackagesChanged(userId);
}
@@ -855,6 +937,95 @@
}
@Override
+ @EnforcePermission(REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE)
+ public void startObservingDevicePresence(ObservingDevicePresenceRequest request,
+ String packageName, int userId) {
+ startObservingDevicePresence_enforcePermission();
+ registerDevicePresenceListener(request, packageName, userId, /* active */ true);
+ }
+
+ @Override
+ @EnforcePermission(REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE)
+ public void stopObservingDevicePresence(ObservingDevicePresenceRequest request,
+ String packageName, int userId) {
+ stopObservingDevicePresence_enforcePermission();
+ registerDevicePresenceListener(request, packageName, userId, /* active */ false);
+ }
+
+ private void registerDevicePresenceListener(ObservingDevicePresenceRequest request,
+ String packageName, int userId, boolean active) {
+ enforceUsesCompanionDeviceFeature(getContext(), userId, packageName);
+ enforceCallerIsSystemOr(userId, packageName);
+
+ final int associationId = request.getAssociationId();
+ final AssociationInfo associationInfo = mAssociationStore.getAssociationById(
+ associationId);
+ final ParcelUuid uuid = request.getUuid();
+
+ if (uuid != null) {
+ enforceCallerCanObservingDevicePresenceByUuid(getContext());
+ if (active) {
+ startObservingDevicePresenceByUuid(uuid, packageName, userId);
+ } else {
+ stopObservingDevicePresenceByUuid(uuid, packageName, userId);
+ }
+ } else if (associationInfo == null) {
+ throw new IllegalArgumentException("App " + packageName
+ + " is not associated with device " + request.getAssociationId()
+ + " for user " + userId);
+ } else {
+ processDevicePresenceListener(
+ associationInfo, userId, packageName, active);
+ }
+ }
+
+ private void startObservingDevicePresenceByUuid(ParcelUuid uuid, String packageName,
+ int userId) {
+ final List<ObservableUuid> observableUuids =
+ mObservableUuidStore.getObservableUuidsForPackage(userId, packageName);
+
+ for (ObservableUuid observableUuid : observableUuids) {
+ if (observableUuid.getUuid().equals(uuid)) {
+ Slog.i(TAG, "The uuid: " + uuid + " for package:" + packageName
+ + "has been already scheduled for observing");
+ return;
+ }
+ }
+
+ final ObservableUuid observableUuid = new ObservableUuid(userId, uuid,
+ packageName, System.currentTimeMillis());
+
+ mObservableUuidStore.writeObservableUuid(userId, observableUuid);
+ }
+
+ private void stopObservingDevicePresenceByUuid(ParcelUuid uuid, String packageName,
+ int userId) {
+ final List<ObservableUuid> uuidsTobeObserved =
+ mObservableUuidStore.getObservableUuidsForPackage(userId, packageName);
+ boolean isScheduledObserving = false;
+
+ for (ObservableUuid observableUuid : uuidsTobeObserved) {
+ if (observableUuid.getUuid().equals(uuid)) {
+ isScheduledObserving = true;
+ break;
+ }
+ }
+
+ if (!isScheduledObserving) {
+ Slog.i(TAG, "The uuid: " + uuid.toString() + " for package:" + packageName
+ + "has NOT been scheduled for observing yet");
+ return;
+ }
+
+ mObservableUuidStore.removeObservableUuid(userId, uuid, packageName);
+ mDevicePresenceMonitor.removeCurrentConnectedUuidDevice(uuid);
+
+ if (!shouldBindPackage(userId, packageName)) {
+ mCompanionAppController.unbindCompanionApplication(userId, packageName);
+ }
+ }
+
+ @Override
public PendingIntent buildPermissionTransferUserConsentIntent(String packageName,
int userId, int associationId) {
return mSystemDataTransferProcessor.buildPermissionTransferUserConsentIntent(
@@ -1002,6 +1173,11 @@
+ " for user " + userId));
}
+ processDevicePresenceListener(association, userId, packageName, active);
+ }
+
+ private void processDevicePresenceListener(AssociationInfo association,
+ int userId, String packageName, boolean active) {
// If already at specified state, then no-op.
if (active == association.isNotifyOnDeviceNearby()) {
if (DEBUG) Log.d(TAG, "Device presence listener is already at desired state.");
@@ -1025,9 +1201,9 @@
if (mDevicePresenceMonitor.isBlePresent(associationId)
|| mDevicePresenceMonitor.isSimulatePresent(associationId)) {
onDeviceAppearedInternal(associationId);
- onDeviceEventInternal(associationId, DEVICE_EVENT_BLE_APPEARED);
+ onDevicePresenceEventInternal(associationId, EVENT_BLE_APPEARED);
} else if (mDevicePresenceMonitor.isBtConnected(associationId)) {
- onDeviceEventInternal(associationId, DEVICE_EVENT_BT_CONNECTED);
+ onDevicePresenceEventInternal(associationId, EVENT_BT_CONNECTED);
}
}
@@ -1518,20 +1694,25 @@
private final CompanionDevicePresenceMonitor.Callback mDevicePresenceCallback =
new CompanionDevicePresenceMonitor.Callback() {
- @Override
- public void onDeviceAppeared(int associationId) {
- onDeviceAppearedInternal(associationId);
- }
+ @Override
+ public void onDeviceAppeared(int associationId) {
+ onDeviceAppearedInternal(associationId);
+ }
- @Override
- public void onDeviceDisappeared(int associationId) {
- onDeviceDisappearedInternal(associationId);
- }
+ @Override
+ public void onDeviceDisappeared(int associationId) {
+ onDeviceDisappearedInternal(associationId);
+ }
- @Override
- public void onDeviceEvent(int associationId, int event) {
- onDeviceEventInternal(associationId, event);
- }
+ @Override
+ public void onDevicePresenceEvent(int associationId, int event) {
+ onDevicePresenceEventInternal(associationId, event);
+ }
+
+ @Override
+ public void onDevicePresenceEventByUuid(ObservableUuid uuid, int event) {
+ onDevicePresenceEventByUuidInternal(uuid, event);
+ }
};
private final PackageMonitor mPackageMonitor = new PackageMonitor() {
diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceServiceConnector.java b/services/companion/java/com/android/server/companion/CompanionDeviceServiceConnector.java
index 928842c..5abdb42 100644
--- a/services/companion/java/com/android/server/companion/CompanionDeviceServiceConnector.java
+++ b/services/companion/java/com/android/server/companion/CompanionDeviceServiceConnector.java
@@ -26,6 +26,7 @@
import android.annotation.UserIdInt;
import android.companion.AssociationInfo;
import android.companion.CompanionDeviceService;
+import android.companion.DevicePresenceEvent;
import android.companion.ICompanionDeviceService;
import android.content.ComponentName;
import android.content.Context;
@@ -106,12 +107,11 @@
void postOnDeviceDisappeared(@NonNull AssociationInfo associationInfo) {
post(companionService -> companionService.onDeviceDisappeared(associationInfo));
}
- void postOnDeviceEvent(@NonNull AssociationInfo associationInfo, int event) {
- post(companionService -> companionService.onDeviceEvent(associationInfo, event));
+
+ void postOnDevicePresenceEvent(@NonNull DevicePresenceEvent event) {
+ post(companionService -> companionService.onDevicePresenceEvent(event));
}
-
-
/**
* Post "unbind" job, which will run *after* all previously posted jobs complete.
*
diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceShellCommand.java b/services/companion/java/com/android/server/companion/CompanionDeviceShellCommand.java
index e5a8c4f..5663434 100644
--- a/services/companion/java/com/android/server/companion/CompanionDeviceShellCommand.java
+++ b/services/companion/java/com/android/server/companion/CompanionDeviceShellCommand.java
@@ -27,6 +27,7 @@
import android.companion.datatransfer.PermissionSyncRequest;
import android.net.MacAddress;
import android.os.Binder;
+import android.os.ParcelUuid;
import android.os.ShellCommand;
import android.util.Base64;
import android.util.proto.ProtoOutputStream;
@@ -80,6 +81,19 @@
mDevicePresenceMonitor.simulateDeviceEvent(associationId, event);
return 0;
}
+
+ if ("simulate-device-uuid-event".equals(cmd) && Flags.devicePresence()) {
+ String uuid = getNextArgRequired();
+ String packageName = getNextArgRequired();
+ int userId = getNextIntArgRequired();
+ int event = getNextIntArgRequired();
+ ObservableUuid observableUuid = new ObservableUuid(
+ userId, ParcelUuid.fromString(uuid), packageName,
+ System.currentTimeMillis());
+ mDevicePresenceMonitor.simulateDeviceEventByUuid(observableUuid, event);
+ return 0;
+ }
+
switch (cmd) {
case "list": {
final int userId = getNextIntArgRequired();
@@ -447,6 +461,16 @@
pw.println(" Case(3): ");
pw.println(" Make CDM act as if the given companion device is BT disconnected ");
pw.println(" USE FOR DEBUGGING AND/OR TESTING PURPOSES ONLY.");
+
+ pw.println(" simulate-device-uuid-event UUID PACKAGE USERID EVENT");
+ pw.println(" Simulate the companion device event changes:");
+ pw.println(" Case(2): ");
+ pw.println(" Make CDM act as if the given DEVICE is BT connected base"
+ + "on the UUID");
+ pw.println(" Case(3): ");
+ pw.println(" Make CDM act as if the given DEVICE is BT disconnected base"
+ + "on the UUID");
+ pw.println(" USE FOR DEBUGGING AND/OR TESTING PURPOSES ONLY.");
}
pw.println(" remove-inactive-associations");
diff --git a/services/companion/java/com/android/server/companion/ObservableUuid.java b/services/companion/java/com/android/server/companion/ObservableUuid.java
new file mode 100644
index 0000000..6ab3188
--- /dev/null
+++ b/services/companion/java/com/android/server/companion/ObservableUuid.java
@@ -0,0 +1,54 @@
+/*
+ * 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.server.companion;
+
+import android.annotation.NonNull;
+import android.annotation.UserIdInt;
+import android.os.ParcelUuid;
+
+public class ObservableUuid {
+ private final int mUserId;
+ private final String mPackageName;
+
+ private final ParcelUuid mUuid;
+
+ private final long mTimeApprovedMs;
+
+ public ObservableUuid(@UserIdInt int userId, @NonNull ParcelUuid uuid,
+ @NonNull String packageName, Long timeApprovedMs) {
+ mUserId = userId;
+ mUuid = uuid;
+ mPackageName = packageName;
+ mTimeApprovedMs = timeApprovedMs;
+ }
+
+ public int getUserId() {
+ return mUserId;
+ }
+
+ public ParcelUuid getUuid() {
+ return mUuid;
+ }
+
+ public String getPackageName() {
+ return mPackageName;
+ }
+
+ public long getTimeApprovedMs() {
+ return mTimeApprovedMs;
+ }
+}
diff --git a/services/companion/java/com/android/server/companion/ObservableUuidStore.java b/services/companion/java/com/android/server/companion/ObservableUuidStore.java
new file mode 100644
index 0000000..94be22a
--- /dev/null
+++ b/services/companion/java/com/android/server/companion/ObservableUuidStore.java
@@ -0,0 +1,295 @@
+/*
+ * 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.server.companion;
+
+import static com.android.internal.util.XmlUtils.readIntAttribute;
+import static com.android.internal.util.XmlUtils.readLongAttribute;
+import static com.android.internal.util.XmlUtils.readStringAttribute;
+import static com.android.internal.util.XmlUtils.writeIntAttribute;
+import static com.android.internal.util.XmlUtils.writeLongAttribute;
+import static com.android.internal.util.XmlUtils.writeStringAttribute;
+import static com.android.server.companion.DataStoreUtils.createStorageFileForUser;
+import static com.android.server.companion.DataStoreUtils.isEndOfTag;
+import static com.android.server.companion.DataStoreUtils.isStartOfTag;
+import static com.android.server.companion.DataStoreUtils.writeToFileSafely;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.UserIdInt;
+import android.os.ParcelUuid;
+import android.util.AtomicFile;
+import android.util.Slog;
+import android.util.SparseArray;
+import android.util.Xml;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.util.XmlUtils;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
+
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+public class ObservableUuidStore {
+ private static final String TAG = "CDM_ObservableUuidStore";
+ private static final String FILE_NAME = "observing_uuids_presence.xml";
+ private static final String XML_TAG_UUIDS = "uuids";
+ private static final String XML_TAG_UUID = "uuid";
+ private static final String XML_ATTR_UUID = "uuid";
+ private static final String XML_ATTR_TIME_APPROVED = "time_approved";
+ private static final String XML_ATTR_USER_ID = "user_id";
+ private static final String XML_ATTR_PACKAGE = "package_name";
+ private static final int READ_FROM_DISK_TIMEOUT = 5; // in seconds
+
+
+ private final ExecutorService mExecutor;
+ private final ConcurrentMap<Integer, AtomicFile> mUserIdToStorageFile =
+ new ConcurrentHashMap<>();
+
+ private final Object mLock = new Object();
+
+ @GuardedBy("mLock")
+ private final SparseArray<List<ObservableUuid>> mCachedPerUser =
+ new SparseArray<>();
+
+ public ObservableUuidStore() {
+ mExecutor = Executors.newSingleThreadExecutor();
+ }
+
+ /**
+ * Remove the observable uuid from the disk.
+ */
+ void removeObservableUuid(@UserIdInt int userId, ParcelUuid uuid, String packageName) {
+ List<ObservableUuid> cachedObservableUuids;
+
+ synchronized (mLock) {
+ // Remove requests from cache
+ cachedObservableUuids = readObservableUuidsFromCache(userId);
+ cachedObservableUuids.removeIf(
+ uuid1 -> uuid1.getPackageName().equals(packageName)
+ && uuid1.getUuid().equals(uuid));
+ mCachedPerUser.set(userId, cachedObservableUuids);
+ }
+ // Remove requests from store
+ mExecutor.execute(() -> writeObservableUuidToStore(userId, cachedObservableUuids));
+ }
+
+ void writeObservableUuid(@UserIdInt int userId, ObservableUuid uuid) {
+ Slog.i(TAG, "Writing uuid=" + uuid.getUuid() + " to store.");
+
+ List<ObservableUuid> cachedObservableUuids;
+ synchronized (mLock) {
+ // Write to cache
+ cachedObservableUuids = readObservableUuidsFromCache(userId);
+ cachedObservableUuids.removeIf(uuid1 -> uuid1.getUuid().equals(
+ uuid.getUuid()) && uuid1.getPackageName().equals(uuid.getPackageName()));
+ cachedObservableUuids.add(uuid);
+ mCachedPerUser.set(userId, cachedObservableUuids);
+ }
+ // Write to store
+ mExecutor.execute(() -> writeObservableUuidToStore(userId, cachedObservableUuids));
+ }
+
+ private void writeObservableUuidToStore(@UserIdInt int userId,
+ @NonNull List<ObservableUuid> cachedObservableUuids) {
+ final AtomicFile file = getStorageFileForUser(userId);
+ Slog.i(TAG, "Writing ObservableUuid for user " + userId + " to file="
+ + file.getBaseFile().getPath());
+
+ // getStorageFileForUser() ALWAYS returns the SAME OBJECT, which allows us to synchronize
+ // accesses to the file on the file system using this AtomicFile object.
+ synchronized (file) {
+ writeToFileSafely(file, out -> {
+ final TypedXmlSerializer serializer = Xml.resolveSerializer(out);
+ serializer.setFeature(
+ "http://xmlpull.org/v1/doc/features.html#indent-output", true);
+ serializer.startDocument(null, true);
+ writeObservableUuidToXml(serializer, cachedObservableUuids);
+ serializer.endDocument();
+ });
+ }
+ }
+
+ private void writeObservableUuidToXml(@NonNull TypedXmlSerializer serializer,
+ @Nullable Collection<ObservableUuid> uuids) throws IOException {
+ serializer.startTag(null, XML_TAG_UUIDS);
+
+ for (ObservableUuid uuid : uuids) {
+ writeUuidToXml(serializer, uuid);
+ }
+
+ serializer.endTag(null, XML_TAG_UUIDS);
+ }
+
+ private void writeUuidToXml(@NonNull TypedXmlSerializer serializer,
+ @NonNull ObservableUuid uuid) throws IOException {
+ serializer.startTag(null, XML_TAG_UUID);
+
+ writeIntAttribute(serializer, XML_ATTR_USER_ID, uuid.getUserId());
+ writeStringAttribute(serializer, XML_ATTR_UUID, uuid.getUuid().toString());
+ writeStringAttribute(serializer, XML_ATTR_PACKAGE, uuid.getPackageName());
+ writeLongAttribute(serializer, XML_ATTR_TIME_APPROVED, uuid.getTimeApprovedMs());
+
+ serializer.endTag(null, XML_TAG_UUID);
+ }
+
+ /**
+ * Read the observable UUIDs from the cache.
+ */
+ @GuardedBy("mLock")
+ private List<ObservableUuid> readObservableUuidsFromCache(@UserIdInt int userId) {
+ List<ObservableUuid> cachedObservableUuids = mCachedPerUser.get(userId);
+ if (cachedObservableUuids == null) {
+ Future<List<ObservableUuid>> future =
+ mExecutor.submit(() -> readObservableUuidFromStore(userId));
+ try {
+ cachedObservableUuids = future.get(READ_FROM_DISK_TIMEOUT, TimeUnit.SECONDS);
+ } catch (InterruptedException e) {
+ Slog.e(TAG, "Thread reading ObservableUuid from disk is "
+ + "interrupted.");
+ } catch (ExecutionException e) {
+ Slog.e(TAG, "Error occurred while reading ObservableUuid "
+ + "from disk.");
+ } catch (TimeoutException e) {
+ Slog.e(TAG, "Reading ObservableUuid from disk timed out.");
+ }
+ mCachedPerUser.set(userId, cachedObservableUuids);
+ }
+ return cachedObservableUuids;
+ }
+
+ /**
+ * Reads previously persisted data for the given user
+ *
+ * @param userId Android UserID
+ * @return a list of ObservableUuid
+ */
+ @NonNull
+ public List<ObservableUuid> readObservableUuidFromStore(@UserIdInt int userId) {
+ final AtomicFile file = getStorageFileForUser(userId);
+ Slog.i(TAG, "Reading ObservableUuid for user " + userId + " from "
+ + "file=" + file.getBaseFile().getPath());
+
+ // getStorageFileForUser() ALWAYS returns the SAME OBJECT, which allows us to synchronize
+ // accesses to the file on the file system using this AtomicFile object.
+ synchronized (file) {
+ if (!file.getBaseFile().exists()) {
+ Slog.d(TAG, "File does not exist -> Abort");
+ return new ArrayList<>();
+ }
+ try (FileInputStream in = file.openRead()) {
+ final TypedXmlPullParser parser = Xml.resolvePullParser(in);
+ XmlUtils.beginDocument(parser, XML_TAG_UUIDS);
+
+ return readObservableUuidFromXml(parser);
+ } catch (XmlPullParserException | IOException e) {
+ Slog.e(TAG, "Error while reading requests file", e);
+ return new ArrayList<>();
+ }
+ }
+ }
+
+ @NonNull
+ private List<ObservableUuid> readObservableUuidFromXml(
+ @NonNull TypedXmlPullParser parser) throws XmlPullParserException, IOException {
+ if (!isStartOfTag(parser, XML_TAG_UUIDS)) {
+ throw new XmlPullParserException("The XML doesn't have start tag: " + XML_TAG_UUIDS);
+ }
+
+ List<ObservableUuid> observableUuids = new ArrayList<>();
+
+ while (true) {
+ parser.nextTag();
+ if (isEndOfTag(parser, XML_TAG_UUIDS)) {
+ break;
+ }
+ if (isStartOfTag(parser, XML_TAG_UUID)) {
+ observableUuids.add(readUuidFromXml(parser));
+ }
+ }
+
+ return observableUuids;
+ }
+
+ private ObservableUuid readUuidFromXml(@NonNull TypedXmlPullParser parser)
+ throws XmlPullParserException, IOException {
+ if (!isStartOfTag(parser, XML_TAG_UUID)) {
+ throw new XmlPullParserException("XML doesn't have start tag: " + XML_TAG_UUID);
+ }
+
+ final int userId = readIntAttribute(parser, XML_ATTR_USER_ID);
+ final ParcelUuid uuid = ParcelUuid.fromString(readStringAttribute(parser, XML_ATTR_UUID));
+ final String packageName = readStringAttribute(parser, XML_ATTR_PACKAGE);
+ final Long timeApproved = readLongAttribute(parser, XML_ATTR_TIME_APPROVED);
+
+ return new ObservableUuid(userId, uuid, packageName, timeApproved);
+ }
+
+ /**
+ * Creates and caches {@link AtomicFile} object that represents the back-up file for the given
+ * user.
+ * <p>
+ * IMPORTANT: the method will ALWAYS return the same {@link AtomicFile} object, which makes it
+ * possible to synchronize reads and writes to the file using the returned object.
+ */
+ @NonNull
+ private AtomicFile getStorageFileForUser(@UserIdInt int userId) {
+ return mUserIdToStorageFile.computeIfAbsent(userId,
+ u -> createStorageFileForUser(userId, FILE_NAME));
+ }
+
+ /**
+ * @return A list of ObservableUuids per package.
+ */
+ public List<ObservableUuid> getObservableUuidsForPackage(
+ @UserIdInt int userId, @NonNull String packageName) {
+ final List<ObservableUuid> uuidsTobeObservedPerPackage = new ArrayList<>();
+ synchronized (mLock) {
+ final List<ObservableUuid> uuids = readObservableUuidsFromCache(userId);
+
+ for (ObservableUuid uuid : uuids) {
+ if (uuid.getPackageName().equals(packageName)) {
+ uuidsTobeObservedPerPackage.add(uuid);
+ }
+ }
+ }
+
+ return uuidsTobeObservedPerPackage;
+ }
+
+ /**
+ * @return A list of ObservableUuids per user.
+ */
+ public List<ObservableUuid> getObservableUuidsForUser(@UserIdInt int userId) {
+ synchronized (mLock) {
+ return readObservableUuidsFromCache(userId);
+ }
+ }
+}
diff --git a/services/companion/java/com/android/server/companion/PermissionsUtils.java b/services/companion/java/com/android/server/companion/PermissionsUtils.java
index f4e14df..15bebba 100644
--- a/services/companion/java/com/android/server/companion/PermissionsUtils.java
+++ b/services/companion/java/com/android/server/companion/PermissionsUtils.java
@@ -19,6 +19,7 @@
import static android.Manifest.permission.INTERACT_ACROSS_USERS;
import static android.Manifest.permission.MANAGE_COMPANION_DEVICES;
import static android.Manifest.permission.REQUEST_COMPANION_SELF_MANAGED;
+import static android.Manifest.permission.REQUEST_OBSERVE_DEVICE_UUID_PRESENCE;
import static android.app.AppOpsManager.MODE_ALLOWED;
import static android.companion.AssociationRequest.DEVICE_PROFILE_APP_STREAMING;
import static android.companion.AssociationRequest.DEVICE_PROFILE_AUTOMOTIVE_PROJECTION;
@@ -174,6 +175,14 @@
+ " for u" + userId + "/" + packageName);
}
+ static void enforceCallerCanObservingDevicePresenceByUuid(@NonNull Context context) {
+ if (context.checkCallingPermission(REQUEST_OBSERVE_DEVICE_UUID_PRESENCE)
+ != PERMISSION_GRANTED) {
+ throw new SecurityException("Caller (uid=" + getCallingUid() + ") does not have "
+ + "permissions to request observing device presence base on the UUID");
+ }
+ }
+
/**
* Check if the caller is either:
* <ul>
diff --git a/services/companion/java/com/android/server/companion/presence/BluetoothCompanionDeviceConnectionListener.java b/services/companion/java/com/android/server/companion/presence/BluetoothCompanionDeviceConnectionListener.java
index 6ba85bd..7eca119 100644
--- a/services/companion/java/com/android/server/companion/presence/BluetoothCompanionDeviceConnectionListener.java
+++ b/services/companion/java/com/android/server/companion/presence/BluetoothCompanionDeviceConnectionListener.java
@@ -16,6 +16,9 @@
package com.android.server.companion.presence;
+import static android.companion.DevicePresenceEvent.EVENT_BT_CONNECTED;
+import static android.companion.DevicePresenceEvent.EVENT_BT_DISCONNECTED;
+
import static com.android.server.companion.presence.CompanionDevicePresenceMonitor.DEBUG;
import static com.android.server.companion.presence.Utils.btDeviceToString;
@@ -27,6 +30,7 @@
import android.net.MacAddress;
import android.os.Handler;
import android.os.HandlerExecutor;
+import android.os.ParcelUuid;
import android.os.UserHandle;
import android.os.UserManager;
import android.util.Log;
@@ -35,8 +39,11 @@
import com.android.internal.annotations.GuardedBy;
import com.android.server.companion.AssociationStore;
+import com.android.server.companion.ObservableUuid;
+import com.android.server.companion.ObservableUuidStore;
import java.util.Arrays;
+import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
@@ -53,6 +60,8 @@
void onBluetoothCompanionDeviceConnected(int associationId);
void onBluetoothCompanionDeviceDisconnected(int associationId);
+
+ void onDevicePresenceEventByUuid(ObservableUuid uuid, int event);
}
private final UserManager mUserManager;
@@ -61,6 +70,8 @@
/** A set of ALL connected BT device (not only companion.) */
private final @NonNull Map<MacAddress, BluetoothDevice> mAllConnectedDevices = new HashMap<>();
+ private final @NonNull ObservableUuidStore mObservableUuidStore;
+
/**
* A structure hold the connected BT devices that are pending to be reported to the companion
* app when the user unlocks the local device per userId.
@@ -70,8 +81,10 @@
final SparseArray<Set<BluetoothDevice>> mPendingConnectedDevices = new SparseArray<>();
BluetoothCompanionDeviceConnectionListener(UserManager userManager,
- @NonNull AssociationStore associationStore, @NonNull Callback callback) {
+ @NonNull AssociationStore associationStore,
+ @NonNull ObservableUuidStore observableUuidStore, @NonNull Callback callback) {
mAssociationStore = associationStore;
+ mObservableUuidStore = observableUuidStore;
mCallback = callback;
mUserManager = userManager;
}
@@ -109,7 +122,6 @@
bluetoothDevices.add(device);
mPendingConnectedDevices.put(userId, bluetoothDevices);
}
-
} else {
onDeviceConnectivityChanged(device, true);
}
@@ -155,8 +167,13 @@
}
private void onDeviceConnectivityChanged(@NonNull BluetoothDevice device, boolean connected) {
+ int userId = UserHandle.myUserId();
final List<AssociationInfo> associations =
mAssociationStore.getAssociationsByAddress(device.getAddress());
+ final List<ObservableUuid> observableUuids =
+ mObservableUuidStore.getObservableUuidsForUser(userId);
+ final List<ParcelUuid> deviceUuids = device.getUuids() == null
+ ? Collections.emptyList() : Arrays.asList(device.getUuids());
if (DEBUG) {
Log.d(TAG, "onDevice_ConnectivityChanged() " + btDeviceToString(device)
@@ -177,6 +194,14 @@
mCallback.onBluetoothCompanionDeviceDisconnected(id);
}
}
+
+ for (ObservableUuid uuid : observableUuids) {
+ if (deviceUuids.contains(uuid.getUuid())) {
+ mCallback.onDevicePresenceEventByUuid(
+ uuid, connected ? EVENT_BT_CONNECTED
+ : EVENT_BT_DISCONNECTED);
+ }
+ }
}
@Override
diff --git a/services/companion/java/com/android/server/companion/presence/CompanionDevicePresenceMonitor.java b/services/companion/java/com/android/server/companion/presence/CompanionDevicePresenceMonitor.java
index e42b935..54a4692 100644
--- a/services/companion/java/com/android/server/companion/presence/CompanionDevicePresenceMonitor.java
+++ b/services/companion/java/com/android/server/companion/presence/CompanionDevicePresenceMonitor.java
@@ -16,12 +16,12 @@
package com.android.server.companion.presence;
-import static android.companion.CompanionDeviceService.DEVICE_EVENT_BLE_APPEARED;
-import static android.companion.CompanionDeviceService.DEVICE_EVENT_BLE_DISAPPEARED;
-import static android.companion.CompanionDeviceService.DEVICE_EVENT_BT_CONNECTED;
-import static android.companion.CompanionDeviceService.DEVICE_EVENT_BT_DISCONNECTED;
-import static android.companion.CompanionDeviceService.DEVICE_EVENT_SELF_MANAGED_APPEARED;
-import static android.companion.CompanionDeviceService.DEVICE_EVENT_SELF_MANAGED_DISAPPEARED;
+import static android.companion.DevicePresenceEvent.EVENT_BLE_APPEARED;
+import static android.companion.DevicePresenceEvent.EVENT_BLE_DISAPPEARED;
+import static android.companion.DevicePresenceEvent.EVENT_BT_CONNECTED;
+import static android.companion.DevicePresenceEvent.EVENT_BT_DISCONNECTED;
+import static android.companion.DevicePresenceEvent.EVENT_SELF_MANAGED_APPEARED;
+import static android.companion.DevicePresenceEvent.EVENT_SELF_MANAGED_DISAPPEARED;
import static android.os.Process.ROOT_UID;
import static android.os.Process.SHELL_UID;
@@ -36,12 +36,15 @@
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
+import android.os.ParcelUuid;
import android.os.UserManager;
import android.util.Log;
import android.util.Slog;
import android.util.SparseArray;
import com.android.server.companion.AssociationStore;
+import com.android.server.companion.ObservableUuid;
+import com.android.server.companion.ObservableUuidStore;
import java.io.PrintWriter;
import java.util.HashSet;
@@ -61,7 +64,7 @@
* <li> {@link #isDevicePresent(int)}
* <li> {@link Callback#onDeviceAppeared(int) Callback.onDeviceAppeared(int)}
* <li> {@link Callback#onDeviceDisappeared(int) Callback.onDeviceDisappeared(int)}
- * <li> {@link Callback#onDeviceStateChanged(int, int)}}
+ * <li> {@link Callback#onDevicePresenceEvent(int, int)}}
* </ul>
*/
@SuppressLint("LongLogTag")
@@ -78,11 +81,15 @@
/** Invoked when a companion device no longer seen nearby or disconnects. */
void onDeviceDisappeared(int associationId);
- /**Invoked when device has corresponding event changes. */
- void onDeviceEvent(int associationId, int event);
+ /** Invoked when device has corresponding event changes. */
+ void onDevicePresenceEvent(int associationId, int event);
+
+ /** Invoked when device has corresponding event changes base on the UUID */
+ void onDevicePresenceEventByUuid(ObservableUuid uuid, int event);
}
private final @NonNull AssociationStore mAssociationStore;
+ private final @NonNull ObservableUuidStore mObservableUuidStore;
private final @NonNull Callback mCallback;
private final @NonNull BluetoothCompanionDeviceConnectionListener mBtConnectionListener;
private final @NonNull BleCompanionDeviceScanner mBleScanner;
@@ -94,6 +101,7 @@
private final @NonNull Set<Integer> mConnectedBtDevices = new HashSet<>();
private final @NonNull Set<Integer> mNearbyBleDevices = new HashSet<>();
private final @NonNull Set<Integer> mReportedSelfManagedDevices = new HashSet<>();
+ private final @NonNull Set<ParcelUuid> mConnectedUuidDevices = new HashSet<>();
// Tracking "simulated" presence. Used for debugging and testing only.
private final @NonNull Set<Integer> mSimulated = new HashSet<>();
@@ -101,11 +109,14 @@
new SimulatedDevicePresenceSchedulerHelper();
public CompanionDevicePresenceMonitor(UserManager userManager,
- @NonNull AssociationStore associationStore, @NonNull Callback callback) {
+ @NonNull AssociationStore associationStore,
+ @NonNull ObservableUuidStore observableUuidStore, @NonNull Callback callback) {
mAssociationStore = associationStore;
+ mObservableUuidStore = observableUuidStore;
mCallback = callback;
mBtConnectionListener = new BluetoothCompanionDeviceConnectionListener(userManager,
- associationStore, /* BluetoothCompanionDeviceConnectionListener.Callback */ this);
+ associationStore, mObservableUuidStore,
+ /* BluetoothCompanionDeviceConnectionListener.Callback */ this);
mBleScanner = new BleCompanionDeviceScanner(associationStore,
/* BleCompanionDeviceScanner.Callback */ this);
}
@@ -126,6 +137,20 @@
}
/**
+ * @return current connected UUID devices.
+ */
+ public Set<ParcelUuid> getCurrentConnectedUuidDevices() {
+ return mConnectedUuidDevices;
+ }
+
+ /**
+ * Remove current connected UUID device.
+ */
+ public void removeCurrentConnectedUuidDevice(ParcelUuid uuid) {
+ mConnectedUuidDevices.remove(uuid);
+ }
+
+ /**
* @return whether the associated companion devices is present. I.e. device is nearby (for BLE);
* or devices is connected (for Bluetooth); or reported (by the application) to be
* nearby (for "self-managed" associations).
@@ -138,6 +163,13 @@
}
/**
+ * @return whether the current uuid to be observed is present.
+ */
+ public boolean isDeviceUuidPresent(ParcelUuid uuid) {
+ return mConnectedUuidDevices.contains(uuid);
+ }
+
+ /**
* @return whether the current device is BT connected and had already reported to the app.
*/
@@ -169,8 +201,8 @@
* {@link android.companion.CompanionDeviceManager#notifyDeviceAppeared(int) notifyDeviceAppeared()}
*/
public void onSelfManagedDeviceConnected(int associationId) {
- onDeviceEvent(mReportedSelfManagedDevices,
- associationId, DEVICE_EVENT_SELF_MANAGED_APPEARED);
+ onDevicePresenceEvent(mReportedSelfManagedDevices,
+ associationId, EVENT_SELF_MANAGED_APPEARED);
}
/**
@@ -183,23 +215,23 @@
* {@link android.companion.CompanionDeviceManager#notifyDeviceDisappeared(int) notifyDeviceDisappeared()}
*/
public void onSelfManagedDeviceDisconnected(int associationId) {
- onDeviceEvent(mReportedSelfManagedDevices,
- associationId, DEVICE_EVENT_SELF_MANAGED_DISAPPEARED);
+ onDevicePresenceEvent(mReportedSelfManagedDevices,
+ associationId, EVENT_SELF_MANAGED_DISAPPEARED);
}
/**
* Marks a "self-managed" device as disconnected when binderDied.
*/
public void onSelfManagedDeviceReporterBinderDied(int associationId) {
- onDeviceEvent(mReportedSelfManagedDevices,
- associationId, DEVICE_EVENT_SELF_MANAGED_DISAPPEARED);
+ onDevicePresenceEvent(mReportedSelfManagedDevices,
+ associationId, EVENT_SELF_MANAGED_DISAPPEARED);
}
@Override
public void onBluetoothCompanionDeviceConnected(int associationId) {
Slog.i(TAG, "onBluetoothCompanionDeviceConnected: "
+ "associationId( " + associationId + " )");
- onDeviceEvent(mConnectedBtDevices, associationId, DEVICE_EVENT_BT_CONNECTED);
+ onDevicePresenceEvent(mConnectedBtDevices, associationId, EVENT_BT_CONNECTED);
// Stop scanning for BLE devices when this device is connected
// and there are no other devices to connect to.
if (canStopBleScan()) {
@@ -214,22 +246,53 @@
// Start BLE scanning when the device is disconnected.
mBleScanner.startScan();
- onDeviceEvent(mConnectedBtDevices, associationId, DEVICE_EVENT_BT_DISCONNECTED);
+ onDevicePresenceEvent(mConnectedBtDevices, associationId, EVENT_BT_DISCONNECTED);
}
@Override
+ public void onDevicePresenceEventByUuid(ObservableUuid uuid, int event) {
+ final ParcelUuid parcelUuid = uuid.getUuid();
+
+ switch(event) {
+ case EVENT_BT_CONNECTED:
+ boolean added = mConnectedUuidDevices.add(parcelUuid);
+
+ if (!added) {
+ Slog.w(TAG, "Uuid= " + parcelUuid + "is ALREADY reported as "
+ + "present by this event=" + event);
+ }
+
+ break;
+ case EVENT_BT_DISCONNECTED:
+ final boolean removed = mConnectedUuidDevices.remove(parcelUuid);
+
+ if (!removed) {
+ Slog.w(TAG, "UUID= " + parcelUuid + " was NOT reported "
+ + "as present by this event= " + event);
+
+ return;
+ }
+
+ break;
+ }
+
+ mCallback.onDevicePresenceEventByUuid(uuid, event);
+ }
+
+
+ @Override
public void onBleCompanionDeviceFound(int associationId) {
- onDeviceEvent(mNearbyBleDevices, associationId, DEVICE_EVENT_BLE_APPEARED);
+ onDevicePresenceEvent(mNearbyBleDevices, associationId, EVENT_BLE_APPEARED);
}
@Override
public void onBleCompanionDeviceLost(int associationId) {
- onDeviceEvent(mNearbyBleDevices, associationId, DEVICE_EVENT_BLE_DISAPPEARED);
+ onDevicePresenceEvent(mNearbyBleDevices, associationId, EVENT_BLE_DISAPPEARED);
}
/** FOR DEBUGGING AND/OR TESTING PURPOSES ONLY. */
@TestApi
- public void simulateDeviceEvent(int associationId, int state) {
+ public void simulateDeviceEvent(int associationId, int event) {
// IMPORTANT: this API should only be invoked via the
// 'companiondevice simulate-device-appeared' Shell command, so the only uid-s allowed to
// make this call are SHELL and ROOT.
@@ -238,32 +301,43 @@
// Make sure the association exists.
enforceAssociationExists(associationId);
- switch (state) {
- case DEVICE_EVENT_BLE_APPEARED:
- simulateDeviceAppeared(associationId, state);
+ switch (event) {
+ case EVENT_BLE_APPEARED:
+ simulateDeviceAppeared(associationId, event);
break;
- case DEVICE_EVENT_BT_CONNECTED:
+ case EVENT_BT_CONNECTED:
onBluetoothCompanionDeviceConnected(associationId);
break;
- case DEVICE_EVENT_BLE_DISAPPEARED:
- simulateDeviceDisappeared(associationId, state);
+ case EVENT_BLE_DISAPPEARED:
+ simulateDeviceDisappeared(associationId, event);
break;
- case DEVICE_EVENT_BT_DISCONNECTED:
+ case EVENT_BT_DISCONNECTED:
onBluetoothCompanionDeviceDisconnected(associationId);
break;
default:
- throw new IllegalArgumentException("State: " + state + "is not supported");
+ throw new IllegalArgumentException("Event: " + event + "is not supported");
}
}
+ /** FOR DEBUGGING AND/OR TESTING PURPOSES ONLY. */
+ @TestApi
+ public void simulateDeviceEventByUuid(ObservableUuid uuid, int event) {
+ // IMPORTANT: this API should only be invoked via the
+ // 'companiondevice simulate-device-uuid-events' Shell command, so the only uid-s allowed to
+ // make this call are SHELL and ROOT.
+ // No other caller (including SYSTEM!) should be allowed.
+ enforceCallerShellOrRoot();
+ onDevicePresenceEventByUuid(uuid, event);
+ }
+
private void simulateDeviceAppeared(int associationId, int state) {
- onDeviceEvent(mSimulated, associationId, state);
+ onDevicePresenceEvent(mSimulated, associationId, state);
mSchedulerHelper.scheduleOnDeviceGoneCallForSimulatedDevicePresence(associationId);
}
private void simulateDeviceDisappeared(int associationId, int state) {
mSchedulerHelper.unscheduleOnDeviceGoneCallForSimulatedDevicePresence(associationId);
- onDeviceEvent(mSimulated, associationId, state);
+ onDevicePresenceEvent(mSimulated, associationId, state);
}
private void enforceAssociationExists(int associationId) {
@@ -273,14 +347,14 @@
}
}
- private void onDeviceEvent(@NonNull Set<Integer> presentDevicesForSource,
+ private void onDevicePresenceEvent(@NonNull Set<Integer> presentDevicesForSource,
int associationId, int event) {
- Slog.i(TAG, "onDeviceEvent() id=" + associationId + ", state=" + event);
+ Slog.i(TAG, "onDevicePresenceEvent() id=" + associationId + ", event=" + event);
switch (event) {
- case DEVICE_EVENT_BLE_APPEARED:
- case DEVICE_EVENT_BT_CONNECTED:
- case DEVICE_EVENT_SELF_MANAGED_APPEARED:
+ case EVENT_BLE_APPEARED:
+ case EVENT_BT_CONNECTED:
+ case EVENT_SELF_MANAGED_APPEARED:
final boolean added = presentDevicesForSource.add(associationId);
if (!added) {
@@ -292,9 +366,9 @@
mCallback.onDeviceAppeared(associationId);
break;
- case DEVICE_EVENT_BLE_DISAPPEARED:
- case DEVICE_EVENT_BT_DISCONNECTED:
- case DEVICE_EVENT_SELF_MANAGED_DISAPPEARED:
+ case EVENT_BLE_DISAPPEARED:
+ case EVENT_BT_DISCONNECTED:
+ case EVENT_SELF_MANAGED_DISAPPEARED:
final boolean removed = presentDevicesForSource.remove(associationId);
if (!removed) {
@@ -312,7 +386,7 @@
return;
}
- mCallback.onDeviceEvent(associationId, event);
+ mCallback.onDevicePresenceEvent(associationId, event);
}
/**
@@ -436,7 +510,7 @@
public void handleMessage(@NonNull Message msg) {
final int associationId = msg.what;
if (mSimulated.contains(associationId)) {
- onDeviceEvent(mSimulated, associationId, DEVICE_EVENT_BLE_DISAPPEARED);
+ onDevicePresenceEvent(mSimulated, associationId, EVENT_BLE_DISAPPEARED);
}
}
}
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 b6e1140..2168cb2 100644
--- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java
+++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java
@@ -583,6 +583,11 @@
return associationInfo == null ? null : associationInfo.getDisplayName();
}
+ @Override // Binder call
+ public @NonNull List<String> getAllPersistentDeviceIds() {
+ return new ArrayList<>(mLocalService.getAllPersistentDeviceIds());
+ }
+
// Binder call
@Override
public boolean isValidVirtualDeviceId(int deviceId) {
diff --git a/services/core/Android.bp b/services/core/Android.bp
index fdcd27d..a54a48a 100644
--- a/services/core/Android.bp
+++ b/services/core/Android.bp
@@ -211,7 +211,10 @@
"com_android_wm_shell_flags_lib",
"com.android.server.utils_aconfig-java",
"service-jobscheduler-deviceidle.flags-aconfig-java",
+ "backup_flags_lib",
"policy_flags_lib",
+ "net_flags_lib",
+ "stats_flags_lib",
],
javac_shard_size: 50,
javacflags: [
diff --git a/services/core/java/com/android/server/SystemConfig.java b/services/core/java/com/android/server/SystemConfig.java
index 3483c1a..a493d7a 100644
--- a/services/core/java/com/android/server/SystemConfig.java
+++ b/services/core/java/com/android/server/SystemConfig.java
@@ -95,6 +95,7 @@
private static final int ALLOW_OVERRIDE_APP_RESTRICTIONS = 0x100;
private static final int ALLOW_IMPLICIT_BROADCASTS = 0x200;
private static final int ALLOW_VENDOR_APEX = 0x400;
+ private static final int ALLOW_SIGNATURE_PERMISSIONS = 0x800;
private static final int ALLOW_ALL = ~0;
// property for runtime configuration differentiation
@@ -597,7 +598,7 @@
// Vendors are only allowed to customize these
int vendorPermissionFlag = ALLOW_LIBS | ALLOW_FEATURES | ALLOW_PRIVAPP_PERMISSIONS
- | ALLOW_ASSOCIATIONS | ALLOW_VENDOR_APEX;
+ | ALLOW_SIGNATURE_PERMISSIONS | ALLOW_ASSOCIATIONS | ALLOW_VENDOR_APEX;
if (Build.VERSION.DEVICE_INITIAL_SDK_INT <= Build.VERSION_CODES.O_MR1) {
// For backward compatibility
vendorPermissionFlag |= (ALLOW_PERMISSIONS | ALLOW_APP_CONFIGS);
@@ -649,9 +650,9 @@
// TODO(b/157203468): ALLOW_HIDDENAPI_WHITELISTING must be removed because we prohibited
// the use of hidden APIs from the product partition.
int productPermissionFlag = ALLOW_FEATURES | ALLOW_LIBS | ALLOW_PERMISSIONS
- | ALLOW_APP_CONFIGS | ALLOW_PRIVAPP_PERMISSIONS | ALLOW_HIDDENAPI_WHITELISTING
- | ALLOW_ASSOCIATIONS | ALLOW_OVERRIDE_APP_RESTRICTIONS | ALLOW_IMPLICIT_BROADCASTS
- | ALLOW_VENDOR_APEX;
+ | ALLOW_APP_CONFIGS | ALLOW_PRIVAPP_PERMISSIONS | ALLOW_SIGNATURE_PERMISSIONS
+ | ALLOW_HIDDENAPI_WHITELISTING | ALLOW_ASSOCIATIONS
+ | ALLOW_OVERRIDE_APP_RESTRICTIONS | ALLOW_IMPLICIT_BROADCASTS | ALLOW_VENDOR_APEX;
if (Build.VERSION.DEVICE_INITIAL_SDK_INT <= Build.VERSION_CODES.R) {
// TODO(b/157393157): This must check product interface enforcement instead of
// DEVICE_INITIAL_SDK_INT for the devices without product interface enforcement.
@@ -772,6 +773,8 @@
final boolean allowAppConfigs = (permissionFlag & ALLOW_APP_CONFIGS) != 0;
final boolean allowPrivappPermissions = (permissionFlag & ALLOW_PRIVAPP_PERMISSIONS)
!= 0;
+ final boolean allowSignaturePermissions = (permissionFlag & ALLOW_SIGNATURE_PERMISSIONS)
+ != 0;
final boolean allowOemPermissions = (permissionFlag & ALLOW_OEM_PERMISSIONS) != 0;
final boolean allowApiWhitelisting = (permissionFlag & ALLOW_HIDDENAPI_WHITELISTING)
!= 0;
@@ -1246,6 +1249,38 @@
XmlUtils.skipCurrentTag(parser);
}
} break;
+ case "signature-permissions": {
+ if (allowSignaturePermissions) {
+ // signature permissions from system, apex, vendor, product and
+ // system_ext partitions are stored separately. This is to
+ // prevent xml files in the vendor partition from granting
+ // permissions to signature apps in the system partition and vice versa.
+ boolean vendor = permFile.toPath().startsWith(
+ Environment.getVendorDirectory().toPath() + "/")
+ || permFile.toPath().startsWith(
+ Environment.getOdmDirectory().toPath() + "/");
+ boolean product = permFile.toPath().startsWith(
+ Environment.getProductDirectory().toPath() + "/");
+ boolean systemExt = permFile.toPath().startsWith(
+ Environment.getSystemExtDirectory().toPath() + "/");
+ if (vendor) {
+ readSignatureAppPermissions(parser,
+ mPermissionAllowlist.getVendorSignatureAppAllowlist());
+ } else if (product) {
+ readSignatureAppPermissions(parser,
+ mPermissionAllowlist.getProductSignatureAppAllowlist());
+ } else if (systemExt) {
+ readSignatureAppPermissions(parser,
+ mPermissionAllowlist.getSystemExtSignatureAppAllowlist());
+ } else {
+ readSignatureAppPermissions(parser,
+ mPermissionAllowlist.getSignatureAppAllowlist());
+ }
+ } else {
+ logNotAllowedInPartition(name, permFile, parser);
+ XmlUtils.skipCurrentTag(parser);
+ }
+ } break;
case "oem-permissions": {
if (allowOemPermissions) {
readOemPermissions(parser);
@@ -1655,6 +1690,12 @@
readPermissionAllowlist(parser, allowlist, "privapp-permissions");
}
+ private void readSignatureAppPermissions(@NonNull XmlPullParser parser,
+ @NonNull ArrayMap<String, ArrayMap<String, Boolean>> allowlist)
+ throws IOException, XmlPullParserException {
+ readPermissionAllowlist(parser, allowlist, "signature-permissions");
+ }
+
private void readInstallInUserType(XmlPullParser parser,
Map<String, Set<String>> doInstallMap,
Map<String, Set<String>> nonInstallMap)
diff --git a/services/core/java/com/android/server/am/ActiveServices.java b/services/core/java/com/android/server/am/ActiveServices.java
index 9b1fade..afb8345 100644
--- a/services/core/java/com/android/server/am/ActiveServices.java
+++ b/services/core/java/com/android/server/am/ActiveServices.java
@@ -4491,6 +4491,12 @@
}
}
if (userId > 0) {
+ if (mAm.isSystemUserOnly(sInfo.flags)) {
+ Slog.w(TAG_SERVICE, service + " is only available for the SYSTEM user,"
+ + " calling userId is: " + userId);
+ return null;
+ }
+
if (mAm.isSingleton(sInfo.processName, sInfo.applicationInfo,
sInfo.name, sInfo.flags)
&& mAm.isValidSingletonCall(callingUid, sInfo.applicationInfo.uid)) {
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index 86894fd..ca04e41 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -472,6 +472,8 @@
import com.android.server.pm.snapshot.PackageDataSnapshot;
import com.android.server.power.stats.BatteryStatsImpl;
import com.android.server.sdksandbox.SdkSandboxManagerLocal;
+import com.android.server.stats.pull.StatsPullAtomService;
+import com.android.server.stats.pull.StatsPullAtomServiceInternal;
import com.android.server.uri.GrantUri;
import com.android.server.uri.NeededUriGrants;
import com.android.server.uri.UriGrantsManagerInternal;
@@ -1308,6 +1310,8 @@
*/
final BatteryStatsService mBatteryStatsService;
+ StatsPullAtomServiceInternal mStatsPullAtomServiceInternal;
+
/**
* Information about component usage
*/
@@ -5087,13 +5091,11 @@
intent.addFlags(Intent.FLAG_RECEIVER_NO_ABORT
| Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND
| Intent.FLAG_INCLUDE_STOPPED_PACKAGES);
- final BroadcastOptions bOptions = mUserController.getTemporaryAppAllowlistBroadcastOptions(
- reason);
broadcastIntentLocked(null, null, null, intent, null, null, 0, null, null,
new String[]{android.Manifest.permission.RECEIVE_BOOT_COMPLETED},
null, null, AppOpsManager.OP_NONE,
- bOptions.toBundle(), true,
+ null, true,
false, MY_PID, SYSTEM_UID,
SYSTEM_UID, MY_PID, app.userId);
}
@@ -13763,6 +13765,11 @@
return result;
}
+ boolean isSystemUserOnly(int flags) {
+ return android.multiuser.Flags.enableSystemUserOnlyForServicesAndProviders()
+ && (flags & ServiceInfo.FLAG_SYSTEM_USER_ONLY) != 0;
+ }
+
/**
* Checks to see if the caller is in the same app as the singleton
* component, or the component is in a special app. It allows special apps
@@ -16551,6 +16558,21 @@
final @ProcessCapability int capability) {
mBatteryStatsService.noteUidProcessState(uid, state);
mAppOpsService.updateUidProcState(uid, state, capability);
+ if (StatsPullAtomService.ENABLE_MOBILE_DATA_STATS_AGGREGATED_PULLER) {
+ try {
+ if (mStatsPullAtomServiceInternal == null) {
+ mStatsPullAtomServiceInternal = LocalServices.getService(
+ StatsPullAtomServiceInternal.class);
+ }
+ if (mStatsPullAtomServiceInternal != null) {
+ mStatsPullAtomServiceInternal.noteUidProcessState(uid, state);
+ } else {
+ Slog.d(TAG, "StatsPullAtomService not ready yet");
+ }
+ } catch (Exception e) {
+ Slog.e(TAG, "Exception during logging uid proc state change event", e);
+ }
+ }
if (mTrackingAssociations) {
for (int i1=0, N1=mAssociations.size(); i1<N1; i1++) {
ArrayMap<ComponentName, SparseArray<ArrayMap<String, Association>>> targetComponents
diff --git a/services/core/java/com/android/server/am/AppBatteryExemptionTracker.java b/services/core/java/com/android/server/am/AppBatteryExemptionTracker.java
index b07d9a6..9c2e69b 100644
--- a/services/core/java/com/android/server/am/AppBatteryExemptionTracker.java
+++ b/services/core/java/com/android/server/am/AppBatteryExemptionTracker.java
@@ -520,7 +520,7 @@
/**
* Default value to {@link #mTrackerEnabled}.
*/
- static final boolean DEFAULT_BG_BATTERY_EXEMPTION_ENABLED = true;
+ static final boolean DEFAULT_BG_BATTERY_EXEMPTION_ENABLED = false;
AppBatteryExemptionPolicy(@NonNull Injector injector,
@NonNull AppBatteryExemptionTracker tracker) {
diff --git a/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java b/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java
index 2cac7a0..db0f03f 100644
--- a/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java
+++ b/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java
@@ -1244,9 +1244,11 @@
}
private void deliveryTimeout(@NonNull BroadcastProcessQueue queue) {
+ final int cookie = traceBegin("deliveryTimeout");
synchronized (mService) {
deliveryTimeoutLocked(queue);
}
+ traceEnd(cookie);
}
private void deliveryTimeoutLocked(@NonNull BroadcastProcessQueue queue) {
diff --git a/services/core/java/com/android/server/am/ContentProviderHelper.java b/services/core/java/com/android/server/am/ContentProviderHelper.java
index 095d907..30f21a6 100644
--- a/services/core/java/com/android/server/am/ContentProviderHelper.java
+++ b/services/core/java/com/android/server/am/ContentProviderHelper.java
@@ -1249,9 +1249,9 @@
ProviderInfo cpi = providers.get(i);
boolean singleton = mService.isSingleton(cpi.processName, cpi.applicationInfo,
cpi.name, cpi.flags);
- if (singleton && app.userId != UserHandle.USER_SYSTEM) {
- // This is a singleton provider, but a user besides the
- // default user is asking to initialize a process it runs
+ if (isSingletonOrSystemUserOnly(cpi) && app.userId != UserHandle.USER_SYSTEM) {
+ // This is a singleton or a SYSTEM user only provider, but a user besides the
+ // SYSTEM user is asking to initialize a process it runs
// in... well, no, it doesn't actually run in this process,
// it runs in the process of the default user. Get rid of it.
providers.remove(i);
@@ -1398,8 +1398,7 @@
final boolean processMatch =
Objects.equals(pi.processName, app.processName)
|| pi.multiprocess;
- final boolean userMatch = !mService.isSingleton(
- pi.processName, pi.applicationInfo, pi.name, pi.flags)
+ final boolean userMatch = !isSingletonOrSystemUserOnly(pi)
|| app.userId == UserHandle.USER_SYSTEM;
final boolean isInstantApp = pi.applicationInfo.isInstantApp();
final boolean splitInstalled = pi.splitName == null
@@ -1985,4 +1984,13 @@
return isAuthRedirected;
}
}
+
+ /**
+ * Returns true if Provider is either singleUser or systemUserOnly provider.
+ */
+ private boolean isSingletonOrSystemUserOnly(ProviderInfo pi) {
+ return (android.multiuser.Flags.enableSystemUserOnlyForServicesAndProviders()
+ && mService.isSystemUserOnly(pi.flags))
+ || mService.isSingleton(pi.processName, pi.applicationInfo, pi.name, pi.flags);
+ }
}
diff --git a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
index 9db5d0a..7aafda5 100644
--- a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
+++ b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java
@@ -153,6 +153,7 @@
"machine_learning",
"mainline_modularization",
"mainline_sdk",
+ "make_pixel_haptics",
"media_audio",
"media_drm",
"media_reliability",
@@ -162,6 +163,7 @@
"pdf_viewer",
"pixel_audio_android",
"pixel_bluetooth",
+ "pixel_connectivity_gps",
"pixel_system_sw_video",
"pixel_watch",
"platform_security",
diff --git a/services/core/java/com/android/server/audio/AudioDeviceBroker.java b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
index 99b45ec..cd295b5 100644
--- a/services/core/java/com/android/server/audio/AudioDeviceBroker.java
+++ b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
@@ -1047,11 +1047,9 @@
private void initAudioHalBluetoothState() {
synchronized (mBluetoothAudioStateLock) {
mBluetoothScoOnApplied = false;
- AudioSystem.setParameters("BT_SCO=off");
mBluetoothA2dpSuspendedApplied = false;
- AudioSystem.setParameters("A2dpSuspended=false");
mBluetoothLeSuspendedApplied = false;
- AudioSystem.setParameters("LeAudioSuspended=false");
+ reapplyAudioHalBluetoothState();
}
}
@@ -1114,6 +1112,34 @@
}
}
+ @GuardedBy("mBluetoothAudioStateLock")
+ private void reapplyAudioHalBluetoothState() {
+ if (AudioService.DEBUG_COMM_RTE) {
+ Log.v(TAG, "reapplyAudioHalBluetoothState() mBluetoothScoOnApplied: "
+ + mBluetoothScoOnApplied + ", mBluetoothA2dpSuspendedApplied: "
+ + mBluetoothA2dpSuspendedApplied + ", mBluetoothLeSuspendedApplied: "
+ + mBluetoothLeSuspendedApplied);
+ }
+ // Note: the order of parameters is important.
+ if (mBluetoothScoOnApplied) {
+ AudioSystem.setParameters("A2dpSuspended=true");
+ AudioSystem.setParameters("LeAudioSuspended=true");
+ AudioSystem.setParameters("BT_SCO=on");
+ } else {
+ AudioSystem.setParameters("BT_SCO=off");
+ if (mBluetoothA2dpSuspendedApplied) {
+ AudioSystem.setParameters("A2dpSuspended=true");
+ } else {
+ AudioSystem.setParameters("A2dpSuspended=false");
+ }
+ if (mBluetoothLeSuspendedApplied) {
+ AudioSystem.setParameters("LeAudioSuspended=true");
+ } else {
+ AudioSystem.setParameters("LeAudioSuspended=false");
+ }
+ }
+ }
+
/*package*/ void setBluetoothScoOn(boolean on, String eventSource) {
if (AudioService.DEBUG_COMM_RTE) {
Log.v(TAG, "setBluetoothScoOn: " + on + " " + eventSource);
@@ -1775,6 +1801,9 @@
initRoutingStrategyIds();
updateActiveCommunicationDevice();
mDeviceInventory.onRestoreDevices();
+ synchronized (mBluetoothAudioStateLock) {
+ reapplyAudioHalBluetoothState();
+ }
mBtHelper.onAudioServerDiedRestoreA2dp();
updateCommunicationRoute("MSG_RESTORE_DEVICES");
}
diff --git a/services/core/java/com/android/server/audio/AudioDeviceInventory.java b/services/core/java/com/android/server/audio/AudioDeviceInventory.java
index 57b19cd..690c37a 100644
--- a/services/core/java/com/android/server/audio/AudioDeviceInventory.java
+++ b/services/core/java/com/android/server/audio/AudioDeviceInventory.java
@@ -914,28 +914,27 @@
di.mDeviceCodecFormat = codec;
mConnectedDevices.replace(key, di);
codecChange = true;
- }
- final int res = mAudioSystem.handleDeviceConfigChange(
- btInfo.mAudioSystemDevice, address, BtHelper.getName(btDevice), codec);
+ final int res = mAudioSystem.handleDeviceConfigChange(
+ btInfo.mAudioSystemDevice, address,
+ BtHelper.getName(btDevice), codec);
+ if (res != AudioSystem.AUDIO_STATUS_OK) {
+ AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent(
+ "APM handleDeviceConfigChange failed for A2DP device addr="
+ + address + " codec="
+ + AudioSystem.audioFormatToString(codec))
+ .printLog(TAG));
- if (res != AudioSystem.AUDIO_STATUS_OK) {
- AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent(
- "APM handleDeviceConfigChange failed for A2DP device addr="
- + address + " codec="
- + AudioSystem.audioFormatToString(codec))
- .printLog(TAG));
-
- // force A2DP device disconnection in case of error so that AudioService
- // state is consistent with audio policy manager state
- setBluetoothActiveDevice(new AudioDeviceBroker.BtDeviceInfo(btInfo,
- BluetoothProfile.STATE_DISCONNECTED));
- } else {
- AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent(
- "APM handleDeviceConfigChange success for A2DP device addr="
- + address
- + " codec=" + AudioSystem.audioFormatToString(codec))
- .printLog(TAG));
-
+ // force A2DP device disconnection in case of error so that AudioService
+ // state is consistent with audio policy manager state
+ setBluetoothActiveDevice(new AudioDeviceBroker.BtDeviceInfo(btInfo,
+ BluetoothProfile.STATE_DISCONNECTED));
+ } else {
+ AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent(
+ "APM handleDeviceConfigChange success for A2DP device addr="
+ + address
+ + " codec=" + AudioSystem.audioFormatToString(codec))
+ .printLog(TAG));
+ }
}
}
if (!codecChange) {
diff --git a/services/core/java/com/android/server/audio/SoundDoseHelper.java b/services/core/java/com/android/server/audio/SoundDoseHelper.java
index a30cdc4..9610034ca 100644
--- a/services/core/java/com/android/server/audio/SoundDoseHelper.java
+++ b/services/core/java/com/android/server/audio/SoundDoseHelper.java
@@ -1203,7 +1203,7 @@
@GuardedBy("mCsdStateLock")
private void sanitizeDoseRecords_l() {
if (mDoseRecords.size() > MAX_NUMBER_OF_CACHED_RECORDS) {
- int nrToRemove = MAX_NUMBER_OF_CACHED_RECORDS - mDoseRecords.size();
+ int nrToRemove = mDoseRecords.size() - MAX_NUMBER_OF_CACHED_RECORDS;
Log.w(TAG,
"Removing " + nrToRemove + " records from the total of " + mDoseRecords.size());
// Remove older elements to fit into persisted settings max length
diff --git a/services/core/java/com/android/server/backup/SystemBackupAgent.java b/services/core/java/com/android/server/backup/SystemBackupAgent.java
index de4979a..5b9469b 100644
--- a/services/core/java/com/android/server/backup/SystemBackupAgent.java
+++ b/services/core/java/com/android/server/backup/SystemBackupAgent.java
@@ -20,7 +20,8 @@
import android.app.backup.BackupAgentHelper;
import android.app.backup.BackupAnnotations.BackupDestination;
import android.app.backup.BackupDataInput;
-import android.app.backup.BackupHelper;
+import android.app.backup.BackupHelperWithLogger;
+import android.app.backup.BackupRestoreEventLogger;
import android.app.backup.FullBackup;
import android.app.backup.FullBackupDataOutput;
import android.app.backup.WallpaperBackupHelper;
@@ -33,9 +34,10 @@
import android.os.UserHandle;
import android.os.UserManager;
import android.util.Slog;
-
import com.google.android.collect.Sets;
+import com.android.server.backup.Flags;
+
import java.io.File;
import java.io.IOException;
import java.util.Set;
@@ -107,10 +109,12 @@
private int mUserId = UserHandle.USER_SYSTEM;
private boolean mIsProfileUser = false;
+ private BackupRestoreEventLogger mLogger;
@Override
public void onCreate(UserHandle user, @BackupDestination int backupDestination) {
super.onCreate(user, backupDestination);
+ mLogger = this.getBackupRestoreEventLogger();
mUserId = user.getIdentifier();
if (mUserId != UserHandle.USER_SYSTEM) {
@@ -209,9 +213,12 @@
}
}
- private void addHelperIfEligibleForUser(String keyPrefix, BackupHelper helper) {
+ private void addHelperIfEligibleForUser(String keyPrefix, BackupHelperWithLogger helper) {
if (isHelperEligibleForUser(keyPrefix)) {
addHelper(keyPrefix, helper);
+ if (Flags.enableMetricsSystemBackupAgents()) {
+ helper.setLogger(mLogger);
+ }
}
}
diff --git a/services/core/java/com/android/server/biometrics/AuthService.java b/services/core/java/com/android/server/biometrics/AuthService.java
index 8fd2ee2..21e6bac 100644
--- a/services/core/java/com/android/server/biometrics/AuthService.java
+++ b/services/core/java/com/android/server/biometrics/AuthService.java
@@ -439,6 +439,10 @@
if (fingerprintService != null) {
fingerprintService.registerAuthenticationStateListener(listener);
}
+ final IFaceService faceService = mInjector.getFaceService();
+ if (faceService != null) {
+ faceService.registerAuthenticationStateListener(listener);
+ }
}
@Override
@@ -449,6 +453,10 @@
if (fingerprintService != null) {
fingerprintService.unregisterAuthenticationStateListener(listener);
}
+ final IFaceService faceService = mInjector.getFaceService();
+ if (faceService != null) {
+ faceService.unregisterAuthenticationStateListener(listener);
+ }
}
@Override
diff --git a/services/core/java/com/android/server/biometrics/sensors/AuthenticationStateListeners.java b/services/core/java/com/android/server/biometrics/sensors/AuthenticationStateListeners.java
index 5863535..1ae4d64 100644
--- a/services/core/java/com/android/server/biometrics/sensors/AuthenticationStateListeners.java
+++ b/services/core/java/com/android/server/biometrics/sensors/AuthenticationStateListeners.java
@@ -91,6 +91,40 @@
}
}
+ /**
+ * Defines behavior in response to a successful authentication
+ * @param requestReason Reason from [BiometricRequestConstants.RequestReason] for the requested
+ * authentication
+ * @param userId The user Id for the requested authentication
+ */
+ public void onAuthenticationSucceeded(int requestReason, int userId) {
+ for (AuthenticationStateListener listener: mAuthenticationStateListeners) {
+ try {
+ listener.onAuthenticationSucceeded(requestReason, userId);
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Remote exception in notifying listener that authentication "
+ + "succeeded", e);
+ }
+ }
+ }
+
+ /**
+ * Defines behavior in response to a failed authentication
+ * @param requestReason Reason from [BiometricRequestConstants.RequestReason] for the requested
+ * authentication
+ * @param userId The user Id for the requested authentication
+ */
+ public void onAuthenticationFailed(int requestReason, int userId) {
+ for (AuthenticationStateListener listener: mAuthenticationStateListeners) {
+ try {
+ listener.onAuthenticationFailed(requestReason, userId);
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Remote exception in notifying listener that authentication "
+ + "failed", e);
+ }
+ }
+ }
+
@Override
public void binderDied() {
// Do nothing, handled below
diff --git a/services/core/java/com/android/server/biometrics/sensors/face/FaceService.java b/services/core/java/com/android/server/biometrics/sensors/face/FaceService.java
index 73f3999..321e951 100644
--- a/services/core/java/com/android/server/biometrics/sensors/face/FaceService.java
+++ b/services/core/java/com/android/server/biometrics/sensors/face/FaceService.java
@@ -24,6 +24,7 @@
import android.annotation.Nullable;
import android.app.ActivityManager;
import android.content.Context;
+import android.hardware.biometrics.AuthenticationStateListener;
import android.hardware.biometrics.BiometricsProtoEnums;
import android.hardware.biometrics.IBiometricSensorReceiver;
import android.hardware.biometrics.IBiometricService;
@@ -63,6 +64,7 @@
import com.android.server.biometrics.Flags;
import com.android.server.biometrics.Utils;
import com.android.server.biometrics.log.BiometricContext;
+import com.android.server.biometrics.sensors.AuthenticationStateListeners;
import com.android.server.biometrics.sensors.BiometricStateCallback;
import com.android.server.biometrics.sensors.ClientMonitorCallbackConverter;
import com.android.server.biometrics.sensors.LockoutResetDispatcher;
@@ -99,6 +101,8 @@
private final BiometricStateCallback<ServiceProvider, FaceSensorPropertiesInternal>
mBiometricStateCallback;
@NonNull
+ private final AuthenticationStateListeners mAuthenticationStateListeners;
+ @NonNull
private final FaceProviderFunction mFaceProviderFunction;
@NonNull private final Function<String, FaceProvider> mFaceProvider;
@NonNull
@@ -695,7 +699,8 @@
for (FaceSensorPropertiesInternal hidlSensor : hidlSensors) {
providers.add(
Face10.newInstance(getContext(), mBiometricStateCallback,
- hidlSensor, mLockoutResetDispatcher));
+ mAuthenticationStateListeners, hidlSensor,
+ mLockoutResetDispatcher));
}
return providers;
@@ -830,6 +835,24 @@
public void registerBiometricStateListener(@NonNull IBiometricStateListener listener) {
mBiometricStateCallback.registerBiometricStateListener(listener);
}
+
+ @android.annotation.EnforcePermission(android.Manifest.permission.USE_BIOMETRIC_INTERNAL)
+ @Override
+ public void registerAuthenticationStateListener(
+ @NonNull AuthenticationStateListener listener) {
+ super.registerAuthenticationStateListener_enforcePermission();
+
+ mAuthenticationStateListeners.registerAuthenticationStateListener(listener);
+ }
+
+ @android.annotation.EnforcePermission(android.Manifest.permission.USE_BIOMETRIC_INTERNAL)
+ @Override
+ public void unregisterAuthenticationStateListener(
+ @NonNull AuthenticationStateListener listener) {
+ super.unregisterAuthenticationStateListener_enforcePermission();
+
+ mAuthenticationStateListeners.unregisterAuthenticationStateListener(listener);
+ }
}
public FaceService(Context context) {
@@ -848,6 +871,7 @@
mLockoutResetDispatcher = new LockoutResetDispatcher(context);
mLockPatternUtils = new LockPatternUtils(context);
mBiometricStateCallback = new BiometricStateCallback<>(UserManager.get(context));
+ mAuthenticationStateListeners = new AuthenticationStateListeners();
mRegistry = new FaceServiceRegistry(mServiceWrapper, biometricServiceSupplier);
mRegistry.addAllRegisteredCallback(new IFaceAuthenticatorsRegisteredCallback.Stub() {
@Override
@@ -868,8 +892,8 @@
try {
final SensorProps[] props = face.getSensorProps();
return new FaceProvider(getContext(),
- mBiometricStateCallback, props, name, mLockoutResetDispatcher,
- BiometricContext.getInstance(getContext()),
+ mBiometricStateCallback, mAuthenticationStateListeners, props, name,
+ mLockoutResetDispatcher, BiometricContext.getInstance(getContext()),
false /* resetLockoutRequiresChallenge */);
} catch (RemoteException e) {
Slog.e(TAG, "Remote exception in getSensorProps: " + fqName);
@@ -881,7 +905,7 @@
if (Flags.deHidl()) {
mFaceProviderFunction = faceProviderFunction != null ? faceProviderFunction :
((filteredSensorProps, resetLockoutRequiresChallenge) -> new FaceProvider(
- getContext(), mBiometricStateCallback,
+ getContext(), mBiometricStateCallback, mAuthenticationStateListeners,
filteredSensorProps.second,
filteredSensorProps.first, mLockoutResetDispatcher,
BiometricContext.getInstance(getContext()),
diff --git a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceAuthenticationClient.java b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceAuthenticationClient.java
index 22e399c..f35de93 100644
--- a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceAuthenticationClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceAuthenticationClient.java
@@ -16,6 +16,8 @@
package com.android.server.biometrics.sensors.face.aidl;
+import static android.adaptiveauth.Flags.reportBiometricAuthAttempts;
+
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.NotificationManager;
@@ -44,6 +46,7 @@
import com.android.server.biometrics.log.OperationContextExt;
import com.android.server.biometrics.sensors.AuthSessionCoordinator;
import com.android.server.biometrics.sensors.AuthenticationClient;
+import com.android.server.biometrics.sensors.AuthenticationStateListeners;
import com.android.server.biometrics.sensors.ClientMonitorCallback;
import com.android.server.biometrics.sensors.ClientMonitorCallbackConverter;
import com.android.server.biometrics.sensors.ClientMonitorCompositeCallback;
@@ -77,6 +80,8 @@
private ICancellationSignal mCancellationSignal;
@Nullable
private final SensorPrivacyManager mSensorPrivacyManager;
+ @NonNull
+ private final AuthenticationStateListeners mAuthenticationStateListeners;
@FaceManager.FaceAcquired
private int mLastAcquire = FaceManager.FACE_ACQUIRED_UNKNOWN;
@@ -89,11 +94,13 @@
@NonNull BiometricLogger logger, @NonNull BiometricContext biometricContext,
boolean isStrongBiometric, @NonNull UsageStats usageStats,
@NonNull LockoutTracker lockoutCache, boolean allowBackgroundAuthentication,
- @Authenticators.Types int sensorStrength) {
+ @Authenticators.Types int sensorStrength,
+ @NonNull AuthenticationStateListeners authenticationStateListeners) {
this(context, lazyDaemon, token, requestId, listener, operationId,
restricted, options, cookie, requireConfirmation, logger, biometricContext,
isStrongBiometric, usageStats, lockoutCache, allowBackgroundAuthentication,
- context.getSystemService(SensorPrivacyManager.class), sensorStrength);
+ context.getSystemService(SensorPrivacyManager.class), sensorStrength,
+ authenticationStateListeners);
}
@VisibleForTesting
@@ -107,7 +114,8 @@
boolean isStrongBiometric, @NonNull UsageStats usageStats,
@NonNull LockoutTracker lockoutTracker, boolean allowBackgroundAuthentication,
SensorPrivacyManager sensorPrivacyManager,
- @Authenticators.Types int biometricStrength) {
+ @Authenticators.Types int biometricStrength,
+ @NonNull AuthenticationStateListeners authenticationStateListeners) {
super(context, lazyDaemon, token, listener, operationId, restricted,
options, cookie, requireConfirmation, logger, biometricContext,
isStrongBiometric, null /* taskStackListener */, lockoutTracker,
@@ -118,6 +126,7 @@
mNotificationManager = context.getSystemService(NotificationManager.class);
mSensorPrivacyManager = sensorPrivacyManager;
mAuthSessionCoordinator = biometricContext.getAuthSessionCoordinator();
+ mAuthenticationStateListeners = authenticationStateListeners;
final Resources resources = getContext().getResources();
mBiometricPromptIgnoreList = resources.getIntArray(
@@ -262,6 +271,16 @@
0 /* error */,
0 /* vendorError */,
getTargetUserId()));
+
+ if (reportBiometricAuthAttempts()) {
+ if (authenticated) {
+ mAuthenticationStateListeners.onAuthenticationSucceeded(getRequestReason(),
+ getTargetUserId());
+ } else {
+ mAuthenticationStateListeners.onAuthenticationFailed(getRequestReason(),
+ getTargetUserId());
+ }
+ }
}
@Override
diff --git a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceProvider.java b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceProvider.java
index e4ecf1a..d01c268 100644
--- a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceProvider.java
+++ b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceProvider.java
@@ -59,6 +59,7 @@
import com.android.server.biometrics.log.BiometricLogger;
import com.android.server.biometrics.sensors.AuthSessionCoordinator;
import com.android.server.biometrics.sensors.AuthenticationClient;
+import com.android.server.biometrics.sensors.AuthenticationStateListeners;
import com.android.server.biometrics.sensors.BaseClientMonitor;
import com.android.server.biometrics.sensors.BiometricScheduler;
import com.android.server.biometrics.sensors.BiometricStateCallback;
@@ -103,6 +104,8 @@
@NonNull
private final BiometricStateCallback mBiometricStateCallback;
@NonNull
+ private final AuthenticationStateListeners mAuthenticationStateListeners;
+ @NonNull
private final String mHalInstanceName;
@NonNull
private final Handler mHandler;
@@ -156,18 +159,20 @@
public FaceProvider(@NonNull Context context,
@NonNull BiometricStateCallback biometricStateCallback,
+ @NonNull AuthenticationStateListeners authenticationStateListeners,
@NonNull SensorProps[] props,
@NonNull String halInstanceName,
@NonNull LockoutResetDispatcher lockoutResetDispatcher,
@NonNull BiometricContext biometricContext,
boolean resetLockoutRequiresChallenge) {
- this(context, biometricStateCallback, props, halInstanceName, lockoutResetDispatcher,
- biometricContext, null /* daemon */, getHandler(), resetLockoutRequiresChallenge,
- false /* testHalEnabled */);
+ this(context, biometricStateCallback, authenticationStateListeners, props, halInstanceName,
+ lockoutResetDispatcher, biometricContext, null /* daemon */, getHandler(),
+ resetLockoutRequiresChallenge, false /* testHalEnabled */);
}
@VisibleForTesting FaceProvider(@NonNull Context context,
@NonNull BiometricStateCallback biometricStateCallback,
+ @NonNull AuthenticationStateListeners authenticationStateListeners,
@NonNull SensorProps[] props,
@NonNull String halInstanceName,
@NonNull LockoutResetDispatcher lockoutResetDispatcher,
@@ -178,6 +183,7 @@
boolean testHalEnabled) {
mContext = context;
mBiometricStateCallback = biometricStateCallback;
+ mAuthenticationStateListeners = authenticationStateListeners;
mHalInstanceName = halInstanceName;
mFaceSensors = new SensorList<>(ActivityManager.getService());
if (Flags.deHidl()) {
@@ -610,7 +616,8 @@
mAuthenticationStatsCollector),
mBiometricContext, isStrongBiometric,
mUsageStats, lockoutTracker,
- allowBackgroundAuthentication, Utils.getCurrentStrength(sensorId));
+ allowBackgroundAuthentication, Utils.getCurrentStrength(sensorId),
+ mAuthenticationStateListeners);
scheduleForSensor(sensorId, client, new ClientMonitorCallback() {
@Override
public void onClientStarted(
diff --git a/services/core/java/com/android/server/biometrics/sensors/face/hidl/Face10.java b/services/core/java/com/android/server/biometrics/sensors/face/hidl/Face10.java
index 5337666..48a676c 100644
--- a/services/core/java/com/android/server/biometrics/sensors/face/hidl/Face10.java
+++ b/services/core/java/com/android/server/biometrics/sensors/face/hidl/Face10.java
@@ -64,6 +64,7 @@
import com.android.server.biometrics.sensors.AcquisitionClient;
import com.android.server.biometrics.sensors.AuthSessionCoordinator;
import com.android.server.biometrics.sensors.AuthenticationConsumer;
+import com.android.server.biometrics.sensors.AuthenticationStateListeners;
import com.android.server.biometrics.sensors.BaseClientMonitor;
import com.android.server.biometrics.sensors.BiometricScheduler;
import com.android.server.biometrics.sensors.BiometricStateCallback;
@@ -119,6 +120,8 @@
@NonNull private final FaceSensorPropertiesInternal mSensorProperties;
@NonNull private final BiometricStateCallback mBiometricStateCallback;
+ @NonNull
+ private final AuthenticationStateListeners mAuthenticationStateListeners;
@NonNull private final Context mContext;
@NonNull private final BiometricScheduler<IBiometricsFace, AidlSession> mScheduler;
@NonNull private final Handler mHandler;
@@ -350,6 +353,7 @@
@VisibleForTesting
Face10(@NonNull Context context,
@NonNull BiometricStateCallback biometricStateCallback,
+ @NonNull AuthenticationStateListeners authenticationStateListeners,
@NonNull FaceSensorPropertiesInternal sensorProps,
@NonNull LockoutResetDispatcher lockoutResetDispatcher,
@NonNull Handler handler,
@@ -358,6 +362,7 @@
mSensorProperties = sensorProps;
mContext = context;
mBiometricStateCallback = biometricStateCallback;
+ mAuthenticationStateListeners = authenticationStateListeners;
mSensorId = sensorProps.sensorId;
mScheduler = scheduler;
mHandler = handler;
@@ -392,11 +397,12 @@
public static Face10 newInstance(@NonNull Context context,
@NonNull BiometricStateCallback biometricStateCallback,
+ @NonNull AuthenticationStateListeners authenticationStateListeners,
@NonNull FaceSensorPropertiesInternal sensorProps,
@NonNull LockoutResetDispatcher lockoutResetDispatcher) {
final Handler handler = new Handler(Looper.getMainLooper());
- return new Face10(context, biometricStateCallback, sensorProps, lockoutResetDispatcher,
- handler, new BiometricScheduler<>(
+ return new Face10(context, biometricStateCallback, authenticationStateListeners,
+ sensorProps, lockoutResetDispatcher, handler, new BiometricScheduler<>(
BiometricScheduler.SENSOR_TYPE_FACE,
null /* gestureAvailabilityTracker */),
BiometricContext.getInstance(context));
@@ -846,7 +852,8 @@
createLogger(BiometricsProtoEnums.ACTION_AUTHENTICATE, statsClient,
mAuthenticationStatsCollector), mBiometricContext,
isStrongBiometric, mUsageStats, mLockoutTracker,
- allowBackgroundAuthentication, Utils.getCurrentStrength(mSensorId));
+ allowBackgroundAuthentication, Utils.getCurrentStrength(mSensorId),
+ mAuthenticationStateListeners);
mScheduler.scheduleClientMonitor(client);
}
@@ -860,7 +867,8 @@
createLogger(BiometricsProtoEnums.ACTION_AUTHENTICATE, statsClient,
mAuthenticationStatsCollector), mBiometricContext,
isStrongBiometric, mLockoutTracker, mUsageStats,
- allowBackgroundAuthentication, Utils.getCurrentStrength(mSensorId));
+ allowBackgroundAuthentication, Utils.getCurrentStrength(mSensorId),
+ mAuthenticationStateListeners);
mScheduler.scheduleClientMonitor(client);
}
diff --git a/services/core/java/com/android/server/biometrics/sensors/face/hidl/FaceAuthenticationClient.java b/services/core/java/com/android/server/biometrics/sensors/face/hidl/FaceAuthenticationClient.java
index 8ab8892..e44b263 100644
--- a/services/core/java/com/android/server/biometrics/sensors/face/hidl/FaceAuthenticationClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/face/hidl/FaceAuthenticationClient.java
@@ -16,6 +16,8 @@
package com.android.server.biometrics.sensors.face.hidl;
+import static android.adaptiveauth.Flags.reportBiometricAuthAttempts;
+
import android.annotation.NonNull;
import android.content.Context;
import android.content.res.Resources;
@@ -36,6 +38,7 @@
import com.android.server.biometrics.log.BiometricContext;
import com.android.server.biometrics.log.BiometricLogger;
import com.android.server.biometrics.sensors.AuthenticationClient;
+import com.android.server.biometrics.sensors.AuthenticationStateListeners;
import com.android.server.biometrics.sensors.BiometricNotificationUtils;
import com.android.server.biometrics.sensors.ClientMonitorCallback;
import com.android.server.biometrics.sensors.ClientMonitorCallbackConverter;
@@ -65,6 +68,8 @@
private int mLastAcquire;
private SensorPrivacyManager mSensorPrivacyManager;
+ @NonNull
+ private final AuthenticationStateListeners mAuthenticationStateListeners;
FaceAuthenticationClient(@NonNull Context context,
@NonNull Supplier<IBiometricsFace> lazyDaemon,
@@ -75,7 +80,8 @@
@NonNull BiometricLogger logger, @NonNull BiometricContext biometricContext,
boolean isStrongBiometric, @NonNull LockoutTracker lockoutTracker,
@NonNull UsageStats usageStats, boolean allowBackgroundAuthentication,
- @Authenticators.Types int sensorStrength) {
+ @Authenticators.Types int sensorStrength,
+ @NonNull AuthenticationStateListeners authenticationStateListeners) {
super(context, lazyDaemon, token, listener, operationId, restricted,
options, cookie, requireConfirmation, logger, biometricContext,
isStrongBiometric, null /* taskStackListener */,
@@ -84,6 +90,7 @@
setRequestId(requestId);
mUsageStats = usageStats;
mSensorPrivacyManager = context.getSystemService(SensorPrivacyManager.class);
+ mAuthenticationStateListeners = authenticationStateListeners;
final Resources resources = getContext().getResources();
mBiometricPromptIgnoreList = resources.getIntArray(
@@ -186,6 +193,16 @@
0 /* error */,
0 /* vendorError */,
getTargetUserId()));
+
+ if (reportBiometricAuthAttempts()) {
+ if (authenticated) {
+ mAuthenticationStateListeners.onAuthenticationSucceeded(getRequestReason(),
+ getTargetUserId());
+ } else {
+ mAuthenticationStateListeners.onAuthenticationFailed(getRequestReason(),
+ getTargetUserId());
+ }
+ }
}
@Override
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java
index f7e8123..6912961 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java
@@ -16,6 +16,8 @@
package com.android.server.biometrics.sensors.fingerprint.aidl;
+import static android.adaptiveauth.Flags.reportBiometricAuthAttempts;
+
import static com.android.systemui.shared.Flags.sidefpsControllerRefactor;
import android.annotation.NonNull;
@@ -232,8 +234,16 @@
if (sidefpsControllerRefactor()) {
mAuthenticationStateListeners.onAuthenticationStopped();
}
+ if (reportBiometricAuthAttempts()) {
+ mAuthenticationStateListeners.onAuthenticationSucceeded(getRequestReason(),
+ getTargetUserId());
+ }
} else {
mState = STATE_STARTED_PAUSED_ATTEMPTED;
+ if (reportBiometricAuthAttempts()) {
+ mAuthenticationStateListeners.onAuthenticationFailed(getRequestReason(),
+ getTargetUserId());
+ }
}
}
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintAuthenticationClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintAuthenticationClient.java
index 4c1d4d6..7a329e9 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintAuthenticationClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintAuthenticationClient.java
@@ -16,6 +16,8 @@
package com.android.server.biometrics.sensors.fingerprint.hidl;
+import static android.adaptiveauth.Flags.reportBiometricAuthAttempts;
+
import static com.android.systemui.shared.Flags.sidefpsControllerRefactor;
import android.annotation.NonNull;
@@ -142,6 +144,10 @@
if (sidefpsControllerRefactor()) {
mAuthenticationStateListeners.onAuthenticationStopped();
}
+ if (reportBiometricAuthAttempts()) {
+ mAuthenticationStateListeners.onAuthenticationSucceeded(getRequestReason(),
+ getTargetUserId());
+ }
} else {
mState = STATE_STARTED_PAUSED_ATTEMPTED;
final @LockoutTracker.LockoutMode int lockoutMode =
@@ -161,6 +167,10 @@
onErrorInternal(errorCode, 0 /* vendorCode */, false /* finish */);
cancel();
}
+ if (reportBiometricAuthAttempts()) {
+ mAuthenticationStateListeners.onAuthenticationFailed(getRequestReason(),
+ getTargetUserId());
+ }
}
}
diff --git a/services/core/java/com/android/server/compat/CompatConfig.java b/services/core/java/com/android/server/compat/CompatConfig.java
index 9dd7daf..9102cfd 100644
--- a/services/core/java/com/android/server/compat/CompatConfig.java
+++ b/services/core/java/com/android/server/compat/CompatConfig.java
@@ -286,6 +286,9 @@
return new CompatChange(changeId);
});
c.addPackageOverride(packageName, overrides, allowedState, versionCode);
+ Slog.d(TAG, (overrides.isEnabled() ? "Enabled" : "Disabled")
+ + " change " + changeId + (c.getName() != null ? " [" + c.getName() + "]" : "")
+ + " for " + packageName);
invalidateCache();
return alreadyKnown.get();
}
@@ -372,7 +375,14 @@
long changeId = change.getId();
OverrideAllowedState allowedState =
mOverrideValidator.getOverrideAllowedState(changeId, packageName);
- return change.removePackageOverride(packageName, allowedState, versionCode);
+ boolean overrideExists = change.removePackageOverride(packageName, allowedState,
+ versionCode);
+ if (overrideExists) {
+ Slog.d(TAG, "Reset change " + changeId
+ + (change.getName() != null ? " [" + change.getName() + "]" : "")
+ + " for " + packageName + " to default value.");
+ }
+ return overrideExists;
}
/**
diff --git a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java
index d34661d..34e75c0 100644
--- a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java
+++ b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java
@@ -47,6 +47,7 @@
import android.media.AudioProfile;
import android.media.tv.TvInputInfo;
import android.media.tv.TvInputManager.TvInputCallback;
+import android.os.Handler;
import android.util.Slog;
import android.util.SparseBooleanArray;
@@ -97,9 +98,15 @@
private boolean mSystemAudioMute = false;
// If true, do not do routing control/send active source for internal source.
- // Set to true when the device was woken up by <Text/Image View On>.
+ // Set to true for a short duration when the device is woken up by <Text/Image View On>.
private boolean mSkipRoutingControl;
+ // Handler for posting a runnable to set `mSkipRoutingControl` to false after a delay
+ private final Handler mSkipRoutingControlHandler;
+
+ // Runnable that sets `mSkipRoutingControl` to false
+ private final Runnable mResetSkipRoutingControlRunnable = () -> mSkipRoutingControl = false;
+
// Message buffer used to buffer selected messages to process later. <Active Source>
// from a source device, for instance, needs to be buffered if the device is not
// discovered yet. The buffered commands are taken out and when they are ready to
@@ -162,6 +169,7 @@
HdmiControlManager.CEC_SETTING_NAME_SYSTEM_AUDIO_CONTROL)
== HdmiControlManager.SYSTEM_AUDIO_CONTROL_ENABLED;
mStandbyHandler = new HdmiCecStandbyModeHandler(service, this);
+ mSkipRoutingControlHandler = new Handler(service.getServiceLooper());
}
@Override
@@ -184,7 +192,14 @@
mService.getHdmiCecNetwork().addCecSwitch(
mService.getHdmiCecNetwork().getPhysicalAddress()); // TV is a CEC switch too.
mTvInputs.clear();
+
mSkipRoutingControl = (reason == HdmiControlService.INITIATED_BY_WAKE_UP_MESSAGE);
+ mSkipRoutingControlHandler.removeCallbacks(mResetSkipRoutingControlRunnable);
+ if (mSkipRoutingControl) {
+ mSkipRoutingControlHandler.postDelayed(mResetSkipRoutingControlRunnable,
+ HdmiConfig.TIMEOUT_MS);
+ }
+
launchRoutingControl(reason != HdmiControlService.INITIATED_BY_ENABLE_CEC &&
reason != HdmiControlService.INITIATED_BY_BOOT_UP);
resetSelectRequestBuffer();
diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java
index ef66803..687def0 100644
--- a/services/core/java/com/android/server/input/InputManagerService.java
+++ b/services/core/java/com/android/server/input/InputManagerService.java
@@ -1328,8 +1328,7 @@
mPointerIconDisplayContext = null;
}
- updateAdditionalDisplayInputProperties(displayId,
- AdditionalDisplayInputProperties::reset);
+ updateAdditionalDisplayInputProperties(displayId, AdditionalDisplayInputProperties::reset);
// TODO(b/320763728): Rely on WindowInfosListener to determine when a display has been
// removed in InputDispatcher instead of this callback.
@@ -1820,8 +1819,6 @@
mPointerIconType = icon.getType();
mPointerIcon = mPointerIconType == PointerIcon.TYPE_CUSTOM ? icon : null;
- if (!mCurrentDisplayProperties.pointerIconVisible) return false;
-
return mNative.setPointerIcon(icon, displayId, deviceId, pointerId, inputToken);
}
}
@@ -3486,6 +3483,10 @@
private void applyAdditionalDisplayInputPropertiesLocked(
AdditionalDisplayInputProperties properties) {
// Handle changes to each of the individual properties.
+ // TODO(b/293587049): This approach for updating pointer display properties is only for when
+ // PointerChoreographer is disabled. Remove this logic when PointerChoreographer is
+ // permanently enabled.
+
if (properties.pointerIconVisible != mCurrentDisplayProperties.pointerIconVisible) {
mCurrentDisplayProperties.pointerIconVisible = properties.pointerIconVisible;
if (properties.pointerIconVisible) {
@@ -3504,7 +3505,6 @@
!= mCurrentDisplayProperties.mousePointerAccelerationEnabled) {
mCurrentDisplayProperties.mousePointerAccelerationEnabled =
properties.mousePointerAccelerationEnabled;
- mNative.setMousePointerAccelerationEnabled(properties.mousePointerAccelerationEnabled);
}
}
@@ -3517,7 +3517,16 @@
properties = new AdditionalDisplayInputProperties();
mAdditionalDisplayInputProperties.put(displayId, properties);
}
+ final boolean oldPointerIconVisible = properties.pointerIconVisible;
+ final boolean oldMouseAccelerationEnabled = properties.mousePointerAccelerationEnabled;
updater.accept(properties);
+ if (oldPointerIconVisible != properties.pointerIconVisible) {
+ mNative.setPointerIconVisibility(displayId, properties.pointerIconVisible);
+ }
+ if (oldMouseAccelerationEnabled != properties.mousePointerAccelerationEnabled) {
+ mNative.setMousePointerAccelerationEnabled(displayId,
+ properties.mousePointerAccelerationEnabled);
+ }
if (properties.allDefaults()) {
mAdditionalDisplayInputProperties.remove(displayId);
}
@@ -3613,6 +3622,13 @@
}
/**
+ * Sets Accessibility slow keys threshold in milliseconds.
+ */
+ public void setAccessibilitySlowKeysThreshold(int thresholdTimeMs) {
+ mNative.setAccessibilitySlowKeysThreshold(thresholdTimeMs);
+ }
+
+ /**
* Sets whether Accessibility sticky keys is enabled.
*/
public void setAccessibilityStickyKeysEnabled(boolean enabled) {
diff --git a/services/core/java/com/android/server/input/InputSettingsObserver.java b/services/core/java/com/android/server/input/InputSettingsObserver.java
index bc55b24..165dfe4 100644
--- a/services/core/java/com/android/server/input/InputSettingsObserver.java
+++ b/services/core/java/com/android/server/input/InputSettingsObserver.java
@@ -16,6 +16,8 @@
package com.android.server.input;
+import static com.android.input.flags.Flags.rateLimitUserActivityPokeInDispatcher;
+
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
@@ -87,6 +89,8 @@
(reason) -> updateShowRotaryInput()),
Map.entry(Settings.Secure.getUriFor(Settings.Secure.ACCESSIBILITY_BOUNCE_KEYS),
(reason) -> updateAccessibilityBounceKeys()),
+ Map.entry(Settings.Secure.getUriFor(Settings.Secure.ACCESSIBILITY_SLOW_KEYS),
+ (reason) -> updateAccessibilitySlowKeys()),
Map.entry(Settings.Secure.getUriFor(Settings.Secure.ACCESSIBILITY_STICKY_KEYS),
(reason) -> updateAccessibilityStickyKeys()));
}
@@ -113,6 +117,8 @@
for (Consumer<String> observer : mObservers.values()) {
observer.accept("just booted");
}
+
+ configureUserActivityPokeInterval();
}
@Override
@@ -224,8 +230,22 @@
InputSettings.getAccessibilityBounceKeysThreshold(mContext));
}
+ private void updateAccessibilitySlowKeys() {
+ mService.setAccessibilitySlowKeysThreshold(
+ InputSettings.getAccessibilitySlowKeysThreshold(mContext));
+ }
+
private void updateAccessibilityStickyKeys() {
mService.setAccessibilityStickyKeysEnabled(
InputSettings.isAccessibilityStickyKeysEnabled(mContext));
}
+
+ private void configureUserActivityPokeInterval() {
+ if (rateLimitUserActivityPokeInDispatcher()) {
+ final int intervalMillis = mContext.getResources().getInteger(
+ com.android.internal.R.integer.config_minMillisBetweenInputUserActivityEvents);
+ Log.i(TAG, "Setting user activity interval (ms) of " + intervalMillis);
+ mNative.setMinTimeBetweenUserActivityPokes(intervalMillis);
+ }
+ }
}
diff --git a/services/core/java/com/android/server/input/NativeInputManagerService.java b/services/core/java/com/android/server/input/NativeInputManagerService.java
index 309d493..bc82078 100644
--- a/services/core/java/com/android/server/input/NativeInputManagerService.java
+++ b/services/core/java/com/android/server/input/NativeInputManagerService.java
@@ -108,6 +108,8 @@
void setFocusedDisplay(int displayId);
+ void setMinTimeBetweenUserActivityPokes(long millis);
+
boolean transferTouchFocus(IBinder fromChannelToken, IBinder toChannelToken,
boolean isDragDrop);
@@ -119,7 +121,7 @@
void setPointerSpeed(int speed);
- void setMousePointerAccelerationEnabled(boolean enabled);
+ void setMousePointerAccelerationEnabled(int displayId, boolean enabled);
void setTouchpadPointerSpeed(int speed);
@@ -190,6 +192,8 @@
boolean setPointerIcon(@NonNull PointerIcon icon, int displayId, int deviceId, int pointerId,
@NonNull IBinder inputToken);
+ void setPointerIconVisibility(int displayId, boolean visible);
+
void requestPointerCapture(IBinder windowToken, boolean enabled);
boolean canDispatchToDisplay(int deviceId, int displayId);
@@ -254,6 +258,11 @@
void setAccessibilityBounceKeysThreshold(int thresholdTimeMs);
/**
+ * Notify if Accessibility slow keys threshold is changed from InputSettings.
+ */
+ void setAccessibilitySlowKeysThreshold(int thresholdTimeMs);
+
+ /**
* Notify if Accessibility sticky keys is enabled/disabled from InputSettings.
*/
void setAccessibilityStickyKeysEnabled(boolean enabled);
@@ -343,6 +352,9 @@
public native void setFocusedDisplay(int displayId);
@Override
+ public native void setMinTimeBetweenUserActivityPokes(long millis);
+
+ @Override
public native boolean transferTouchFocus(IBinder fromChannelToken, IBinder toChannelToken,
boolean isDragDrop);
@@ -353,7 +365,7 @@
public native void setPointerSpeed(int speed);
@Override
- public native void setMousePointerAccelerationEnabled(boolean enabled);
+ public native void setMousePointerAccelerationEnabled(int displayId, boolean enabled);
@Override
public native void setTouchpadPointerSpeed(int speed);
@@ -453,6 +465,9 @@
int pointerId, IBinder inputToken);
@Override
+ public native void setPointerIconVisibility(int displayId, boolean visible);
+
+ @Override
public native void requestPointerCapture(IBinder windowToken, boolean enabled);
@Override
@@ -517,6 +532,9 @@
public native void setAccessibilityBounceKeysThreshold(int thresholdTimeMs);
@Override
+ public native void setAccessibilitySlowKeysThreshold(int thresholdTimeMs);
+
+ @Override
public native void setAccessibilityStickyKeysEnabled(boolean enabled);
}
}
diff --git a/services/core/java/com/android/server/inputmethod/AdditionalSubtypeUtils.java b/services/core/java/com/android/server/inputmethod/AdditionalSubtypeUtils.java
index c76ca2b..fba71fd 100644
--- a/services/core/java/com/android/server/inputmethod/AdditionalSubtypeUtils.java
+++ b/services/core/java/com/android/server/inputmethod/AdditionalSubtypeUtils.java
@@ -114,7 +114,7 @@
* @param userId The user ID to be associated with.
*/
static void save(ArrayMap<String, List<InputMethodSubtype>> allSubtypes,
- ArrayMap<String, InputMethodInfo> methodMap, @UserIdInt int userId) {
+ InputMethodMap methodMap, @UserIdInt int userId) {
final File inputMethodDir = getInputMethodDir(userId);
if (allSubtypes.isEmpty()) {
@@ -143,7 +143,7 @@
@VisibleForTesting
static void saveToFile(ArrayMap<String, List<InputMethodSubtype>> allSubtypes,
- ArrayMap<String, InputMethodInfo> methodMap, AtomicFile subtypesFile) {
+ InputMethodMap methodMap, AtomicFile subtypesFile) {
// Safety net for the case that this function is called before methodMap is set.
final boolean isSetMethodMap = methodMap != null && methodMap.size() > 0;
FileOutputStream fos = null;
diff --git a/services/core/java/com/android/server/inputmethod/ClientController.java b/services/core/java/com/android/server/inputmethod/ClientController.java
index 21b952b..ece236a 100644
--- a/services/core/java/com/android/server/inputmethod/ClientController.java
+++ b/services/core/java/com/android/server/inputmethod/ClientController.java
@@ -21,8 +21,6 @@
import android.os.IBinder;
import android.os.RemoteException;
import android.util.ArrayMap;
-import android.util.SparseArray;
-import android.view.inputmethod.InputBinding;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
@@ -156,48 +154,4 @@
return InputMethodUtils.checkIfPackageBelongsToUid(
mPackageManagerInternal, cs.mUid, packageName);
}
-
- static final class ClientState {
- final IInputMethodClientInvoker mClient;
- final IRemoteInputConnection mFallbackInputConnection;
- final int mUid;
- final int mPid;
- final int mSelfReportedDisplayId;
- final InputBinding mBinding;
- final IBinder.DeathRecipient mClientDeathRecipient;
-
- @GuardedBy("ImfLock.class")
- boolean mSessionRequested;
-
- @GuardedBy("ImfLock.class")
- boolean mSessionRequestedForAccessibility;
-
- @GuardedBy("ImfLock.class")
- InputMethodManagerService.SessionState mCurSession;
-
- @GuardedBy("ImfLock.class")
- SparseArray<InputMethodManagerService.AccessibilitySessionState> mAccessibilitySessions =
- new SparseArray<>();
-
- @Override
- public String toString() {
- return "ClientState{" + Integer.toHexString(
- System.identityHashCode(this)) + " mUid=" + mUid
- + " mPid=" + mPid + " mSelfReportedDisplayId=" + mSelfReportedDisplayId + "}";
- }
-
- ClientState(IInputMethodClientInvoker client,
- IRemoteInputConnection fallbackInputConnection,
- int uid, int pid, int selfReportedDisplayId,
- IBinder.DeathRecipient clientDeathRecipient) {
- mClient = client;
- mFallbackInputConnection = fallbackInputConnection;
- mUid = uid;
- mPid = pid;
- mSelfReportedDisplayId = selfReportedDisplayId;
- mBinding = new InputBinding(null /*conn*/, mFallbackInputConnection.asBinder(), mUid,
- mPid);
- mClientDeathRecipient = clientDeathRecipient;
- }
- }
}
diff --git a/services/core/java/com/android/server/inputmethod/ClientState.java b/services/core/java/com/android/server/inputmethod/ClientState.java
new file mode 100644
index 0000000..e98a5a7
--- /dev/null
+++ b/services/core/java/com/android/server/inputmethod/ClientState.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.inputmethod;
+
+import android.os.IBinder;
+import android.util.SparseArray;
+import android.view.inputmethod.InputBinding;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.inputmethod.IRemoteInputConnection;
+
+final class ClientState {
+ final IInputMethodClientInvoker mClient;
+ final IRemoteInputConnection mFallbackInputConnection;
+ final int mUid;
+ final int mPid;
+ final int mSelfReportedDisplayId;
+ final InputBinding mBinding;
+ final IBinder.DeathRecipient mClientDeathRecipient;
+
+ @GuardedBy("ImfLock.class")
+ boolean mSessionRequested;
+
+ @GuardedBy("ImfLock.class")
+ boolean mSessionRequestedForAccessibility;
+
+ @GuardedBy("ImfLock.class")
+ InputMethodManagerService.SessionState mCurSession;
+
+ @GuardedBy("ImfLock.class")
+ SparseArray<InputMethodManagerService.AccessibilitySessionState> mAccessibilitySessions =
+ new SparseArray<>();
+
+ @Override
+ public String toString() {
+ return "ClientState{" + Integer.toHexString(
+ System.identityHashCode(this)) + " mUid=" + mUid
+ + " mPid=" + mPid + " mSelfReportedDisplayId=" + mSelfReportedDisplayId + "}";
+ }
+
+ ClientState(IInputMethodClientInvoker client,
+ IRemoteInputConnection fallbackInputConnection,
+ int uid, int pid, int selfReportedDisplayId,
+ IBinder.DeathRecipient clientDeathRecipient) {
+ mClient = client;
+ mFallbackInputConnection = fallbackInputConnection;
+ mUid = uid;
+ mPid = pid;
+ mSelfReportedDisplayId = selfReportedDisplayId;
+ mBinding = new InputBinding(null /*conn*/, mFallbackInputConnection.asBinder(), mUid,
+ mPid);
+ mClientDeathRecipient = clientDeathRecipient;
+ }
+}
diff --git a/services/core/java/com/android/server/inputmethod/HardwareKeyboardShortcutController.java b/services/core/java/com/android/server/inputmethod/HardwareKeyboardShortcutController.java
index 4b85d09..62adb25 100644
--- a/services/core/java/com/android/server/inputmethod/HardwareKeyboardShortcutController.java
+++ b/services/core/java/com/android/server/inputmethod/HardwareKeyboardShortcutController.java
@@ -20,7 +20,6 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.UserIdInt;
-import android.util.ArrayMap;
import android.view.inputmethod.InputMethodInfo;
import android.view.inputmethod.InputMethodSubtype;
@@ -44,24 +43,23 @@
return mUserId;
}
- HardwareKeyboardShortcutController(
- @NonNull ArrayMap<String, InputMethodInfo> methodMap, @UserIdInt int userId) {
+ HardwareKeyboardShortcutController(@NonNull InputMethodMap methodMap, @UserIdInt int userId) {
mUserId = userId;
reset(methodMap);
}
@GuardedBy("ImfLock.class")
- void reset(@NonNull ArrayMap<String, InputMethodInfo> methodMap) {
+ void reset(@NonNull InputMethodMap methodMap) {
mSubtypeHandles.clear();
- final InputMethodSettings settings = new InputMethodSettings(methodMap, mUserId);
- final List<InputMethodInfo> inputMethods = settings.getEnabledInputMethodListLocked();
+ final InputMethodSettings settings = InputMethodSettings.create(methodMap, mUserId);
+ final List<InputMethodInfo> inputMethods = settings.getEnabledInputMethodList();
for (int i = 0; i < inputMethods.size(); ++i) {
final InputMethodInfo imi = inputMethods.get(i);
if (!imi.shouldShowInInputMethodPicker()) {
continue;
}
final List<InputMethodSubtype> subtypes =
- settings.getEnabledInputMethodSubtypeListLocked(imi, true);
+ settings.getEnabledInputMethodSubtypeList(imi, true);
if (subtypes.isEmpty()) {
mSubtypeHandles.add(InputMethodSubtypeHandle.of(imi, null));
} else {
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodInfoUtils.java b/services/core/java/com/android/server/inputmethod/InputMethodInfoUtils.java
index 542165d..6339686 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodInfoUtils.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodInfoUtils.java
@@ -23,7 +23,6 @@
import android.annotation.Nullable;
import android.content.Context;
import android.text.TextUtils;
-import android.util.ArrayMap;
import android.util.Slog;
import android.view.inputmethod.InputMethodInfo;
import android.view.inputmethod.InputMethodSubtype;
@@ -204,7 +203,7 @@
*/
@Nullable
static InputMethodInfo chooseSystemVoiceIme(
- @NonNull ArrayMap<String, InputMethodInfo> methodMap,
+ @NonNull InputMethodMap methodMap,
@Nullable String systemSpeechRecognizerPackageName,
@Nullable String currentDefaultVoiceImeId) {
if (TextUtils.isEmpty(systemSpeechRecognizerPackageName)) {
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
index 8448fc2..d72ca7d 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
@@ -48,7 +48,6 @@
import static android.view.WindowManager.DISPLAY_IME_POLICY_HIDE;
import static android.view.WindowManager.DISPLAY_IME_POLICY_LOCAL;
-import static com.android.server.inputmethod.ClientController.ClientState;
import static com.android.server.inputmethod.ImeVisibilityStateComputer.ImeTargetWindowState;
import static com.android.server.inputmethod.ImeVisibilityStateComputer.ImeVisibilityResult;
import static com.android.server.inputmethod.ImeVisibilityStateComputer.STATE_HIDE_IME;
@@ -314,10 +313,6 @@
@Nullable
private VirtualDeviceManagerInternal mVdmInternal = null;
- // All known input methods.
- final ArrayList<InputMethodInfo> mMethodList = new ArrayList<>();
- private final ArrayMap<String, InputMethodInfo> mMethodMap = new ArrayMap<>();
-
// Mapping from deviceId to the device-specific imeId for that device.
@GuardedBy("ImfLock.class")
private final SparseArray<String> mVirtualDeviceMethodMap = new SparseArray<>();
@@ -330,7 +325,7 @@
private HardwareKeyboardShortcutController mHardwareKeyboardShortcutController;
/**
- * Tracks how many times {@link #mMethodMap} was updated.
+ * Tracks how many times {@link #mSettings} was updated.
*/
@GuardedBy("ImfLock.class")
private int mMethodMapUpdateCount = 0;
@@ -472,7 +467,7 @@
@GuardedBy("ImfLock.class")
@Nullable
InputMethodInfo queryInputMethodForCurrentUserLocked(@NonNull String imeId) {
- return mMethodMap.get(imeId);
+ return mSettings.getMethodMap().get(imeId);
}
/**
@@ -1171,7 +1166,7 @@
// sender userId can be a real user ID or USER_ALL.
final int senderUserId = pendingResult.getSendingUserId();
if (senderUserId != UserHandle.USER_ALL) {
- if (senderUserId != mSettings.getCurrentUserId()) {
+ if (senderUserId != mSettings.getUserId()) {
// A background user is trying to hide the dialog. Ignore.
return;
}
@@ -1249,7 +1244,7 @@
@GuardedBy("ImfLock.class")
private boolean isChangingPackagesOfCurrentUserLocked() {
final int userId = getChangingUserId();
- final boolean retval = userId == mSettings.getCurrentUserId();
+ final boolean retval = userId == mSettings.getUserId();
if (DEBUG) {
if (!retval) {
Slog.d(TAG, "--- ignore this call back from a background user: " + userId);
@@ -1265,10 +1260,11 @@
return false;
}
String curInputMethodId = mSettings.getSelectedInputMethod();
- final int numImes = mMethodList.size();
+ final List<InputMethodInfo> methodList = mSettings.getMethodList();
+ final int numImes = methodList.size();
if (curInputMethodId != null) {
for (int i = 0; i < numImes; i++) {
- InputMethodInfo imi = mMethodList.get(i);
+ InputMethodInfo imi = methodList.get(i);
if (imi.getId().equals(curInputMethodId)) {
for (String pkg : packages) {
if (imi.getPackageName().equals(pkg)) {
@@ -1339,7 +1335,7 @@
@Override
public void onPackageDataCleared(String packageName, int uid) {
boolean changed = false;
- for (InputMethodInfo imi : mMethodList) {
+ for (InputMethodInfo imi : mSettings.getMethodList()) {
if (imi.getPackageName().equals(packageName)) {
mAdditionalSubtypeMap.remove(imi.getId());
changed = true;
@@ -1347,7 +1343,7 @@
}
if (changed) {
AdditionalSubtypeUtils.save(
- mAdditionalSubtypeMap, mMethodMap, mSettings.getCurrentUserId());
+ mAdditionalSubtypeMap, mSettings.getMethodMap(), mSettings.getUserId());
mChangedPackages.add(packageName);
}
}
@@ -1405,10 +1401,11 @@
InputMethodInfo curIm = null;
String curInputMethodId = mSettings.getSelectedInputMethod();
- final int numImes = mMethodList.size();
+ final List<InputMethodInfo> methodList = mSettings.getMethodList();
+ final int numImes = methodList.size();
if (curInputMethodId != null) {
for (int i = 0; i < numImes; i++) {
- InputMethodInfo imi = mMethodList.get(i);
+ InputMethodInfo imi = methodList.get(i);
final String imiId = imi.getId();
if (imiId.equals(curInputMethodId)) {
curIm = imi;
@@ -1426,8 +1423,7 @@
+ imi.getComponent());
mAdditionalSubtypeMap.remove(imi.getId());
AdditionalSubtypeUtils.save(mAdditionalSubtypeMap,
- mMethodMap,
- mSettings.getCurrentUserId());
+ mSettings.getMethodMap(), mSettings.getUserId());
}
}
}
@@ -1441,7 +1437,7 @@
if (change == PACKAGE_TEMPORARY_CHANGE
|| change == PACKAGE_PERMANENT_CHANGE) {
final PackageManager userAwarePackageManager =
- getPackageManagerForUser(mContext, mSettings.getCurrentUserId());
+ getPackageManagerForUser(mContext, mSettings.getUserId());
ServiceInfo si = null;
try {
si = userAwarePackageManager.getServiceInfo(curIm.getComponent(),
@@ -1577,14 +1573,14 @@
void onUnlockUser(@UserIdInt int userId) {
synchronized (ImfLock.class) {
- final int currentUserId = mSettings.getCurrentUserId();
+ final int currentUserId = mSettings.getUserId();
if (DEBUG) {
Slog.d(TAG, "onUnlockUser: userId=" + userId + " curUserId=" + currentUserId);
}
if (userId != currentUserId) {
return;
}
- mSettings = new InputMethodSettings(mMethodMap, userId);
+ mSettings = InputMethodSettings.createEmptyMap(userId);
if (mSystemReady) {
// We need to rebuild IMEs.
buildInputMethodListLocked(false /* resetDefaultEnabledIme */);
@@ -1658,14 +1654,15 @@
mLastSwitchUserId = userId;
// mSettings should be created before buildInputMethodListLocked
- mSettings = new InputMethodSettings(mMethodMap, userId);
+ mSettings = InputMethodSettings.createEmptyMap(userId);
AdditionalSubtypeUtils.load(mAdditionalSubtypeMap, userId);
mSwitchingController =
- InputMethodSubtypeSwitchingController.createInstanceLocked(context, mMethodMap,
- userId);
+ InputMethodSubtypeSwitchingController.createInstanceLocked(context,
+ mSettings.getMethodMap(), userId);
mHardwareKeyboardShortcutController =
- new HardwareKeyboardShortcutController(mMethodMap, userId);
+ new HardwareKeyboardShortcutController(mSettings.getMethodMap(),
+ mSettings.getUserId());
mMenuController = new InputMethodMenuController(this);
mBindingController =
bindingControllerForTesting != null
@@ -1696,7 +1693,7 @@
@GuardedBy("ImfLock.class")
@UserIdInt
int getCurrentImeUserIdLocked() {
- return mSettings.getCurrentUserId();
+ return mSettings.getUserId();
}
private final class InkWindowInitializer implements Runnable {
@@ -1723,11 +1720,12 @@
private void resetDefaultImeLocked(Context context) {
// Do not reset the default (current) IME when it is a 3rd-party IME
String selectedMethodId = getSelectedMethodIdLocked();
- if (selectedMethodId != null && !mMethodMap.get(selectedMethodId).isSystem()) {
+ if (selectedMethodId != null
+ && !mSettings.getMethodMap().get(selectedMethodId).isSystem()) {
return;
}
final List<InputMethodInfo> suitableImes = InputMethodInfoUtils.getDefaultEnabledImes(
- context, mSettings.getEnabledInputMethodListLocked());
+ context, mSettings.getEnabledInputMethodList());
if (suitableImes.isEmpty()) {
Slog.i(TAG, "No default found");
return;
@@ -1783,7 +1781,7 @@
IInputMethodClientInvoker clientToBeReset) {
if (DEBUG) {
Slog.d(TAG, "Switching user stage 1/3. newUserId=" + newUserId
- + " currentUserId=" + mSettings.getCurrentUserId());
+ + " currentUserId=" + mSettings.getUserId());
}
maybeInitImeNavbarConfigLocked(newUserId);
@@ -1791,7 +1789,7 @@
// ContentObserver should be registered again when the user is changed
mSettingsObserver.registerContentObserverLocked(newUserId);
- mSettings = new InputMethodSettings(mMethodMap, newUserId);
+ mSettings = InputMethodSettings.createEmptyMap(newUserId);
// Additional subtypes should be reset when the user is changed
AdditionalSubtypeUtils.load(mAdditionalSubtypeMap, newUserId);
final String defaultImiId = mSettings.getSelectedInputMethod();
@@ -1822,7 +1820,7 @@
if (initialUserSwitch) {
InputMethodUtils.setNonSelectedSystemImesDisabledUntilUsed(
getPackageManagerForUser(mContext, newUserId),
- mSettings.getEnabledInputMethodListLocked());
+ mSettings.getEnabledInputMethodList());
}
if (DEBUG) {
@@ -1853,7 +1851,7 @@
}
if (!mSystemReady) {
mSystemReady = true;
- final int currentUserId = mSettings.getCurrentUserId();
+ final int currentUserId = mSettings.getUserId();
mStatusBarManagerInternal =
LocalServices.getService(StatusBarManagerInternal.class);
hideStatusBarIconLocked();
@@ -1874,7 +1872,7 @@
// the "mImeDrawsImeNavBarResLazyInitFuture" field.
synchronized (ImfLock.class) {
mImeDrawsImeNavBarResLazyInitFuture = null;
- if (currentUserId != mSettings.getCurrentUserId()) {
+ if (currentUserId != mSettings.getUserId()) {
// This means that the current user is already switched to other user
// before the background task is executed. In this scenario the relevant
// field should already be initialized.
@@ -1899,7 +1897,7 @@
updateFromSettingsLocked(true);
InputMethodUtils.setNonSelectedSystemImesDisabledUntilUsed(
getPackageManagerForUser(mContext, currentUserId),
- mSettings.getEnabledInputMethodListLocked());
+ mSettings.getEnabledInputMethodList());
}
}
}
@@ -1946,7 +1944,7 @@
}
synchronized (ImfLock.class) {
final int[] resolvedUserIds = InputMethodUtils.resolveUserId(userId,
- mSettings.getCurrentUserId(), null);
+ mSettings.getUserId(), null);
if (resolvedUserIds.length != 1) {
return Collections.emptyList();
}
@@ -1969,7 +1967,7 @@
}
synchronized (ImfLock.class) {
final int[] resolvedUserIds = InputMethodUtils.resolveUserId(userId,
- mSettings.getCurrentUserId(), null);
+ mSettings.getUserId(), null);
if (resolvedUserIds.length != 1) {
return Collections.emptyList();
}
@@ -1996,14 +1994,14 @@
}
// Check if selected IME of current user supports handwriting.
- if (userId == mSettings.getCurrentUserId()) {
+ if (userId == mSettings.getUserId()) {
return mBindingController.supportsStylusHandwriting();
}
//TODO(b/197848765): This can be optimized by caching multi-user methodMaps/methodList.
//TODO(b/210039666): use cache.
- final ArrayMap<String, InputMethodInfo> methodMap = queryMethodMapForUser(userId);
- final InputMethodSettings settings = new InputMethodSettings(methodMap, userId);
- final InputMethodInfo imi = methodMap.get(settings.getSelectedInputMethod());
+ final InputMethodSettings settings = queryMethodMapForUser(userId);
+ final InputMethodInfo imi = settings.getMethodMap().get(
+ settings.getSelectedInputMethod());
return imi != null && imi.supportsStylusHandwriting();
}
}
@@ -2023,23 +2021,19 @@
@GuardedBy("ImfLock.class")
private List<InputMethodInfo> getInputMethodListLocked(@UserIdInt int userId,
@DirectBootAwareness int directBootAwareness, int callingUid) {
- final ArrayList<InputMethodInfo> methodList;
final InputMethodSettings settings;
- if (userId == mSettings.getCurrentUserId()
+ if (userId == mSettings.getUserId()
&& directBootAwareness == DirectBootAwareness.AUTO) {
- // Create a copy.
- methodList = new ArrayList<>(mMethodList);
settings = mSettings;
} else {
- final ArrayMap<String, InputMethodInfo> methodMap = new ArrayMap<>();
- methodList = new ArrayList<>();
final ArrayMap<String, List<InputMethodSubtype>> additionalSubtypeMap =
new ArrayMap<>();
AdditionalSubtypeUtils.load(additionalSubtypeMap, userId);
- queryInputMethodServicesInternal(mContext, userId, additionalSubtypeMap, methodMap,
- methodList, directBootAwareness);
- settings = new InputMethodSettings(methodMap, userId);
+ settings = queryInputMethodServicesInternal(mContext, userId, additionalSubtypeMap,
+ directBootAwareness);
}
+ // Create a copy.
+ final ArrayList<InputMethodInfo> methodList = new ArrayList<>(settings.getMethodList());
// filter caller's access to input methods
methodList.removeIf(imi ->
!canCallerAccessInputMethod(imi.getPackageName(), callingUid, userId, settings));
@@ -2051,13 +2045,12 @@
int callingUid) {
final ArrayList<InputMethodInfo> methodList;
final InputMethodSettings settings;
- if (userId == mSettings.getCurrentUserId()) {
- methodList = mSettings.getEnabledInputMethodListLocked();
+ if (userId == mSettings.getUserId()) {
+ methodList = mSettings.getEnabledInputMethodList();
settings = mSettings;
} else {
- final ArrayMap<String, InputMethodInfo> methodMap = queryMethodMapForUser(userId);
- settings = new InputMethodSettings(methodMap, userId);
- methodList = settings.getEnabledInputMethodListLocked();
+ settings = queryMethodMapForUser(userId);
+ methodList = settings.getEnabledInputMethodList();
}
// filter caller's access to input methods
methodList.removeIf(imi ->
@@ -2116,31 +2109,30 @@
@GuardedBy("ImfLock.class")
private List<InputMethodSubtype> getEnabledInputMethodSubtypeListLocked(String imiId,
boolean allowsImplicitlyEnabledSubtypes, @UserIdInt int userId, int callingUid) {
- if (userId == mSettings.getCurrentUserId()) {
+ if (userId == mSettings.getUserId()) {
final InputMethodInfo imi;
String selectedMethodId = getSelectedMethodIdLocked();
if (imiId == null && selectedMethodId != null) {
- imi = mMethodMap.get(selectedMethodId);
+ imi = mSettings.getMethodMap().get(selectedMethodId);
} else {
- imi = mMethodMap.get(imiId);
+ imi = mSettings.getMethodMap().get(imiId);
}
if (imi == null || !canCallerAccessInputMethod(
imi.getPackageName(), callingUid, userId, mSettings)) {
return Collections.emptyList();
}
- return mSettings.getEnabledInputMethodSubtypeListLocked(
+ return mSettings.getEnabledInputMethodSubtypeList(
imi, allowsImplicitlyEnabledSubtypes);
}
- final ArrayMap<String, InputMethodInfo> methodMap = queryMethodMapForUser(userId);
- final InputMethodInfo imi = methodMap.get(imiId);
+ final InputMethodSettings settings = queryMethodMapForUser(userId);
+ final InputMethodInfo imi = settings.getMethodMap().get(imiId);
if (imi == null) {
return Collections.emptyList();
}
- final InputMethodSettings settings = new InputMethodSettings(methodMap, userId);
if (!canCallerAccessInputMethod(imi.getPackageName(), callingUid, userId, settings)) {
return Collections.emptyList();
}
- return settings.getEnabledInputMethodSubtypeListLocked(
+ return settings.getEnabledInputMethodSubtypeList(
imi, allowsImplicitlyEnabledSubtypes);
}
@@ -2183,7 +2175,7 @@
/**
* Hide the IME if the removed user is the current user.
*/
- private void onClientRemoved(ClientController.ClientState client) {
+ private void onClientRemoved(ClientState client) {
synchronized (ImfLock.class) {
clearClientSessionLocked(client);
clearClientSessionForAccessibilityLocked(client);
@@ -2305,7 +2297,7 @@
final boolean restarting = !initial;
final Binder startInputToken = new Binder();
- final StartInputInfo info = new StartInputInfo(mSettings.getCurrentUserId(),
+ final StartInputInfo info = new StartInputInfo(mSettings.getUserId(),
getCurTokenLocked(),
mCurTokenDisplayId, getCurIdLocked(), startInputReason, restarting,
UserHandle.getUserId(mCurClient.mUid),
@@ -2320,9 +2312,9 @@
// same-user scenarios.
// That said ignoring cross-user scenario will never affect IMEs that do not have
// INTERACT_ACROSS_USERS(_FULL) permissions, which is actually almost always the case.
- if (mSettings.getCurrentUserId() == UserHandle.getUserId(
+ if (mSettings.getUserId() == UserHandle.getUserId(
mCurClient.mUid)) {
- mPackageManagerInternal.grantImplicitAccess(mSettings.getCurrentUserId(),
+ mPackageManagerInternal.grantImplicitAccess(mSettings.getUserId(),
null /* intent */, UserHandle.getAppId(getCurMethodUidLocked()),
mCurClient.mUid, true /* direct */);
}
@@ -2343,7 +2335,7 @@
}
String curId = getCurIdLocked();
- final InputMethodInfo curInputMethodInfo = mMethodMap.get(curId);
+ final InputMethodInfo curInputMethodInfo = mSettings.getMethodMap().get(curId);
final boolean suppressesSpellChecker =
curInputMethodInfo != null && curInputMethodInfo.suppressesSpellChecker();
final SparseArray<IAccessibilityInputMethodSession> accessibilityInputMethodSessions =
@@ -2559,7 +2551,7 @@
mVirtualDeviceMethodMap.get(mDeviceIdToShowIme, currentMethodId);
if (Objects.equals(deviceMethodId, currentMethodId)) {
return currentMethodId;
- } else if (!mMethodMap.containsKey(deviceMethodId)) {
+ } else if (!mSettings.getMethodMap().containsKey(deviceMethodId)) {
if (DEBUG) {
Slog.v(TAG, "Disabling IME on virtual device with id " + mDeviceIdToShowIme
+ " because its custom input method is not available: " + deviceMethodId);
@@ -2601,7 +2593,7 @@
if (isSoftInputModeStateVisibleAllowed(unverifiedTargetSdkVersion, startInputFlags)) {
return false;
}
- final InputMethodInfo imi = mMethodMap.get(selectedMethodId);
+ final InputMethodInfo imi = mSettings.getMethodMap().get(selectedMethodId);
if (imi == null) {
return false;
}
@@ -2944,7 +2936,7 @@
} else if (packageName != null) {
if (DEBUG) Slog.d(TAG, "show a small icon for the input method");
final PackageManager userAwarePackageManager =
- getPackageManagerForUser(mContext, mSettings.getCurrentUserId());
+ getPackageManagerForUser(mContext, mSettings.getUserId());
ApplicationInfo applicationInfo = null;
try {
applicationInfo = userAwarePackageManager.getApplicationInfo(packageName,
@@ -3006,7 +2998,7 @@
return false;
}
if (mWindowManagerInternal.isKeyguardShowingAndNotOccluded()
- && mWindowManagerInternal.isKeyguardSecure(mSettings.getCurrentUserId())) {
+ && mWindowManagerInternal.isKeyguardSecure(mSettings.getUserId())) {
return false;
}
if ((visibility & InputMethodService.IME_ACTIVE) == 0
@@ -3023,7 +3015,7 @@
return false;
}
- List<InputMethodInfo> imes = mSettings.getEnabledInputMethodListWithFilterLocked(
+ List<InputMethodInfo> imes = mSettings.getEnabledInputMethodListWithFilter(
InputMethodInfo::shouldShowInInputMethodPicker);
final int numImes = imes.size();
if (numImes > 2) return true;
@@ -3035,7 +3027,7 @@
for (int i = 0; i < numImes; ++i) {
final InputMethodInfo imi = imes.get(i);
final List<InputMethodSubtype> subtypes =
- mSettings.getEnabledInputMethodSubtypeListLocked(imi, true);
+ mSettings.getEnabledInputMethodSubtypeList(imi, true);
final int subtypeCount = subtypes.size();
if (subtypeCount == 0) {
++nonAuxCount;
@@ -3185,9 +3177,9 @@
void updateInputMethodsFromSettingsLocked(boolean enabledMayChange) {
if (enabledMayChange) {
final PackageManager userAwarePackageManager = getPackageManagerForUser(mContext,
- mSettings.getCurrentUserId());
+ mSettings.getUserId());
- List<InputMethodInfo> enabled = mSettings.getEnabledInputMethodListLocked();
+ List<InputMethodInfo> enabled = mSettings.getEnabledInputMethodList();
for (int i = 0; i < enabled.size(); i++) {
// We allow the user to select "disabled until used" apps, so if they
// are enabling one of those here we now need to make it enabled.
@@ -3233,18 +3225,18 @@
}
// TODO: Instantiate mSwitchingController for each user.
- if (mSettings.getCurrentUserId() == mSwitchingController.getUserId()) {
- mSwitchingController.resetCircularListLocked(mMethodMap);
+ if (mSettings.getUserId() == mSwitchingController.getUserId()) {
+ mSwitchingController.resetCircularListLocked(mSettings.getMethodMap());
} else {
mSwitchingController = InputMethodSubtypeSwitchingController.createInstanceLocked(
- mContext, mMethodMap, mSettings.getCurrentUserId());
+ mContext, mSettings.getMethodMap(), mSettings.getUserId());
}
// TODO: Instantiate mHardwareKeyboardShortcutController for each user.
- if (mSettings.getCurrentUserId() == mHardwareKeyboardShortcutController.getUserId()) {
- mHardwareKeyboardShortcutController.reset(mMethodMap);
+ if (mSettings.getUserId() == mHardwareKeyboardShortcutController.getUserId()) {
+ mHardwareKeyboardShortcutController.reset(mSettings.getMethodMap());
} else {
mHardwareKeyboardShortcutController = new HardwareKeyboardShortcutController(
- mMethodMap, mSettings.getCurrentUserId());
+ mSettings.getMethodMap(), mSettings.getUserId());
}
sendOnNavButtonFlagsChangedLocked();
}
@@ -3268,14 +3260,14 @@
@GuardedBy("ImfLock.class")
void setInputMethodLocked(String id, int subtypeId, int deviceId) {
- InputMethodInfo info = mMethodMap.get(id);
+ InputMethodInfo info = mSettings.getMethodMap().get(id);
if (info == null) {
throw getExceptionForUnknownImeId(id);
}
// See if we need to notify a subtype change within the same IME.
if (id.equals(getSelectedMethodIdLocked())) {
- final int userId = mSettings.getCurrentUserId();
+ final int userId = mSettings.getUserId();
final int subtypeCount = info.getSubtypeCount();
if (subtypeCount <= 0) {
notifyInputMethodSubtypeChangedLocked(userId, info, null);
@@ -3786,7 +3778,7 @@
return InputBindResult.USER_SWITCHING;
}
final int[] profileIdsWithDisabled = mUserManagerInternal.getProfileIds(
- mSettings.getCurrentUserId(), false /* enabledOnly */);
+ mSettings.getUserId(), false /* enabledOnly */);
for (int profileId : profileIdsWithDisabled) {
if (profileId == userId) {
scheduleSwitchUserTaskLocked(userId, cs.mClient);
@@ -3805,7 +3797,7 @@
mVisibilityStateComputer.mShowForced = false;
}
- final int currentUserId = mSettings.getCurrentUserId();
+ final int currentUserId = mSettings.getUserId();
if (userId != currentUserId) {
if (ArrayUtils.contains(
mUserManagerInternal.getProfileIds(currentUserId, false), userId)) {
@@ -3949,7 +3941,7 @@
&& mCurFocusedWindowClient.mClient.asBinder() == client.asBinder()) {
return true;
}
- if (mSettings.getCurrentUserId() != UserHandle.getUserId(uid)) {
+ if (mSettings.getUserId() != UserHandle.getUserId(uid)) {
return false;
}
if (getCurIntentLocked() != null && InputMethodUtils.checkIfPackageBelongsToUid(
@@ -4017,7 +4009,7 @@
if (!calledWithValidTokenLocked(token)) {
return;
}
- final InputMethodInfo imi = mMethodMap.get(id);
+ final InputMethodInfo imi = mSettings.getMethodMap().get(id);
if (imi == null || !canCallerAccessInputMethod(
imi.getPackageName(), callingUid, userId, mSettings)) {
throw getExceptionForUnknownImeId(id);
@@ -4035,7 +4027,7 @@
if (!calledWithValidTokenLocked(token)) {
return;
}
- final InputMethodInfo imi = mMethodMap.get(id);
+ final InputMethodInfo imi = mSettings.getMethodMap().get(id);
if (imi == null || !canCallerAccessInputMethod(
imi.getPackageName(), callingUid, userId, mSettings)) {
throw getExceptionForUnknownImeId(id);
@@ -4055,10 +4047,10 @@
if (!calledWithValidTokenLocked(token)) {
return false;
}
- final Pair<String, String> lastIme = mSettings.getLastInputMethodAndSubtypeLocked();
+ final Pair<String, String> lastIme = mSettings.getLastInputMethodAndSubtype();
final InputMethodInfo lastImi;
if (lastIme != null) {
- lastImi = mMethodMap.get(lastIme.first);
+ lastImi = mSettings.getMethodMap().get(lastIme.first);
} else {
lastImi = null;
}
@@ -4082,7 +4074,7 @@
// This is a safety net. If the currentSubtype can't be added to the history
// and the framework couldn't find the last ime, we will make the last ime be
// the most applicable enabled keyboard subtype of the system imes.
- final List<InputMethodInfo> enabled = mSettings.getEnabledInputMethodListLocked();
+ final List<InputMethodInfo> enabled = mSettings.getEnabledInputMethodList();
if (enabled != null) {
final int enabledCount = enabled.size();
final String locale;
@@ -4090,14 +4082,13 @@
&& !TextUtils.isEmpty(mCurrentSubtype.getLocale())) {
locale = mCurrentSubtype.getLocale();
} else {
- locale = SystemLocaleWrapper.get(mSettings.getCurrentUserId()).get(0)
- .toString();
+ locale = SystemLocaleWrapper.get(mSettings.getUserId()).get(0).toString();
}
for (int i = 0; i < enabledCount; ++i) {
final InputMethodInfo imi = enabled.get(i);
if (imi.getSubtypeCount() > 0 && imi.isSystem()) {
InputMethodSubtype keyboardSubtype =
- SubtypeUtils.findLastResortApplicableSubtypeLocked(
+ SubtypeUtils.findLastResortApplicableSubtype(
SubtypeUtils.getSubtypes(imi),
SubtypeUtils.SUBTYPE_MODE_KEYBOARD, locale, true);
if (keyboardSubtype != null) {
@@ -4139,7 +4130,8 @@
@GuardedBy("ImfLock.class")
private boolean switchToNextInputMethodLocked(@Nullable IBinder token, boolean onlyCurrentIme) {
final ImeSubtypeListItem nextSubtype = mSwitchingController.getNextInputMethodLocked(
- onlyCurrentIme, mMethodMap.get(getSelectedMethodIdLocked()), mCurrentSubtype);
+ onlyCurrentIme, mSettings.getMethodMap().get(getSelectedMethodIdLocked()),
+ mCurrentSubtype);
if (nextSubtype == null) {
return false;
}
@@ -4155,8 +4147,8 @@
return false;
}
final ImeSubtypeListItem nextSubtype = mSwitchingController.getNextInputMethodLocked(
- false /* onlyCurrentIme */, mMethodMap.get(getSelectedMethodIdLocked()),
- mCurrentSubtype);
+ false /* onlyCurrentIme */,
+ mSettings.getMethodMap().get(getSelectedMethodIdLocked()), mCurrentSubtype);
return nextSubtype != null;
}
}
@@ -4168,13 +4160,12 @@
Manifest.permission.INTERACT_ACROSS_USERS_FULL, null);
}
synchronized (ImfLock.class) {
- if (mSettings.getCurrentUserId() == userId) {
- return mSettings.getLastInputMethodSubtypeLocked();
+ if (mSettings.getUserId() == userId) {
+ return mSettings.getLastInputMethodSubtype();
}
- final ArrayMap<String, InputMethodInfo> methodMap = queryMethodMapForUser(userId);
- final InputMethodSettings settings = new InputMethodSettings(methodMap, userId);
- return settings.getLastInputMethodSubtypeLocked();
+ final InputMethodSettings settings = queryMethodMapForUser(userId);
+ return settings.getLastInputMethodSubtype();
}
}
@@ -4204,7 +4195,7 @@
return;
}
- if (mSettings.getCurrentUserId() == userId) {
+ if (mSettings.getUserId() == userId) {
if (!mSettings.setAdditionalInputMethodSubtypes(imiId, toBeAdded,
mAdditionalSubtypeMap, mPackageManagerInternal, callingUid)) {
return;
@@ -4218,14 +4209,11 @@
return;
}
- final ArrayMap<String, InputMethodInfo> methodMap = new ArrayMap<>();
- final ArrayList<InputMethodInfo> methodList = new ArrayList<>();
final ArrayMap<String, List<InputMethodSubtype>> additionalSubtypeMap =
new ArrayMap<>();
AdditionalSubtypeUtils.load(additionalSubtypeMap, userId);
- queryInputMethodServicesInternal(mContext, userId, additionalSubtypeMap, methodMap,
- methodList, DirectBootAwareness.AUTO);
- final InputMethodSettings settings = new InputMethodSettings(methodMap, userId);
+ final InputMethodSettings settings = queryInputMethodServicesInternal(mContext, userId,
+ additionalSubtypeMap, DirectBootAwareness.AUTO);
settings.setAdditionalInputMethodSubtypes(imiId, toBeAdded, additionalSubtypeMap,
mPackageManagerInternal, callingUid);
}
@@ -4251,10 +4239,9 @@
final long ident = Binder.clearCallingIdentity();
try {
synchronized (ImfLock.class) {
- final boolean currentUser = (mSettings.getCurrentUserId() == userId);
+ final boolean currentUser = (mSettings.getUserId() == userId);
final InputMethodSettings settings = currentUser
- ? mSettings
- : new InputMethodSettings(queryMethodMapForUser(userId), userId);
+ ? mSettings : queryMethodMapForUser(userId);
if (!settings.setEnabledInputMethodSubtypes(imeId, subtypeHashCodes)) {
return;
}
@@ -4623,10 +4610,11 @@
}
return;
}
- if (mSettings.getCurrentUserId() != mSwitchingController.getUserId()) {
+ if (mSettings.getUserId() != mSwitchingController.getUserId()) {
return;
}
- final InputMethodInfo imi = mMethodMap.get(getSelectedMethodIdLocked());
+ final InputMethodInfo imi =
+ mSettings.getMethodMap().get(getSelectedMethodIdLocked());
if (imi != null) {
mSwitchingController.onUserActionLocked(imi, mCurrentSubtype);
}
@@ -4684,8 +4672,8 @@
return;
} else {
// Called with current IME's token.
- if (mMethodMap.get(id) != null
- && mSettings.getEnabledInputMethodListWithFilterLocked(
+ if (mSettings.getMethodMap().get(id) != null
+ && mSettings.getEnabledInputMethodListWithFilter(
(info) -> info.getId().equals(id)).isEmpty()) {
throw new IllegalStateException("Requested IME is not enabled: " + id);
}
@@ -4857,8 +4845,7 @@
}
synchronized (ImfLock.class) {
final boolean isScreenLocked = mWindowManagerInternal.isKeyguardLocked()
- && mWindowManagerInternal.isKeyguardSecure(
- mSettings.getCurrentUserId());
+ && mWindowManagerInternal.isKeyguardSecure(mSettings.getUserId());
final String lastInputMethodId = mSettings.getSelectedInputMethod();
int lastInputMethodSubtypeId =
mSettings.getSelectedInputMethodSubtypeId(lastInputMethodId);
@@ -4866,7 +4853,7 @@
final List<ImeSubtypeListItem> imList = InputMethodSubtypeSwitchingController
.getSortedInputMethodAndSubtypeList(
showAuxSubtypes, isScreenLocked, true /* forImeMenu */,
- mContext, mMethodMap, mSettings.getCurrentUserId());
+ mContext, mSettings.getMethodMap(), mSettings.getUserId());
mMenuController.showInputMethodMenuLocked(showAuxSubtypes, displayId,
lastInputMethodId, lastInputMethodSubtypeId, imList);
}
@@ -5050,7 +5037,7 @@
@GuardedBy("ImfLock.class")
private boolean chooseNewDefaultIMELocked() {
final InputMethodInfo imi = InputMethodInfoUtils.getMostApplicableDefaultIME(
- mSettings.getEnabledInputMethodListLocked());
+ mSettings.getEnabledInputMethodList());
if (imi != null) {
if (DEBUG) {
Slog.d(TAG, "New default IME was selected: " + imi.getId());
@@ -5062,17 +5049,14 @@
return false;
}
- static void queryInputMethodServicesInternal(Context context,
+ @NonNull
+ static InputMethodSettings queryInputMethodServicesInternal(Context context,
@UserIdInt int userId, ArrayMap<String, List<InputMethodSubtype>> additionalSubtypeMap,
- ArrayMap<String, InputMethodInfo> methodMap, ArrayList<InputMethodInfo> methodList,
@DirectBootAwareness int directBootAwareness) {
final Context userAwareContext = context.getUserId() == userId
? context
: context.createContextAsUser(UserHandle.of(userId), 0 /* flags */);
- methodList.clear();
- methodMap.clear();
-
final int directBootAwarenessFlags;
switch (directBootAwareness) {
case DirectBootAwareness.ANY:
@@ -5095,24 +5079,23 @@
new Intent(InputMethod.SERVICE_INTERFACE),
PackageManager.ResolveInfoFlags.of(flags));
- methodList.ensureCapacity(services.size());
- methodMap.ensureCapacity(services.size());
-
// Note: This is a temporary solution for Bug 261723412. If there is any better solution,
// we should remove this data dependency.
final List<String> enabledInputMethodList =
InputMethodUtils.getEnabledInputMethodIdsForFiltering(context, userId);
- filterInputMethodServices(additionalSubtypeMap, methodMap, methodList,
- enabledInputMethodList, userAwareContext, services);
+ final InputMethodMap methodMap = filterInputMethodServices(
+ additionalSubtypeMap, enabledInputMethodList, userAwareContext, services);
+ return InputMethodSettings.create(methodMap, userId);
}
- static void filterInputMethodServices(
+ @NonNull
+ static InputMethodMap filterInputMethodServices(
ArrayMap<String, List<InputMethodSubtype>> additionalSubtypeMap,
- ArrayMap<String, InputMethodInfo> methodMap, ArrayList<InputMethodInfo> methodList,
List<String> enabledInputMethodList, Context userAwareContext,
List<ResolveInfo> services) {
final ArrayMap<String, Integer> imiPackageCount = new ArrayMap<>();
+ final ArrayMap<String, InputMethodInfo> methodMap = new ArrayMap<>(services.size());
for (int i = 0; i < services.size(); ++i) {
ResolveInfo ri = services.get(i);
@@ -5141,7 +5124,6 @@
imiPackageCount.put(packageName,
1 + imiPackageCount.getOrDefault(packageName, 0));
- methodList.add(imi);
methodMap.put(imi.getId(), imi);
if (DEBUG) {
Slog.d(TAG, "Found an input method " + imi);
@@ -5153,6 +5135,7 @@
Slog.wtf(TAG, "Unable to load input method " + imeId, e);
}
}
+ return InputMethodMap.of(methodMap);
}
@GuardedBy("ImfLock.class")
@@ -5168,8 +5151,8 @@
mMethodMapUpdateCount++;
mMyPackageMonitor.clearKnownImePackageNamesLocked();
- queryInputMethodServicesInternal(mContext, mSettings.getCurrentUserId(),
- mAdditionalSubtypeMap, mMethodMap, mMethodList, DirectBootAwareness.AUTO);
+ mSettings = queryInputMethodServicesInternal(mContext, mSettings.getUserId(),
+ mAdditionalSubtypeMap, DirectBootAwareness.AUTO);
// Construct the set of possible IME packages for onPackageChanged() to avoid false
// negatives when the package state remains to be the same but only the component state is
@@ -5181,7 +5164,7 @@
final List<ResolveInfo> allInputMethodServices =
mContext.getPackageManager().queryIntentServicesAsUser(
new Intent(InputMethod.SERVICE_INTERFACE),
- PackageManager.MATCH_DISABLED_COMPONENTS, mSettings.getCurrentUserId());
+ PackageManager.MATCH_DISABLED_COMPONENTS, mSettings.getUserId());
final int numImes = allInputMethodServices.size();
for (int i = 0; i < numImes; ++i) {
final ServiceInfo si = allInputMethodServices.get(i).serviceInfo;
@@ -5196,11 +5179,11 @@
if (!resetDefaultEnabledIme) {
boolean enabledImeFound = false;
boolean enabledNonAuxImeFound = false;
- final List<InputMethodInfo> enabledImes = mSettings.getEnabledInputMethodListLocked();
+ final List<InputMethodInfo> enabledImes = mSettings.getEnabledInputMethodList();
final int numImes = enabledImes.size();
for (int i = 0; i < numImes; ++i) {
final InputMethodInfo imi = enabledImes.get(i);
- if (mMethodList.contains(imi)) {
+ if (mSettings.getMethodMap().containsKey(imi.getId())) {
enabledImeFound = true;
if (!imi.isAuxiliaryIme()) {
enabledNonAuxImeFound = true;
@@ -5224,7 +5207,7 @@
if (resetDefaultEnabledIme || reenableMinimumNonAuxSystemImes) {
final ArrayList<InputMethodInfo> defaultEnabledIme =
- InputMethodInfoUtils.getDefaultEnabledImes(mContext, mMethodList,
+ InputMethodInfoUtils.getDefaultEnabledImes(mContext, mSettings.getMethodList(),
reenableMinimumNonAuxSystemImes);
final int numImes = defaultEnabledIme.size();
for (int i = 0; i < numImes; ++i) {
@@ -5238,7 +5221,7 @@
final String defaultImiId = mSettings.getSelectedInputMethod();
if (!TextUtils.isEmpty(defaultImiId)) {
- if (!mMethodMap.containsKey(defaultImiId)) {
+ if (!mSettings.getMethodMap().containsKey(defaultImiId)) {
Slog.w(TAG, "Default IME is uninstalled. Choose new default IME.");
if (chooseNewDefaultIMELocked()) {
updateInputMethodsFromSettingsLocked(true);
@@ -5252,26 +5235,26 @@
updateDefaultVoiceImeIfNeededLocked();
// TODO: Instantiate mSwitchingController for each user.
- if (mSettings.getCurrentUserId() == mSwitchingController.getUserId()) {
- mSwitchingController.resetCircularListLocked(mMethodMap);
+ if (mSettings.getUserId() == mSwitchingController.getUserId()) {
+ mSwitchingController.resetCircularListLocked(mSettings.getMethodMap());
} else {
mSwitchingController = InputMethodSubtypeSwitchingController.createInstanceLocked(
- mContext, mMethodMap, mSettings.getCurrentUserId());
+ mContext, mSettings.getMethodMap(), mSettings.getUserId());
}
// TODO: Instantiate mHardwareKeyboardShortcutController for each user.
- if (mSettings.getCurrentUserId() == mHardwareKeyboardShortcutController.getUserId()) {
- mHardwareKeyboardShortcutController.reset(mMethodMap);
+ if (mSettings.getUserId() == mHardwareKeyboardShortcutController.getUserId()) {
+ mHardwareKeyboardShortcutController.reset(mSettings.getMethodMap());
} else {
mHardwareKeyboardShortcutController = new HardwareKeyboardShortcutController(
- mMethodMap, mSettings.getCurrentUserId());
+ mSettings.getMethodMap(), mSettings.getUserId());
}
sendOnNavButtonFlagsChangedLocked();
// Notify InputMethodListListeners of the new installed InputMethods.
- final List<InputMethodInfo> inputMethodList = new ArrayList<>(mMethodList);
+ final List<InputMethodInfo> inputMethodList = mSettings.getMethodList();
mHandler.obtainMessage(MSG_DISPATCH_ON_INPUT_METHOD_LIST_UPDATED,
- mSettings.getCurrentUserId(), 0 /* unused */, inputMethodList).sendToTarget();
+ mSettings.getUserId(), 0 /* unused */, inputMethodList).sendToTarget();
}
@GuardedBy("ImfLock.class")
@@ -5290,7 +5273,7 @@
mContext.getString(com.android.internal.R.string.config_systemSpeechRecognizer);
final String currentDefaultVoiceImeId = mSettings.getDefaultVoiceInputMethod();
final InputMethodInfo newSystemVoiceIme = InputMethodInfoUtils.chooseSystemVoiceIme(
- mMethodMap, systemSpeechRecognizer, currentDefaultVoiceImeId);
+ mSettings.getMethodMap(), systemSpeechRecognizer, currentDefaultVoiceImeId);
if (newSystemVoiceIme == null) {
if (DEBUG) {
Slog.i(TAG, "Found no valid default Voice IME. If the user is still locked,"
@@ -5341,9 +5324,9 @@
return false;
} else {
final List<Pair<String, ArrayList<String>>> enabledInputMethodsList = mSettings
- .getEnabledInputMethodsAndSubtypeListLocked();
+ .getEnabledInputMethodsAndSubtypeList();
StringBuilder builder = new StringBuilder();
- if (mSettings.buildAndPutEnabledInputMethodsStrRemovingIdLocked(
+ if (mSettings.buildAndPutEnabledInputMethodsStrRemovingId(
builder, enabledInputMethodsList, id)) {
if (mDeviceIdToShowIme == DEVICE_ID_DEFAULT) {
// Disabled input method is currently selected, switch to another one.
@@ -5357,7 +5340,7 @@
// new default one but only update the settings.
InputMethodInfo newDefaultIme =
InputMethodInfoUtils.getMostApplicableDefaultIME(
- mSettings.getEnabledInputMethodListLocked());
+ mSettings.getEnabledInputMethodList());
mSettings.putSelectedDefaultDeviceInputMethod(
newDefaultIme == null ? "" : newDefaultIme.getId());
}
@@ -5392,7 +5375,7 @@
mCurrentSubtype = getCurrentInputMethodSubtypeLocked();
}
}
- notifyInputMethodSubtypeChangedLocked(mSettings.getCurrentUserId(), imi, mCurrentSubtype);
+ notifyInputMethodSubtypeChangedLocked(mSettings.getUserId(), imi, mCurrentSubtype);
if (!setSubtypeOnly) {
// Set InputMethod here
@@ -5402,11 +5385,11 @@
@GuardedBy("ImfLock.class")
private void resetSelectedInputMethodAndSubtypeLocked(String newDefaultIme) {
- InputMethodInfo imi = mMethodMap.get(newDefaultIme);
+ InputMethodInfo imi = mSettings.getMethodMap().get(newDefaultIme);
int lastSubtypeId = NOT_A_SUBTYPE_ID;
// newDefaultIme is empty when there is no candidate for the selected IME.
if (imi != null && !TextUtils.isEmpty(newDefaultIme)) {
- String subtypeHashCode = mSettings.getLastSubtypeForInputMethodLocked(newDefaultIme);
+ String subtypeHashCode = mSettings.getLastSubtypeForInputMethod(newDefaultIme);
if (subtypeHashCode != null) {
try {
lastSubtypeId = SubtypeUtils.getSubtypeIdFromHashCode(imi,
@@ -5433,12 +5416,11 @@
Manifest.permission.INTERACT_ACROSS_USERS_FULL, null);
}
synchronized (ImfLock.class) {
- if (mSettings.getCurrentUserId() == userId) {
+ if (mSettings.getUserId() == userId) {
return getCurrentInputMethodSubtypeLocked();
}
- final ArrayMap<String, InputMethodInfo> methodMap = queryMethodMapForUser(userId);
- final InputMethodSettings settings = new InputMethodSettings(methodMap, userId);
+ final InputMethodSettings settings = queryMethodMapForUser(userId);
return settings.getCurrentInputMethodSubtypeForNonCurrentUsers();
}
}
@@ -5460,7 +5442,7 @@
return null;
}
final boolean subtypeIsSelected = mSettings.isSubtypeSelected();
- final InputMethodInfo imi = mMethodMap.get(selectedMethodId);
+ final InputMethodInfo imi = mSettings.getMethodMap().get(selectedMethodId);
if (imi == null || imi.getSubtypeCount() == 0) {
return null;
}
@@ -5472,19 +5454,19 @@
// the most applicable subtype from explicitly or implicitly enabled
// subtypes.
List<InputMethodSubtype> explicitlyOrImplicitlyEnabledSubtypes =
- mSettings.getEnabledInputMethodSubtypeListLocked(imi, true);
+ mSettings.getEnabledInputMethodSubtypeList(imi, true);
// If there is only one explicitly or implicitly enabled subtype,
// just returns it.
if (explicitlyOrImplicitlyEnabledSubtypes.size() == 1) {
mCurrentSubtype = explicitlyOrImplicitlyEnabledSubtypes.get(0);
} else if (explicitlyOrImplicitlyEnabledSubtypes.size() > 1) {
- final String locale = SystemLocaleWrapper.get(mSettings.getCurrentUserId())
+ final String locale = SystemLocaleWrapper.get(mSettings.getUserId())
.get(0).toString();
- mCurrentSubtype = SubtypeUtils.findLastResortApplicableSubtypeLocked(
+ mCurrentSubtype = SubtypeUtils.findLastResortApplicableSubtype(
explicitlyOrImplicitlyEnabledSubtypes,
SubtypeUtils.SUBTYPE_MODE_KEYBOARD, locale, true);
if (mCurrentSubtype == null) {
- mCurrentSubtype = SubtypeUtils.findLastResortApplicableSubtypeLocked(
+ mCurrentSubtype = SubtypeUtils.findLastResortApplicableSubtype(
explicitlyOrImplicitlyEnabledSubtypes, null, locale, true);
}
}
@@ -5501,46 +5483,42 @@
*/
@GuardedBy("ImfLock.class")
private InputMethodInfo queryDefaultInputMethodForUserIdLocked(@UserIdInt int userId) {
- if (userId == mSettings.getCurrentUserId()) {
- return mMethodMap.get(mSettings.getSelectedInputMethod());
+ final InputMethodSettings settings;
+ if (userId == mSettings.getUserId()) {
+ settings = mSettings;
+ } else {
+ final ArrayMap<String, List<InputMethodSubtype>> additionalSubtypeMap =
+ new ArrayMap<>();
+ AdditionalSubtypeUtils.load(additionalSubtypeMap, userId);
+ settings = queryInputMethodServicesInternal(mContext, userId,
+ additionalSubtypeMap, DirectBootAwareness.AUTO);
}
-
- final ArrayMap<String, InputMethodInfo> methodMap = new ArrayMap<>();
- final ArrayList<InputMethodInfo> methodList = new ArrayList<>();
- final ArrayMap<String, List<InputMethodSubtype>> additionalSubtypeMap = new ArrayMap<>();
- AdditionalSubtypeUtils.load(additionalSubtypeMap, userId);
- queryInputMethodServicesInternal(mContext, userId, additionalSubtypeMap, methodMap,
- methodList, DirectBootAwareness.AUTO);
- InputMethodSettings settings = new InputMethodSettings(methodMap, userId);
- return methodMap.get(settings.getSelectedInputMethod());
+ return settings.getMethodMap().get(settings.getSelectedInputMethod());
}
- private ArrayMap<String, InputMethodInfo> queryMethodMapForUser(@UserIdInt int userId) {
- final ArrayMap<String, InputMethodInfo> methodMap = new ArrayMap<>();
- final ArrayList<InputMethodInfo> methodList = new ArrayList<>();
+ private InputMethodSettings queryMethodMapForUser(@UserIdInt int userId) {
final ArrayMap<String, List<InputMethodSubtype>> additionalSubtypeMap =
new ArrayMap<>();
AdditionalSubtypeUtils.load(additionalSubtypeMap, userId);
- queryInputMethodServicesInternal(mContext, userId, additionalSubtypeMap,
- methodMap, methodList, DirectBootAwareness.AUTO);
- return methodMap;
+ return queryInputMethodServicesInternal(mContext, userId, additionalSubtypeMap,
+ DirectBootAwareness.AUTO);
}
@GuardedBy("ImfLock.class")
private boolean switchToInputMethodLocked(String imeId, @UserIdInt int userId) {
- if (userId == mSettings.getCurrentUserId()) {
- if (!mMethodMap.containsKey(imeId)
- || !mSettings.getEnabledInputMethodListLocked()
- .contains(mMethodMap.get(imeId))) {
+ if (userId == mSettings.getUserId()) {
+ if (!mSettings.getMethodMap().containsKey(imeId)
+ || !mSettings.getEnabledInputMethodList()
+ .contains(mSettings.getMethodMap().get(imeId))) {
return false; // IME is not found or not enabled.
}
setInputMethodLocked(imeId, NOT_A_SUBTYPE_ID);
return true;
}
- final ArrayMap<String, InputMethodInfo> methodMap = queryMethodMapForUser(userId);
- final InputMethodSettings settings = new InputMethodSettings(methodMap, userId);
- if (!methodMap.containsKey(imeId)
- || !settings.getEnabledInputMethodListLocked().contains(methodMap.get(imeId))) {
+ final InputMethodSettings settings = queryMethodMapForUser(userId);
+ if (!settings.getMethodMap().containsKey(imeId)
+ || !settings.getEnabledInputMethodList().contains(
+ settings.getMethodMap().get(imeId))) {
return false; // IME is not found or not enabled.
}
settings.putSelectedInputMethod(imeId);
@@ -5578,7 +5556,8 @@
@GuardedBy("ImfLock.class")
private void switchKeyboardLayoutLocked(int direction) {
- final InputMethodInfo currentImi = mMethodMap.get(getSelectedMethodIdLocked());
+ final InputMethodInfo currentImi = mSettings.getMethodMap().get(
+ getSelectedMethodIdLocked());
if (currentImi == null) {
return;
}
@@ -5590,7 +5569,7 @@
if (nextSubtypeHandle == null) {
return;
}
- final InputMethodInfo nextImi = mMethodMap.get(nextSubtypeHandle.getImeId());
+ final InputMethodInfo nextImi = mSettings.getMethodMap().get(nextSubtypeHandle.getImeId());
if (nextImi == null) {
return;
}
@@ -5669,16 +5648,15 @@
@Override
public boolean setInputMethodEnabled(String imeId, boolean enabled, @UserIdInt int userId) {
synchronized (ImfLock.class) {
- if (userId == mSettings.getCurrentUserId()) {
- if (!mMethodMap.containsKey(imeId)) {
+ if (userId == mSettings.getUserId()) {
+ if (!mSettings.getMethodMap().containsKey(imeId)) {
return false; // IME is not found.
}
setInputMethodEnabledLocked(imeId, enabled);
return true;
}
- final ArrayMap<String, InputMethodInfo> methodMap = queryMethodMapForUser(userId);
- final InputMethodSettings settings = new InputMethodSettings(methodMap, userId);
- if (!methodMap.containsKey(imeId)) {
+ final InputMethodSettings settings = queryMethodMapForUser(userId);
+ if (!settings.getMethodMap().containsKey(imeId)) {
return false; // IME is not found.
}
if (enabled) {
@@ -5689,9 +5667,9 @@
settings.putEnabledInputMethodsStr(newEnabledImeIdsStr);
}
} else {
- settings.buildAndPutEnabledInputMethodsStrRemovingIdLocked(
+ settings.buildAndPutEnabledInputMethodsStrRemovingId(
new StringBuilder(),
- settings.getEnabledInputMethodsAndSubtypeListLocked(), imeId);
+ settings.getEnabledInputMethodsAndSubtypeList(), imeId);
}
return true;
}
@@ -5997,10 +5975,11 @@
synchronized (ImfLock.class) {
p.println("Current Input Method Manager state:");
- int numImes = mMethodList.size();
+ final List<InputMethodInfo> methodList = mSettings.getMethodList();
+ int numImes = methodList.size();
p.println(" Input Methods: mMethodMapUpdateCount=" + mMethodMapUpdateCount);
for (int i = 0; i < numImes; i++) {
- InputMethodInfo info = mMethodList.get(i);
+ InputMethodInfo info = methodList.get(i);
p.println(" InputMethod #" + i + ":");
info.dump(p, " ");
}
@@ -6047,7 +6026,7 @@
p.println(" mSwitchingController:");
mSwitchingController.dump(p);
p.println(" mSettings:");
- mSettings.dumpLocked(p, " ");
+ mSettings.dump(p, " ");
p.println(" mStartInputHistory:");
mStartInputHistory.dump(pw, " ");
@@ -6311,7 +6290,7 @@
}
synchronized (ImfLock.class) {
final int[] userIds = InputMethodUtils.resolveUserId(userIdToBeResolved,
- mSettings.getCurrentUserId(), shellCommand.getErrPrintWriter());
+ mSettings.getUserId(), shellCommand.getErrPrintWriter());
try (PrintWriter pr = shellCommand.getOutPrintWriter()) {
for (int userId : userIds) {
final List<InputMethodInfo> methods = all
@@ -6356,7 +6335,7 @@
PrintWriter error = shellCommand.getErrPrintWriter()) {
synchronized (ImfLock.class) {
final int[] userIds = InputMethodUtils.resolveUserId(userIdToBeResolved,
- mSettings.getCurrentUserId(), shellCommand.getErrPrintWriter());
+ mSettings.getUserId(), shellCommand.getErrPrintWriter());
for (int userId : userIds) {
if (!userHasDebugPriv(userId, shellCommand)) {
continue;
@@ -6415,17 +6394,16 @@
PrintWriter error) {
boolean failedToEnableUnknownIme = false;
boolean previouslyEnabled = false;
- if (userId == mSettings.getCurrentUserId()) {
- if (enabled && !mMethodMap.containsKey(imeId)) {
+ if (userId == mSettings.getUserId()) {
+ if (enabled && !mSettings.getMethodMap().containsKey(imeId)) {
failedToEnableUnknownIme = true;
} else {
previouslyEnabled = setInputMethodEnabledLocked(imeId, enabled);
}
} else {
- final ArrayMap<String, InputMethodInfo> methodMap = queryMethodMapForUser(userId);
- final InputMethodSettings settings = new InputMethodSettings(methodMap, userId);
+ final InputMethodSettings settings = queryMethodMapForUser(userId);
if (enabled) {
- if (!methodMap.containsKey(imeId)) {
+ if (!settings.getMethodMap().containsKey(imeId)) {
failedToEnableUnknownIme = true;
} else {
final String enabledImeIdsStr = settings.getEnabledInputMethodsStr();
@@ -6438,9 +6416,9 @@
}
} else {
previouslyEnabled =
- settings.buildAndPutEnabledInputMethodsStrRemovingIdLocked(
+ settings.buildAndPutEnabledInputMethodsStrRemovingId(
new StringBuilder(),
- settings.getEnabledInputMethodsAndSubtypeListLocked(), imeId);
+ settings.getEnabledInputMethodsAndSubtypeList(), imeId);
}
}
if (failedToEnableUnknownIme) {
@@ -6478,7 +6456,7 @@
PrintWriter error = shellCommand.getErrPrintWriter()) {
synchronized (ImfLock.class) {
final int[] userIds = InputMethodUtils.resolveUserId(userIdToBeResolved,
- mSettings.getCurrentUserId(), shellCommand.getErrPrintWriter());
+ mSettings.getUserId(), shellCommand.getErrPrintWriter());
for (int userId : userIds) {
if (!userHasDebugPriv(userId, shellCommand)) {
continue;
@@ -6518,7 +6496,7 @@
synchronized (ImfLock.class) {
try (PrintWriter out = shellCommand.getOutPrintWriter()) {
final int[] userIds = InputMethodUtils.resolveUserId(userIdToBeResolved,
- mSettings.getCurrentUserId(), shellCommand.getErrPrintWriter());
+ mSettings.getUserId(), shellCommand.getErrPrintWriter());
for (int userId : userIds) {
if (!userHasDebugPriv(userId, shellCommand)) {
continue;
@@ -6530,16 +6508,16 @@
}
final String nextIme;
final List<InputMethodInfo> nextEnabledImes;
- if (userId == mSettings.getCurrentUserId()) {
+ if (userId == mSettings.getUserId()) {
hideCurrentInputLocked(mCurFocusedWindow, null /* statsToken */,
0 /* flags */, null /* resultReceiver */,
SoftInputShowHideReason.HIDE_RESET_SHELL_COMMAND);
mBindingController.unbindCurrentMethod();
// Enable default IMEs, disable others
- var toDisable = mSettings.getEnabledInputMethodListLocked();
+ var toDisable = mSettings.getEnabledInputMethodList();
var defaultEnabled = InputMethodInfoUtils.getDefaultEnabledImes(
- mContext, mMethodList);
+ mContext, mSettings.getMethodList());
toDisable.removeAll(defaultEnabled);
for (InputMethodInfo info : toDisable) {
setInputMethodEnabledLocked(info.getId(), false);
@@ -6553,23 +6531,19 @@
}
updateInputMethodsFromSettingsLocked(true /* enabledMayChange */);
InputMethodUtils.setNonSelectedSystemImesDisabledUntilUsed(
- getPackageManagerForUser(mContext, mSettings.getCurrentUserId()),
- mSettings.getEnabledInputMethodListLocked());
+ getPackageManagerForUser(mContext, mSettings.getUserId()),
+ mSettings.getEnabledInputMethodList());
nextIme = mSettings.getSelectedInputMethod();
- nextEnabledImes = mSettings.getEnabledInputMethodListLocked();
+ nextEnabledImes = mSettings.getEnabledInputMethodList();
} else {
- final ArrayMap<String, InputMethodInfo> methodMap = new ArrayMap<>();
- final ArrayList<InputMethodInfo> methodList = new ArrayList<>();
final ArrayMap<String, List<InputMethodSubtype>> additionalSubtypeMap =
new ArrayMap<>();
AdditionalSubtypeUtils.load(additionalSubtypeMap, userId);
- queryInputMethodServicesInternal(mContext, userId, additionalSubtypeMap,
- methodMap, methodList, DirectBootAwareness.AUTO);
- final InputMethodSettings settings = new InputMethodSettings(
- methodMap, userId);
+ final InputMethodSettings settings = queryInputMethodServicesInternal(
+ mContext, userId, additionalSubtypeMap, DirectBootAwareness.AUTO);
nextEnabledImes = InputMethodInfoUtils.getDefaultEnabledImes(mContext,
- methodList);
+ settings.getMethodList());
nextIme = InputMethodInfoUtils.getMostApplicableDefaultIME(
nextEnabledImes).getId();
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodMap.java b/services/core/java/com/android/server/inputmethod/InputMethodMap.java
new file mode 100644
index 0000000..a8e5e2e
--- /dev/null
+++ b/services/core/java/com/android/server/inputmethod/InputMethodMap.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.inputmethod;
+
+import android.annotation.AnyThread;
+import android.annotation.IntRange;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.util.ArrayMap;
+import android.view.inputmethod.InputMethodInfo;
+
+import java.util.List;
+
+/**
+ * A map from IME ID to {@link InputMethodInfo}, which is guaranteed to be immutable thus
+ * thread-safe.
+ */
+final class InputMethodMap {
+ private static final ArrayMap<String, InputMethodInfo> EMPTY_MAP =
+ new ArrayMap<>();
+
+ private final ArrayMap<String, InputMethodInfo> mMap;
+
+ static InputMethodMap emptyMap() {
+ return new InputMethodMap(EMPTY_MAP);
+ }
+
+ static InputMethodMap of(@NonNull ArrayMap<String, InputMethodInfo> map) {
+ return new InputMethodMap(map);
+ }
+
+ private InputMethodMap(@NonNull ArrayMap<String, InputMethodInfo> map) {
+ mMap = map.isEmpty() ? EMPTY_MAP : new ArrayMap<>(map);
+ }
+
+ @AnyThread
+ @Nullable
+ InputMethodInfo get(@Nullable String imeId) {
+ return mMap.get(imeId);
+ }
+
+ @AnyThread
+ @NonNull
+ List<InputMethodInfo> values() {
+ return List.copyOf(mMap.values());
+ }
+
+ @AnyThread
+ @Nullable
+ InputMethodInfo valueAt(int index) {
+ return mMap.valueAt(index);
+ }
+
+ @AnyThread
+ boolean containsKey(@Nullable String imeId) {
+ return mMap.containsKey(imeId);
+ }
+
+ @AnyThread
+ @IntRange(from = 0)
+ int size() {
+ return mMap.size();
+ }
+}
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodSettings.java b/services/core/java/com/android/server/inputmethod/InputMethodSettings.java
index 9ddb428..a51002b 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodSettings.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodSettings.java
@@ -16,6 +16,7 @@
package com.android.server.inputmethod;
+import android.annotation.AnyThread;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.UserIdInt;
@@ -58,9 +59,11 @@
private static final char INPUT_METHOD_SUBTYPE_SEPARATOR =
InputMethodUtils.INPUT_METHOD_SUBTYPE_SEPARATOR;
- private final ArrayMap<String, InputMethodInfo> mMethodMap;
+ private final InputMethodMap mMethodMap;
+ private final List<InputMethodInfo> mMethodList;
+
@UserIdInt
- private final int mCurrentUserId;
+ private final int mUserId;
private static void buildEnabledInputMethodsSettingString(
StringBuilder builder, Pair<String, ArrayList<String>> ime) {
@@ -73,9 +76,18 @@
}
}
- InputMethodSettings(ArrayMap<String, InputMethodInfo> methodMap, @UserIdInt int userId) {
+ static InputMethodSettings createEmptyMap(@UserIdInt int userId) {
+ return new InputMethodSettings(InputMethodMap.emptyMap(), userId);
+ }
+
+ static InputMethodSettings create(InputMethodMap methodMap, @UserIdInt int userId) {
+ return new InputMethodSettings(methodMap, userId);
+ }
+
+ private InputMethodSettings(InputMethodMap methodMap, @UserIdInt int userId) {
mMethodMap = methodMap;
- mCurrentUserId = userId;
+ mMethodList = methodMap.values();
+ mUserId = userId;
String ime = getSelectedInputMethod();
String defaultDeviceIme = getSelectedDefaultDeviceInputMethod();
if (defaultDeviceIme != null && !Objects.equals(ime, defaultDeviceIme)) {
@@ -84,49 +96,61 @@
}
}
+ @AnyThread
+ @NonNull
+ InputMethodMap getMethodMap() {
+ return mMethodMap;
+ }
+
+ @AnyThread
+ @NonNull
+ List<InputMethodInfo> getMethodList() {
+ return mMethodList;
+ }
+
private void putString(@NonNull String key, @Nullable String str) {
- SecureSettingsWrapper.putString(key, str, mCurrentUserId);
+ SecureSettingsWrapper.putString(key, str, mUserId);
}
@Nullable
private String getString(@NonNull String key, @Nullable String defaultValue) {
- return SecureSettingsWrapper.getString(key, defaultValue, mCurrentUserId);
+ return SecureSettingsWrapper.getString(key, defaultValue, mUserId);
}
private void putInt(String key, int value) {
- SecureSettingsWrapper.putInt(key, value, mCurrentUserId);
+ SecureSettingsWrapper.putInt(key, value, mUserId);
}
private int getInt(String key, int defaultValue) {
- return SecureSettingsWrapper.getInt(key, defaultValue, mCurrentUserId);
+ return SecureSettingsWrapper.getInt(key, defaultValue, mUserId);
}
- ArrayList<InputMethodInfo> getEnabledInputMethodListLocked() {
- return getEnabledInputMethodListWithFilterLocked(null /* matchingCondition */);
+ ArrayList<InputMethodInfo> getEnabledInputMethodList() {
+ return getEnabledInputMethodListWithFilter(null /* matchingCondition */);
}
@NonNull
- ArrayList<InputMethodInfo> getEnabledInputMethodListWithFilterLocked(
+ ArrayList<InputMethodInfo> getEnabledInputMethodListWithFilter(
@Nullable Predicate<InputMethodInfo> matchingCondition) {
- return createEnabledInputMethodListLocked(
- getEnabledInputMethodsAndSubtypeListLocked(), matchingCondition);
+ return createEnabledInputMethodList(
+ getEnabledInputMethodsAndSubtypeList(), matchingCondition);
}
- List<InputMethodSubtype> getEnabledInputMethodSubtypeListLocked(
+ List<InputMethodSubtype> getEnabledInputMethodSubtypeList(
InputMethodInfo imi, boolean allowsImplicitlyEnabledSubtypes) {
List<InputMethodSubtype> enabledSubtypes =
- getEnabledInputMethodSubtypeListLocked(imi);
+ getEnabledInputMethodSubtypeList(imi);
if (allowsImplicitlyEnabledSubtypes && enabledSubtypes.isEmpty()) {
- enabledSubtypes = SubtypeUtils.getImplicitlyApplicableSubtypesLocked(
- SystemLocaleWrapper.get(mCurrentUserId), imi);
+ enabledSubtypes = SubtypeUtils.getImplicitlyApplicableSubtypes(
+ SystemLocaleWrapper.get(mUserId), imi);
}
return InputMethodSubtype.sort(imi, enabledSubtypes);
}
- List<InputMethodSubtype> getEnabledInputMethodSubtypeListLocked(InputMethodInfo imi) {
- final List<Pair<String, ArrayList<String>>> imsList =
- getEnabledInputMethodsAndSubtypeListLocked();
- final List<InputMethodSubtype> enabledSubtypes = new ArrayList<>();
+ List<InputMethodSubtype> getEnabledInputMethodSubtypeList(InputMethodInfo imi) {
+ List<Pair<String, ArrayList<String>>> imsList =
+ getEnabledInputMethodsAndSubtypeList();
+ ArrayList<InputMethodSubtype> enabledSubtypes = new ArrayList<>();
if (imi != null) {
for (int i = 0; i < imsList.size(); ++i) {
final Pair<String, ArrayList<String>> imsPair = imsList.get(i);
@@ -149,7 +173,7 @@
return enabledSubtypes;
}
- List<Pair<String, ArrayList<String>>> getEnabledInputMethodsAndSubtypeListLocked() {
+ List<Pair<String, ArrayList<String>>> getEnabledInputMethodsAndSubtypeList() {
final String enabledInputMethodsStr = getEnabledInputMethodsStr();
final TextUtils.SimpleStringSplitter inputMethodSplitter =
new TextUtils.SimpleStringSplitter(INPUT_METHOD_SEPARATOR);
@@ -181,7 +205,7 @@
*
* @return the specified id was removed or not.
*/
- boolean buildAndPutEnabledInputMethodsStrRemovingIdLocked(
+ boolean buildAndPutEnabledInputMethodsStrRemovingId(
StringBuilder builder, List<Pair<String, ArrayList<String>>> imsList, String id) {
boolean isRemoved = false;
boolean needsAppendSeparator = false;
@@ -209,7 +233,7 @@
return isRemoved;
}
- private ArrayList<InputMethodInfo> createEnabledInputMethodListLocked(
+ private ArrayList<InputMethodInfo> createEnabledInputMethodList(
List<Pair<String, ArrayList<String>>> imsList,
Predicate<InputMethodInfo> matchingCondition) {
final ArrayList<InputMethodInfo> res = new ArrayList<>();
@@ -271,7 +295,7 @@
}
private void addSubtypeToHistory(String imeId, String subtypeId) {
- final List<Pair<String, String>> subtypeHistory = loadInputMethodAndSubtypeHistoryLocked();
+ final List<Pair<String, String>> subtypeHistory = loadInputMethodAndSubtypeHistory();
for (int i = 0; i < subtypeHistory.size(); ++i) {
final Pair<String, String> ime = subtypeHistory.get(i);
if (ime.first.equals(imeId)) {
@@ -303,14 +327,14 @@
}
}
- Pair<String, String> getLastInputMethodAndSubtypeLocked() {
+ Pair<String, String> getLastInputMethodAndSubtype() {
// Gets the first one from the history
- return getLastSubtypeForInputMethodLockedInternal(null);
+ return getLastSubtypeForInputMethodInternal(null);
}
@Nullable
- InputMethodSubtype getLastInputMethodSubtypeLocked() {
- final Pair<String, String> lastIme = getLastInputMethodAndSubtypeLocked();
+ InputMethodSubtype getLastInputMethodSubtype() {
+ final Pair<String, String> lastIme = getLastInputMethodAndSubtype();
// TODO: Handle the case of the last IME with no subtypes
if (lastIme == null || TextUtils.isEmpty(lastIme.first)
|| TextUtils.isEmpty(lastIme.second)) {
@@ -331,8 +355,8 @@
}
}
- String getLastSubtypeForInputMethodLocked(String imeId) {
- Pair<String, String> ime = getLastSubtypeForInputMethodLockedInternal(imeId);
+ String getLastSubtypeForInputMethod(String imeId) {
+ Pair<String, String> ime = getLastSubtypeForInputMethodInternal(imeId);
if (ime != null) {
return ime.second;
} else {
@@ -340,10 +364,10 @@
}
}
- private Pair<String, String> getLastSubtypeForInputMethodLockedInternal(String imeId) {
+ private Pair<String, String> getLastSubtypeForInputMethodInternal(String imeId) {
final List<Pair<String, ArrayList<String>>> enabledImes =
- getEnabledInputMethodsAndSubtypeListLocked();
- final List<Pair<String, String>> subtypeHistory = loadInputMethodAndSubtypeHistoryLocked();
+ getEnabledInputMethodsAndSubtypeList();
+ final List<Pair<String, String>> subtypeHistory = loadInputMethodAndSubtypeHistory();
for (int i = 0; i < subtypeHistory.size(); ++i) {
final Pair<String, String> imeAndSubtype = subtypeHistory.get(i);
final String imeInTheHistory = imeAndSubtype.first;
@@ -351,7 +375,7 @@
if (TextUtils.isEmpty(imeId) || imeInTheHistory.equals(imeId)) {
final String subtypeInTheHistory = imeAndSubtype.second;
final String subtypeHashCode =
- getEnabledSubtypeHashCodeForInputMethodAndSubtypeLocked(
+ getEnabledSubtypeHashCodeForInputMethodAndSubtype(
enabledImes, imeInTheHistory, subtypeInTheHistory);
if (!TextUtils.isEmpty(subtypeHashCode)) {
if (DEBUG) {
@@ -368,9 +392,9 @@
return null;
}
- private String getEnabledSubtypeHashCodeForInputMethodAndSubtypeLocked(List<Pair<String,
+ private String getEnabledSubtypeHashCodeForInputMethodAndSubtype(List<Pair<String,
ArrayList<String>>> enabledImes, String imeId, String subtypeHashCode) {
- final LocaleList localeList = SystemLocaleWrapper.get(mCurrentUserId);
+ final LocaleList localeList = SystemLocaleWrapper.get(mUserId);
for (int i = 0; i < enabledImes.size(); ++i) {
final Pair<String, ArrayList<String>> enabledIme = enabledImes.get(i);
if (enabledIme.first.equals(imeId)) {
@@ -383,7 +407,7 @@
// are enabled implicitly, so needs to treat them to be enabled.
if (imi != null && imi.getSubtypeCount() > 0) {
List<InputMethodSubtype> implicitlyEnabledSubtypes =
- SubtypeUtils.getImplicitlyApplicableSubtypesLocked(localeList,
+ SubtypeUtils.getImplicitlyApplicableSubtypes(localeList,
imi);
final int numSubtypes = implicitlyEnabledSubtypes.size();
for (int j = 0; j < numSubtypes; ++j) {
@@ -420,7 +444,7 @@
return null;
}
- private List<Pair<String, String>> loadInputMethodAndSubtypeHistoryLocked() {
+ private List<Pair<String, String>> loadInputMethodAndSubtypeHistory() {
ArrayList<Pair<String, String>> imsList = new ArrayList<>();
final String subtypeHistoryStr = getSubtypeHistoryStr();
if (TextUtils.isEmpty(subtypeHistoryStr)) {
@@ -459,16 +483,14 @@
void putSelectedInputMethod(String imeId) {
if (DEBUG) {
- Slog.d(TAG, "putSelectedInputMethodStr: " + imeId + ", "
- + mCurrentUserId);
+ Slog.d(TAG, "putSelectedInputMethodStr: " + imeId + ", " + mUserId);
}
putString(Settings.Secure.DEFAULT_INPUT_METHOD, imeId);
}
void putSelectedSubtype(int subtypeId) {
if (DEBUG) {
- Slog.d(TAG, "putSelectedInputMethodSubtypeStr: " + subtypeId + ", "
- + mCurrentUserId);
+ Slog.d(TAG, "putSelectedInputMethodSubtypeStr: " + subtypeId + ", " + mUserId);
}
putInt(Settings.Secure.SELECTED_INPUT_METHOD_SUBTYPE, subtypeId);
}
@@ -486,24 +508,21 @@
String getSelectedDefaultDeviceInputMethod() {
final String imi = getString(Settings.Secure.DEFAULT_DEVICE_INPUT_METHOD, null);
if (DEBUG) {
- Slog.d(TAG, "getSelectedDefaultDeviceInputMethodStr: " + imi + ", "
- + mCurrentUserId);
+ Slog.d(TAG, "getSelectedDefaultDeviceInputMethodStr: " + imi + ", " + mUserId);
}
return imi;
}
void putSelectedDefaultDeviceInputMethod(String imeId) {
if (DEBUG) {
- Slog.d(TAG, "putSelectedDefaultDeviceInputMethodStr: " + imeId + ", "
- + mCurrentUserId);
+ Slog.d(TAG, "putSelectedDefaultDeviceInputMethodStr: " + imeId + ", " + mUserId);
}
putString(Settings.Secure.DEFAULT_DEVICE_INPUT_METHOD, imeId);
}
void putDefaultVoiceInputMethod(String imeId) {
if (DEBUG) {
- Slog.d(TAG,
- "putDefaultVoiceInputMethodStr: " + imeId + ", " + mCurrentUserId);
+ Slog.d(TAG, "putDefaultVoiceInputMethodStr: " + imeId + ", " + mUserId);
}
putString(Settings.Secure.DEFAULT_VOICE_INPUT_METHOD, imeId);
}
@@ -527,8 +546,8 @@
}
@UserIdInt
- public int getCurrentUserId() {
- return mCurrentUserId;
+ public int getUserId() {
+ return mUserId;
}
int getSelectedInputMethodSubtypeId(String selectedImiId) {
@@ -583,7 +602,7 @@
// If there are no selected subtypes, the framework will try to find the most applicable
// subtype from explicitly or implicitly enabled subtypes.
final List<InputMethodSubtype> explicitlyOrImplicitlyEnabledSubtypes =
- getEnabledInputMethodSubtypeListLocked(imi, true);
+ getEnabledInputMethodSubtypeList(imi, true);
// If there is only one explicitly or implicitly enabled subtype, just returns it.
if (explicitlyOrImplicitlyEnabledSubtypes.isEmpty()) {
return null;
@@ -591,14 +610,14 @@
if (explicitlyOrImplicitlyEnabledSubtypes.size() == 1) {
return explicitlyOrImplicitlyEnabledSubtypes.get(0);
}
- final String locale = SystemLocaleWrapper.get(mCurrentUserId).get(0).toString();
- final InputMethodSubtype subtype = SubtypeUtils.findLastResortApplicableSubtypeLocked(
+ final String locale = SystemLocaleWrapper.get(mUserId).get(0).toString();
+ final InputMethodSubtype subtype = SubtypeUtils.findLastResortApplicableSubtype(
explicitlyOrImplicitlyEnabledSubtypes, SubtypeUtils.SUBTYPE_MODE_KEYBOARD,
locale, true);
if (subtype != null) {
return subtype;
}
- return SubtypeUtils.findLastResortApplicableSubtypeLocked(
+ return SubtypeUtils.findLastResortApplicableSubtype(
explicitlyOrImplicitlyEnabledSubtypes, null, locale, true);
}
@@ -620,7 +639,7 @@
} else {
additionalSubtypeMap.put(imi.getId(), subtypes);
}
- AdditionalSubtypeUtils.save(additionalSubtypeMap, mMethodMap, getCurrentUserId());
+ AdditionalSubtypeUtils.save(additionalSubtypeMap, mMethodMap, getUserId());
return true;
}
@@ -690,7 +709,7 @@
return sb.toString();
}
- void dumpLocked(final Printer pw, final String prefix) {
- pw.println(prefix + "mCurrentUserId=" + mCurrentUserId);
+ void dump(final Printer pw, final String prefix) {
+ pw.println(prefix + "mUserId=" + mUserId);
}
}
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodSubtypeSwitchingController.java b/services/core/java/com/android/server/inputmethod/InputMethodSubtypeSwitchingController.java
index 834ba20..1379d16 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodSubtypeSwitchingController.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodSubtypeSwitchingController.java
@@ -23,7 +23,6 @@
import android.content.Context;
import android.os.UserHandle;
import android.text.TextUtils;
-import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.Printer;
import android.util.Slog;
@@ -158,15 +157,15 @@
static List<ImeSubtypeListItem> getSortedInputMethodAndSubtypeList(
boolean includeAuxiliarySubtypes, boolean isScreenLocked, boolean forImeMenu,
- @NonNull Context context, @NonNull ArrayMap<String, InputMethodInfo> methodMap,
+ @NonNull Context context, @NonNull InputMethodMap methodMap,
@UserIdInt int userId) {
final Context userAwareContext = context.getUserId() == userId
? context
: context.createContextAsUser(UserHandle.of(userId), 0 /* flags */);
final String mSystemLocaleStr = SystemLocaleWrapper.get(userId).get(0).toLanguageTag();
- final InputMethodSettings settings = new InputMethodSettings(methodMap, userId);
+ final InputMethodSettings settings = InputMethodSettings.create(methodMap, userId);
- final ArrayList<InputMethodInfo> imis = settings.getEnabledInputMethodListLocked();
+ final ArrayList<InputMethodInfo> imis = settings.getEnabledInputMethodList();
if (imis.isEmpty()) {
return new ArrayList<>();
}
@@ -184,7 +183,7 @@
continue;
}
final List<InputMethodSubtype> explicitlyOrImplicitlyEnabledSubtypeList =
- settings.getEnabledInputMethodSubtypeListLocked(imi, true);
+ settings.getEnabledInputMethodSubtypeList(imi, true);
final ArraySet<String> enabledSubtypeSet = new ArraySet<>();
for (InputMethodSubtype subtype : explicitlyOrImplicitlyEnabledSubtypeList) {
enabledSubtypeSet.add(String.valueOf(subtype.hashCode()));
@@ -479,7 +478,7 @@
private ControllerImpl mController;
private InputMethodSubtypeSwitchingController(@NonNull Context context,
- @NonNull ArrayMap<String, InputMethodInfo> methodMap, @UserIdInt int userId) {
+ @NonNull InputMethodMap methodMap, @UserIdInt int userId) {
mContext = context;
mUserId = userId;
mController = ControllerImpl.createFrom(null,
@@ -491,7 +490,7 @@
@NonNull
public static InputMethodSubtypeSwitchingController createInstanceLocked(
@NonNull Context context,
- @NonNull ArrayMap<String, InputMethodInfo> methodMap, @UserIdInt int userId) {
+ @NonNull InputMethodMap methodMap, @UserIdInt int userId) {
return new InputMethodSubtypeSwitchingController(context, methodMap, userId);
}
@@ -511,8 +510,7 @@
mController.onUserActionLocked(imi, subtype);
}
- public void resetCircularListLocked(
- @NonNull ArrayMap<String, InputMethodInfo> methodMap) {
+ public void resetCircularListLocked(@NonNull InputMethodMap methodMap) {
mController = ControllerImpl.createFrom(mController,
getSortedInputMethodAndSubtypeList(
false /* includeAuxiliarySubtypes */, false /* isScreenLocked */,
diff --git a/services/core/java/com/android/server/inputmethod/SubtypeUtils.java b/services/core/java/com/android/server/inputmethod/SubtypeUtils.java
index 95df998..3d5c8677 100644
--- a/services/core/java/com/android/server/inputmethod/SubtypeUtils.java
+++ b/services/core/java/com/android/server/inputmethod/SubtypeUtils.java
@@ -26,7 +26,6 @@
import android.view.inputmethod.InputMethodSubtype;
import com.android.internal.annotations.GuardedBy;
-import com.android.internal.annotations.VisibleForTesting;
import java.util.ArrayList;
import java.util.List;
@@ -52,7 +51,7 @@
"EnabledWhenDefaultIsNotAsciiCapable";
// A temporary workaround for the performance concerns in
- // #getImplicitlyApplicableSubtypesLocked(Resources, InputMethodInfo).
+ // #getImplicitlyApplicableSubtypes(Resources, InputMethodInfo).
// TODO: Optimize all the critical paths including this one.
// TODO(b/235661780): Make the cache supports multi-users.
private static final Object sCacheLock = new Object();
@@ -121,9 +120,8 @@
private static final LocaleUtils.LocaleExtractor<InputMethodSubtype> sSubtypeToLocale =
source -> source != null ? source.getLocaleObject() : null;
- @VisibleForTesting
@NonNull
- static ArrayList<InputMethodSubtype> getImplicitlyApplicableSubtypesLocked(
+ static ArrayList<InputMethodSubtype> getImplicitlyApplicableSubtypes(
@NonNull LocaleList systemLocales, InputMethodInfo imi) {
synchronized (sCacheLock) {
// We intentionally do not use InputMethodInfo#equals(InputMethodInfo) here because
@@ -133,11 +131,11 @@
}
}
- // Note: Only resource info in "res" is used in getImplicitlyApplicableSubtypesLockedImpl().
- // TODO: Refactor getImplicitlyApplicableSubtypesLockedImpl() so that it can receive
+ // Note: Only resource info in "res" is used in getImplicitlyApplicableSubtypesImpl().
+ // TODO: Refactor getImplicitlyApplicableSubtypesImpl() so that it can receive
// LocaleList rather than Resource.
final ArrayList<InputMethodSubtype> result =
- getImplicitlyApplicableSubtypesLockedImpl(systemLocales, imi);
+ getImplicitlyApplicableSubtypesImpl(systemLocales, imi);
synchronized (sCacheLock) {
// Both LocaleList and InputMethodInfo are immutable. No need to copy them here.
sCachedSystemLocales = systemLocales;
@@ -147,7 +145,7 @@
return result;
}
- private static ArrayList<InputMethodSubtype> getImplicitlyApplicableSubtypesLockedImpl(
+ private static ArrayList<InputMethodSubtype> getImplicitlyApplicableSubtypesImpl(
@NonNull LocaleList systemLocales, InputMethodInfo imi) {
final List<InputMethodSubtype> subtypes = getSubtypes(imi);
final String systemLocale = systemLocales.get(0).toString();
@@ -215,7 +213,7 @@
}
if (applicableSubtypes.isEmpty()) {
- InputMethodSubtype lastResortKeyboardSubtype = findLastResortApplicableSubtypeLocked(
+ InputMethodSubtype lastResortKeyboardSubtype = findLastResortApplicableSubtype(
subtypes, SUBTYPE_MODE_KEYBOARD, systemLocale, true);
if (lastResortKeyboardSubtype != null) {
applicableSubtypes.add(lastResortKeyboardSubtype);
@@ -244,7 +242,7 @@
*
* @return the most applicable subtypeId
*/
- static InputMethodSubtype findLastResortApplicableSubtypeLocked(
+ static InputMethodSubtype findLastResortApplicableSubtype(
List<InputMethodSubtype> subtypes, String mode, @NonNull String locale,
boolean canIgnoreLocaleAsLastResort) {
if (subtypes == null || subtypes.isEmpty()) {
diff --git a/services/core/java/com/android/server/location/gnss/GnssNetworkConnectivityHandler.java b/services/core/java/com/android/server/location/gnss/GnssNetworkConnectivityHandler.java
index 403b421..71a9f54 100644
--- a/services/core/java/com/android/server/location/gnss/GnssNetworkConnectivityHandler.java
+++ b/services/core/java/com/android/server/location/gnss/GnssNetworkConnectivityHandler.java
@@ -248,6 +248,7 @@
SubscriptionManager subManager = mContext.getSystemService(SubscriptionManager.class);
TelephonyManager telManager = mContext.getSystemService(TelephonyManager.class);
if (subManager != null && telManager != null) {
+ subManager = subManager.createForAllUserProfiles();
List<SubscriptionInfo> subscriptionInfoList =
subManager.getActiveSubscriptionInfoList();
HashSet<Integer> activeSubIds = new HashSet<Integer>();
diff --git a/services/core/java/com/android/server/locksettings/LockSettingsService.java b/services/core/java/com/android/server/locksettings/LockSettingsService.java
index 542b3b0..a06607b 100644
--- a/services/core/java/com/android/server/locksettings/LockSettingsService.java
+++ b/services/core/java/com/android/server/locksettings/LockSettingsService.java
@@ -16,6 +16,7 @@
package com.android.server.locksettings;
+import static android.security.Flags.reportPrimaryAuthAttempts;
import static android.Manifest.permission.ACCESS_KEYGUARD_SECURE_STORAGE;
import static android.Manifest.permission.MANAGE_BIOMETRIC;
import static android.Manifest.permission.SET_AND_VERIFY_LOCKSCREEN_CREDENTIALS;
@@ -92,6 +93,7 @@
import android.os.IBinder;
import android.os.IProgressListener;
import android.os.Process;
+import android.os.RemoteCallbackList;
import android.os.RemoteException;
import android.os.ResultReceiver;
import android.os.ServiceManager;
@@ -137,6 +139,7 @@
import com.android.internal.util.Preconditions;
import com.android.internal.widget.ICheckCredentialProgressCallback;
import com.android.internal.widget.ILockSettings;
+import com.android.internal.widget.ILockSettingsStateListener;
import com.android.internal.widget.IWeakEscrowTokenActivatedListener;
import com.android.internal.widget.IWeakEscrowTokenRemovedListener;
import com.android.internal.widget.LockPatternUtils;
@@ -329,6 +332,9 @@
private HashMap<UserHandle, UserManager> mUserManagerCache = new HashMap<>();
+ private final RemoteCallbackList<ILockSettingsStateListener> mLockSettingsStateListeners =
+ new RemoteCallbackList<>();
+
// This class manages life cycle events for encrypted users on File Based Encryption (FBE)
// devices. The most basic of these is to show/hide notifications about missing features until
// the user unlocks the account and credential-encrypted storage is available.
@@ -2364,9 +2370,37 @@
requireStrongAuth(STRONG_AUTH_REQUIRED_AFTER_LOCKOUT, userId);
}
}
+ if (reportPrimaryAuthAttempts()) {
+ final boolean success =
+ response.getResponseCode() == VerifyCredentialResponse.RESPONSE_OK;
+ notifyLockSettingsStateListeners(success, userId);
+ }
return response;
}
+ private void notifyLockSettingsStateListeners(boolean success, int userId) {
+ int i = mLockSettingsStateListeners.beginBroadcast();
+ try {
+ while (i > 0) {
+ i--;
+ try {
+ if (success) {
+ mLockSettingsStateListeners.getBroadcastItem(i)
+ .onAuthenticationSucceeded(userId);
+ } else {
+ mLockSettingsStateListeners.getBroadcastItem(i)
+ .onAuthenticationFailed(userId);
+ }
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Exception while notifying LockSettingsStateListener:"
+ + " success = " + success + ", userId = " + userId, e);
+ }
+ }
+ } finally {
+ mLockSettingsStateListeners.finishBroadcast();
+ }
+ }
+
@Override
public VerifyCredentialResponse verifyTiedProfileChallenge(LockscreenCredential credential,
int userId, @LockPatternUtils.VerifyFlag int flags) {
@@ -3684,6 +3718,18 @@
public void refreshStrongAuthTimeout(int userId) {
mStrongAuth.refreshStrongAuthTimeout(userId);
}
+
+ @Override
+ public void registerLockSettingsStateListener(
+ @NonNull ILockSettingsStateListener listener) {
+ mLockSettingsStateListeners.register(listener);
+ }
+
+ @Override
+ public void unregisterLockSettingsStateListener(
+ @NonNull ILockSettingsStateListener listener) {
+ mLockSettingsStateListeners.unregister(listener);
+ }
}
private class RebootEscrowCallbacks implements RebootEscrowManager.Callbacks {
diff --git a/services/core/java/com/android/server/media/MediaSessionService.java b/services/core/java/com/android/server/media/MediaSessionService.java
index 2cd3ab1..1d516e2 100644
--- a/services/core/java/com/android/server/media/MediaSessionService.java
+++ b/services/core/java/com/android/server/media/MediaSessionService.java
@@ -287,10 +287,14 @@
}
user.mPriorityStack.onSessionActiveStateChanged(record);
}
- setForegroundServiceAllowance(
- record,
- /* allowRunningInForeground= */ record.isActive()
- && (playbackState == null || playbackState.isActive()));
+ boolean allowRunningInForeground = record.isActive()
+ && (playbackState == null || playbackState.isActive());
+
+ Log.d(TAG, "onSessionActiveStateChanged: "
+ + "record=" + record
+ + "playbackState=" + playbackState
+ + "allowRunningInForeground=" + allowRunningInForeground);
+ setForegroundServiceAllowance(record, allowRunningInForeground);
mHandler.postSessionsChanged(record);
}
}
@@ -388,10 +392,12 @@
}
user.mPriorityStack.onPlaybackStateChanged(record, shouldUpdatePriority);
if (playbackState != null) {
- setForegroundServiceAllowance(
- record,
- /* allowRunningInForeground= */ playbackState.isActive()
- && record.isActive());
+ boolean allowRunningInForeground = playbackState.isActive() && record.isActive();
+ Log.d(TAG, "onSessionPlaybackStateChanged: "
+ + "record=" + record
+ + "playbackState=" + playbackState
+ + "allowRunningInForeground=" + allowRunningInForeground);
+ setForegroundServiceAllowance(record, allowRunningInForeground);
}
}
}
@@ -556,6 +562,8 @@
}
session.close();
+
+ Log.d(TAG, "destroySessionLocked: record=" + session);
setForegroundServiceAllowance(session, /* allowRunningInForeground= */ false);
mHandler.postSessionsChanged(session);
}
diff --git a/services/core/java/com/android/server/media/metrics/MediaMetricsManagerService.java b/services/core/java/com/android/server/media/metrics/MediaMetricsManagerService.java
index df612e6..bbe6d3a 100644
--- a/services/core/java/com/android/server/media/metrics/MediaMetricsManagerService.java
+++ b/services/core/java/com/android/server/media/metrics/MediaMetricsManagerService.java
@@ -20,6 +20,7 @@
import android.content.pm.PackageManager;
import android.media.MediaMetrics;
import android.media.metrics.BundleSession;
+import android.media.metrics.EditingEndedEvent;
import android.media.metrics.IMediaMetricsManager;
import android.media.metrics.NetworkEvent;
import android.media.metrics.PlaybackErrorEvent;
@@ -346,6 +347,24 @@
StatsLog.write(statsEvent);
}
+ @Override
+ public void reportEditingEndedEvent(String sessionId, EditingEndedEvent event, int userId) {
+ int level = loggingLevel();
+ if (level == LOGGING_LEVEL_BLOCKED) {
+ return;
+ }
+ StatsEvent statsEvent =
+ StatsEvent.newBuilder()
+ .setAtomId(798)
+ .writeString(sessionId)
+ .writeInt(event.getFinalState())
+ .writeInt(event.getErrorCode())
+ .writeLong(event.getTimeSinceCreatedMillis())
+ .usePooledBuffer()
+ .build();
+ StatsLog.write(statsEvent);
+ }
+
private int loggingLevel() {
synchronized (mLock) {
int uid = Binder.getCallingUid();
diff --git a/services/core/java/com/android/server/net/Android.bp b/services/core/java/com/android/server/net/Android.bp
new file mode 100644
index 0000000..71d8e6b
--- /dev/null
+++ b/services/core/java/com/android/server/net/Android.bp
@@ -0,0 +1,10 @@
+aconfig_declarations {
+ name: "net_flags",
+ package: "com.android.server.net",
+ srcs: ["*.aconfig"],
+}
+
+java_aconfig_library {
+ name: "net_flags_lib",
+ aconfig_declarations: "net_flags",
+}
diff --git a/services/core/java/com/android/server/net/NetworkManagementService.java b/services/core/java/com/android/server/net/NetworkManagementService.java
index 681d1a0..d25f529 100644
--- a/services/core/java/com/android/server/net/NetworkManagementService.java
+++ b/services/core/java/com/android/server/net/NetworkManagementService.java
@@ -17,6 +17,7 @@
package com.android.server.net;
import static android.Manifest.permission.CONNECTIVITY_INTERNAL;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_BACKGROUND;
import static android.net.ConnectivityManager.FIREWALL_CHAIN_DOZABLE;
import static android.net.ConnectivityManager.FIREWALL_CHAIN_LOW_POWER_STANDBY;
import static android.net.ConnectivityManager.FIREWALL_CHAIN_POWERSAVE;
@@ -27,6 +28,7 @@
import static android.net.INetd.FIREWALL_DENYLIST;
import static android.net.INetd.FIREWALL_RULE_ALLOW;
import static android.net.INetd.FIREWALL_RULE_DENY;
+import static android.net.NetworkPolicyManager.FIREWALL_CHAIN_NAME_BACKGROUND;
import static android.net.NetworkPolicyManager.FIREWALL_CHAIN_NAME_DOZABLE;
import static android.net.NetworkPolicyManager.FIREWALL_CHAIN_NAME_LOW_POWER_STANDBY;
import static android.net.NetworkPolicyManager.FIREWALL_CHAIN_NAME_POWERSAVE;
@@ -187,6 +189,13 @@
*/
@GuardedBy("mRulesLock")
private final SparseIntArray mUidFirewallLowPowerStandbyRules = new SparseIntArray();
+
+ /**
+ * Contains the per-UID firewall rules that are used when Background chain is enabled.
+ */
+ @GuardedBy("mRulesLock")
+ private final SparseIntArray mUidFirewallBackgroundRules = new SparseIntArray();
+
/** Set of states for the child firewall chains. True if the chain is active. */
@GuardedBy("mRulesLock")
final SparseBooleanArray mFirewallChainStates = new SparseBooleanArray();
@@ -449,13 +458,15 @@
syncFirewallChainLocked(FIREWALL_CHAIN_POWERSAVE, "powersave ");
syncFirewallChainLocked(FIREWALL_CHAIN_RESTRICTED, "restricted ");
syncFirewallChainLocked(FIREWALL_CHAIN_LOW_POWER_STANDBY, "low power standby ");
+ syncFirewallChainLocked(FIREWALL_CHAIN_BACKGROUND, FIREWALL_CHAIN_NAME_BACKGROUND);
final int[] chains = {
FIREWALL_CHAIN_STANDBY,
FIREWALL_CHAIN_DOZABLE,
FIREWALL_CHAIN_POWERSAVE,
FIREWALL_CHAIN_RESTRICTED,
- FIREWALL_CHAIN_LOW_POWER_STANDBY
+ FIREWALL_CHAIN_LOW_POWER_STANDBY,
+ FIREWALL_CHAIN_BACKGROUND,
};
for (int chain : chains) {
@@ -1206,6 +1217,8 @@
return FIREWALL_CHAIN_NAME_RESTRICTED;
case FIREWALL_CHAIN_LOW_POWER_STANDBY:
return FIREWALL_CHAIN_NAME_LOW_POWER_STANDBY;
+ case FIREWALL_CHAIN_BACKGROUND:
+ return FIREWALL_CHAIN_NAME_BACKGROUND;
default:
throw new IllegalArgumentException("Bad child chain: " + chain);
}
@@ -1223,6 +1236,8 @@
return FIREWALL_ALLOWLIST;
case FIREWALL_CHAIN_LOW_POWER_STANDBY:
return FIREWALL_ALLOWLIST;
+ case FIREWALL_CHAIN_BACKGROUND:
+ return FIREWALL_ALLOWLIST;
default:
return isFirewallEnabled() ? FIREWALL_ALLOWLIST : FIREWALL_DENYLIST;
}
@@ -1343,6 +1358,8 @@
return mUidFirewallRestrictedRules;
case FIREWALL_CHAIN_LOW_POWER_STANDBY:
return mUidFirewallLowPowerStandbyRules;
+ case FIREWALL_CHAIN_BACKGROUND:
+ return mUidFirewallBackgroundRules;
case FIREWALL_CHAIN_NONE:
return mUidFirewallRules;
default:
@@ -1395,6 +1412,10 @@
pw.println(getFirewallChainState(FIREWALL_CHAIN_LOW_POWER_STANDBY));
dumpUidFirewallRule(pw, FIREWALL_CHAIN_NAME_LOW_POWER_STANDBY,
mUidFirewallLowPowerStandbyRules);
+
+ pw.print("UID firewall background chain enabled: ");
+ pw.println(getFirewallChainState(FIREWALL_CHAIN_BACKGROUND));
+ dumpUidFirewallRule(pw, FIREWALL_CHAIN_NAME_BACKGROUND, mUidFirewallBackgroundRules);
}
pw.print("Firewall enabled: "); pw.println(mFirewallEnabled);
@@ -1494,6 +1515,11 @@
if (DBG) Slog.d(TAG, "Uid " + uid + " restricted because of low power standby");
return true;
}
+ if (getFirewallChainState(FIREWALL_CHAIN_BACKGROUND)
+ && mUidFirewallBackgroundRules.get(uid) != FIREWALL_RULE_ALLOW) {
+ if (DBG) Slog.d(TAG, "Uid " + uid + " restricted because it is in background");
+ return true;
+ }
if (mUidRejectOnMetered.get(uid)) {
if (DBG) Slog.d(TAG, "Uid " + uid + " restricted because of no metered data"
+ " in the background");
diff --git a/services/core/java/com/android/server/net/NetworkPolicyLogger.java b/services/core/java/com/android/server/net/NetworkPolicyLogger.java
index d7188c7..8e2d778 100644
--- a/services/core/java/com/android/server/net/NetworkPolicyLogger.java
+++ b/services/core/java/com/android/server/net/NetworkPolicyLogger.java
@@ -16,6 +16,7 @@
package com.android.server.net;
import static android.net.ConnectivityManager.BLOCKED_REASON_NONE;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_BACKGROUND;
import static android.net.ConnectivityManager.FIREWALL_CHAIN_DOZABLE;
import static android.net.ConnectivityManager.FIREWALL_CHAIN_LOW_POWER_STANDBY;
import static android.net.ConnectivityManager.FIREWALL_CHAIN_POWERSAVE;
@@ -24,6 +25,7 @@
import static android.net.INetd.FIREWALL_RULE_ALLOW;
import static android.net.INetd.FIREWALL_RULE_DENY;
import static android.net.NetworkPolicyManager.ALLOWED_REASON_NONE;
+import static android.net.NetworkPolicyManager.FIREWALL_CHAIN_NAME_BACKGROUND;
import static android.net.NetworkPolicyManager.FIREWALL_CHAIN_NAME_DOZABLE;
import static android.net.NetworkPolicyManager.FIREWALL_CHAIN_NAME_LOW_POWER_STANDBY;
import static android.net.NetworkPolicyManager.FIREWALL_CHAIN_NAME_POWERSAVE;
@@ -389,6 +391,8 @@
return FIREWALL_CHAIN_NAME_RESTRICTED;
case FIREWALL_CHAIN_LOW_POWER_STANDBY:
return FIREWALL_CHAIN_NAME_LOW_POWER_STANDBY;
+ case FIREWALL_CHAIN_BACKGROUND:
+ return FIREWALL_CHAIN_NAME_BACKGROUND;
default:
return String.valueOf(chain);
}
diff --git a/services/core/java/com/android/server/net/NetworkPolicyManagerService.java b/services/core/java/com/android/server/net/NetworkPolicyManagerService.java
index e4e48bd..f9ffb1c 100644
--- a/services/core/java/com/android/server/net/NetworkPolicyManagerService.java
+++ b/services/core/java/com/android/server/net/NetworkPolicyManagerService.java
@@ -47,6 +47,7 @@
import static android.net.ConnectivityManager.BLOCKED_METERED_REASON_DATA_SAVER;
import static android.net.ConnectivityManager.BLOCKED_METERED_REASON_MASK;
import static android.net.ConnectivityManager.BLOCKED_METERED_REASON_USER_RESTRICTED;
+import static android.net.ConnectivityManager.BLOCKED_REASON_APP_BACKGROUND;
import static android.net.ConnectivityManager.BLOCKED_REASON_APP_STANDBY;
import static android.net.ConnectivityManager.BLOCKED_REASON_BATTERY_SAVER;
import static android.net.ConnectivityManager.BLOCKED_REASON_DOZE;
@@ -54,6 +55,7 @@
import static android.net.ConnectivityManager.BLOCKED_REASON_NONE;
import static android.net.ConnectivityManager.BLOCKED_REASON_RESTRICTED_MODE;
import static android.net.ConnectivityManager.CONNECTIVITY_ACTION;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_BACKGROUND;
import static android.net.ConnectivityManager.FIREWALL_CHAIN_DOZABLE;
import static android.net.ConnectivityManager.FIREWALL_CHAIN_LOW_POWER_STANDBY;
import static android.net.ConnectivityManager.FIREWALL_CHAIN_POWERSAVE;
@@ -77,6 +79,7 @@
import static android.net.NetworkPolicyManager.ALLOWED_REASON_FOREGROUND;
import static android.net.NetworkPolicyManager.ALLOWED_REASON_LOW_POWER_STANDBY_ALLOWLIST;
import static android.net.NetworkPolicyManager.ALLOWED_REASON_NONE;
+import static android.net.NetworkPolicyManager.ALLOWED_REASON_NOT_IN_BACKGROUND;
import static android.net.NetworkPolicyManager.ALLOWED_REASON_POWER_SAVE_ALLOWLIST;
import static android.net.NetworkPolicyManager.ALLOWED_REASON_POWER_SAVE_EXCEPT_IDLE_ALLOWLIST;
import static android.net.NetworkPolicyManager.ALLOWED_REASON_RESTRICTED_MODE_PERMISSIONS;
@@ -96,6 +99,7 @@
import static android.net.NetworkPolicyManager.SUBSCRIPTION_OVERRIDE_UNMETERED;
import static android.net.NetworkPolicyManager.allowedReasonsToString;
import static android.net.NetworkPolicyManager.blockedReasonsToString;
+import static android.net.NetworkPolicyManager.isProcStateAllowedNetworkWhileBackground;
import static android.net.NetworkPolicyManager.isProcStateAllowedWhileIdleOrPowerSaveMode;
import static android.net.NetworkPolicyManager.isProcStateAllowedWhileInLowPowerStandby;
import static android.net.NetworkPolicyManager.isProcStateAllowedWhileOnRestrictBackground;
@@ -202,12 +206,12 @@
import android.os.MessageQueue.IdleHandler;
import android.os.ParcelFileDescriptor;
import android.os.PersistableBundle;
+import android.os.PowerExemptionManager;
import android.os.PowerExemptionManager.ReasonCode;
import android.os.PowerManager;
import android.os.PowerManager.ServiceType;
import android.os.PowerManagerInternal;
import android.os.PowerSaveState;
-import android.os.PowerWhitelistManager;
import android.os.Process;
import android.os.RemoteCallbackList;
import android.os.RemoteException;
@@ -243,6 +247,7 @@
import android.util.SparseIntArray;
import android.util.SparseLongArray;
import android.util.SparseSetArray;
+import android.util.TimeUtils;
import android.util.Xml;
import com.android.internal.R;
@@ -458,6 +463,12 @@
*/
private static final int MSG_UIDS_BLOCKED_REASONS_CHANGED = 23;
+ /**
+ * Message to update background restriction rules for uids that should lose network access
+ * due to being in the background.
+ */
+ private static final int MSG_PROCESS_BACKGROUND_TRANSITIONING_UIDS = 24;
+
private static final int UID_MSG_STATE_CHANGED = 100;
private static final int UID_MSG_GONE = 101;
@@ -476,7 +487,7 @@
private ConnectivityManager mConnManager;
private PowerManagerInternal mPowerManagerInternal;
- private PowerWhitelistManager mPowerWhitelistManager;
+ private PowerExemptionManager mPowerExemptionManager;
@NonNull
private final Dependencies mDeps;
@@ -491,6 +502,12 @@
// Denotes the status of restrict background read from disk.
private boolean mLoadedRestrictBackground;
+ /**
+ * Whether or not network for apps in proc-states greater than
+ * {@link NetworkPolicyManager#BACKGROUND_THRESHOLD_STATE} is always blocked.
+ */
+ private boolean mBackgroundNetworkRestricted;
+
// See main javadoc for instructions on how to use these locks.
final Object mUidRulesFirstLock = new Object();
final Object mNetworkPoliciesSecondLock = new Object();
@@ -515,6 +532,15 @@
private volatile boolean mNetworkManagerReady;
+ /**
+ * Delay after which a uid going into a process state greater than or equal to
+ * {@link NetworkPolicyManager#BACKGROUND_THRESHOLD_STATE} will lose network access.
+ * The delay is meant to prevent churn due to quick process-state changes.
+ * Note that there is no delay while granting network access.
+ */
+ @VisibleForTesting
+ long mBackgroundRestrictionDelayMs = TimeUnit.SECONDS.toMillis(5);
+
/** Defined network policies. */
@GuardedBy("mNetworkPoliciesSecondLock")
final ArrayMap<NetworkTemplate, NetworkPolicy> mNetworkPolicy = new ArrayMap<>();
@@ -546,6 +572,8 @@
@GuardedBy("mUidRulesFirstLock")
final SparseIntArray mUidFirewallPowerSaveRules = new SparseIntArray();
@GuardedBy("mUidRulesFirstLock")
+ final SparseIntArray mUidFirewallBackgroundRules = new SparseIntArray();
+ @GuardedBy("mUidRulesFirstLock")
final SparseIntArray mUidFirewallRestrictedModeRules = new SparseIntArray();
@GuardedBy("mUidRulesFirstLock")
final SparseIntArray mUidFirewallLowPowerStandbyModeRules = new SparseIntArray();
@@ -625,6 +653,14 @@
@GuardedBy("mUidRulesFirstLock")
private final SparseArray<UidBlockedState> mTmpUidBlockedState = new SparseArray<>();
+ /**
+ * Stores a map of uids to the time their transition to background is considered complete. They
+ * will lose network access after this time. This is used to prevent churn in rules due to quick
+ * process-state transitions.
+ */
+ @GuardedBy("mUidRulesFirstLock")
+ private final SparseLongArray mBackgroundTransitioningUids = new SparseLongArray();
+
/** Map from network ID to last observed meteredness state */
@GuardedBy("mNetworkPoliciesSecondLock")
private final SparseBooleanArray mNetworkMetered = new SparseBooleanArray();
@@ -824,7 +860,7 @@
mContext = Objects.requireNonNull(context, "missing context");
mActivityManager = Objects.requireNonNull(activityManager, "missing activityManager");
mNetworkManager = Objects.requireNonNull(networkManagement, "missing networkManagement");
- mPowerWhitelistManager = mContext.getSystemService(PowerWhitelistManager.class);
+ mPowerExemptionManager = mContext.getSystemService(PowerExemptionManager.class);
mClock = Objects.requireNonNull(clock, "missing Clock");
mUserManager = (UserManager) mContext.getSystemService(Context.USER_SERVICE);
mCarrierConfigManager = mContext.getSystemService(CarrierConfigManager.class);
@@ -860,15 +896,15 @@
@GuardedBy("mUidRulesFirstLock")
private void updatePowerSaveAllowlistUL() {
- int[] whitelist = mPowerWhitelistManager.getWhitelistedAppIds(/* includingIdle */ false);
+ int[] allowlist = mPowerExemptionManager.getAllowListedAppIds(/* includingIdle */ false);
mPowerSaveWhitelistExceptIdleAppIds.clear();
- for (int uid : whitelist) {
+ for (int uid : allowlist) {
mPowerSaveWhitelistExceptIdleAppIds.put(uid, true);
}
- whitelist = mPowerWhitelistManager.getWhitelistedAppIds(/* includingIdle */ true);
+ allowlist = mPowerExemptionManager.getAllowListedAppIds(/* includingIdle */ true);
mPowerSaveWhitelistAppIds.clear();
- for (int uid : whitelist) {
+ for (int uid : allowlist) {
mPowerSaveWhitelistAppIds.put(uid, true);
}
}
@@ -1018,6 +1054,14 @@
writePolicyAL();
}
+ // The flag is boot-stable.
+ mBackgroundNetworkRestricted = Flags.networkBlockedForTopSleepingAndAbove();
+ if (mBackgroundNetworkRestricted) {
+ // Firewall rules and UidBlockedState will get updated in
+ // updateRulesForGlobalChangeAL below.
+ enableFirewallChainUL(FIREWALL_CHAIN_BACKGROUND, true);
+ }
+
setRestrictBackgroundUL(mLoadedRestrictBackground, "init_service");
updateRulesForGlobalChangeAL(false);
updateNotificationsNL();
@@ -1028,17 +1072,22 @@
final int changes = ActivityManager.UID_OBSERVER_PROCSTATE
| ActivityManager.UID_OBSERVER_GONE
| ActivityManager.UID_OBSERVER_CAPABILITY;
+
+ final int cutpoint = mBackgroundNetworkRestricted ? PROCESS_STATE_UNKNOWN
+ : NetworkPolicyManager.FOREGROUND_THRESHOLD_STATE;
+ // TODO (b/319728914): Filter out the unnecessary changes when using no cutpoint.
+
mActivityManagerInternal.registerNetworkPolicyUidObserver(mUidObserver, changes,
- NetworkPolicyManager.FOREGROUND_THRESHOLD_STATE, "android");
+ cutpoint, "android");
mNetworkManager.registerObserver(mAlertObserver);
} catch (RemoteException e) {
// ignored; both services live in system_server
}
// listen for changes to power save allowlist
- final IntentFilter whitelistFilter = new IntentFilter(
+ final IntentFilter allowlistFilter = new IntentFilter(
PowerManager.ACTION_POWER_SAVE_WHITELIST_CHANGED);
- mContext.registerReceiver(mPowerSaveWhitelistReceiver, whitelistFilter, null, mHandler);
+ mContext.registerReceiver(mPowerSaveAllowlistReceiver, allowlistFilter, null, mHandler);
// watch for network interfaces to be claimed
final IntentFilter connFilter = new IntentFilter(CONNECTIVITY_ACTION);
@@ -1189,12 +1238,15 @@
}
}
- final private BroadcastReceiver mPowerSaveWhitelistReceiver = new BroadcastReceiver() {
+ private final BroadcastReceiver mPowerSaveAllowlistReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
// on background handler thread, and POWER_SAVE_WHITELIST_CHANGED is protected
synchronized (mUidRulesFirstLock) {
updatePowerSaveAllowlistUL();
+ if (mBackgroundNetworkRestricted) {
+ updateRulesForBackgroundChainUL();
+ }
updateRulesForRestrictPowerUL();
updateRulesForAppIdleUL();
}
@@ -3914,6 +3966,11 @@
}
fout.println();
+ fout.println("Flags:");
+ fout.println("Network blocked for TOP_SLEEPING and above: "
+ + mBackgroundNetworkRestricted);
+
+ fout.println();
fout.println("mRestrictBackgroundLowPowerMode: " + mRestrictBackgroundLowPowerMode);
fout.println("mRestrictBackgroundBeforeBsm: " + mRestrictBackgroundBeforeBsm);
fout.println("mLoadedRestrictBackground: " + mLoadedRestrictBackground);
@@ -4055,6 +4112,22 @@
fout.decreaseIndent();
}
+ size = mBackgroundTransitioningUids.size();
+ if (size > 0) {
+ final long nowUptime = SystemClock.uptimeMillis();
+ fout.println("Uids transitioning to background:");
+ fout.increaseIndent();
+ for (int i = 0; i < size; i++) {
+ fout.print("UID=");
+ fout.print(mBackgroundTransitioningUids.keyAt(i));
+ fout.print(", ");
+ TimeUtils.formatDuration(mBackgroundTransitioningUids.valueAt(i), nowUptime,
+ fout);
+ fout.println();
+ }
+ fout.decreaseIndent();
+ }
+
final SparseBooleanArray knownUids = new SparseBooleanArray();
collectKeys(mUidState, knownUids);
synchronized (mUidBlockedState) {
@@ -4182,6 +4255,12 @@
return isProcStateAllowedWhileInLowPowerStandby(uidState);
}
+ @GuardedBy("mUidRulesFirstLock")
+ private boolean isUidExemptFromBackgroundRestrictions(int uid) {
+ return mBackgroundTransitioningUids.indexOfKey(uid) >= 0
+ || isProcStateAllowedNetworkWhileBackground(mUidState.get(uid));
+ }
+
/**
* Process state of UID changed; if needed, will trigger
* {@link #updateRulesForDataUsageRestrictionsUL(int)} and
@@ -4207,6 +4286,8 @@
// state changed, push updated rules
mUidState.put(uid, newUidState);
updateRestrictBackgroundRulesOnUidStatusChangedUL(uid, oldUidState, newUidState);
+
+ boolean updatePowerRestrictionRules = false;
boolean allowedWhileIdleOrPowerSaveModeChanged =
isProcStateAllowedWhileIdleOrPowerSaveMode(oldUidState)
!= isProcStateAllowedWhileIdleOrPowerSaveMode(newUidState);
@@ -4218,19 +4299,44 @@
if (mRestrictPower) {
updateRuleForRestrictPowerUL(uid);
}
- updateRulesForPowerRestrictionsUL(uid, procState);
+ updatePowerRestrictionRules = true;
+ }
+ if (mBackgroundNetworkRestricted) {
+ final boolean wasAllowed = isProcStateAllowedNetworkWhileBackground(
+ oldUidState);
+ final boolean isAllowed = isProcStateAllowedNetworkWhileBackground(newUidState);
+ if (!wasAllowed && isAllowed) {
+ mBackgroundTransitioningUids.delete(uid);
+ updateRuleForBackgroundUL(uid);
+ updatePowerRestrictionRules = true;
+ } else if (wasAllowed && !isAllowed) {
+ final long completionTimeMs = SystemClock.uptimeMillis()
+ + mBackgroundRestrictionDelayMs;
+ if (mBackgroundTransitioningUids.indexOfKey(uid) < 0) {
+ // This is just a defensive check in case the upstream code ever makes
+ // multiple calls for the same process state change.
+ mBackgroundTransitioningUids.put(uid, completionTimeMs);
+ }
+ if (!mHandler.hasMessages(MSG_PROCESS_BACKGROUND_TRANSITIONING_UIDS)) {
+ // Many uids may be in this "transitioning" state at the same time, so
+ // using one message at a time to avoid congestion in the MessageQueue.
+ mHandler.sendEmptyMessageAtTime(
+ MSG_PROCESS_BACKGROUND_TRANSITIONING_UIDS, completionTimeMs);
+ }
+ }
}
if (mLowPowerStandbyActive) {
boolean allowedInLpsChanged =
isProcStateAllowedWhileInLowPowerStandby(oldUidState)
!= isProcStateAllowedWhileInLowPowerStandby(newUidState);
if (allowedInLpsChanged) {
- if (!allowedWhileIdleOrPowerSaveModeChanged) {
- updateRulesForPowerRestrictionsUL(uid, procState);
- }
updateRuleForLowPowerStandbyUL(uid);
+ updatePowerRestrictionRules = true;
}
}
+ if (updatePowerRestrictionRules) {
+ updateRulesForPowerRestrictionsUL(uid, procState);
+ }
return true;
}
} finally {
@@ -4253,6 +4359,12 @@
if (mRestrictPower) {
updateRuleForRestrictPowerUL(uid);
}
+ if (mBackgroundNetworkRestricted) {
+ // Uid is no longer running, there is no point in any grace period of network
+ // access during transitions to lower importance proc-states.
+ mBackgroundTransitioningUids.delete(uid);
+ updateRuleForBackgroundUL(uid);
+ }
updateRulesForPowerRestrictionsUL(uid);
if (mLowPowerStandbyActive) {
updateRuleForLowPowerStandbyUL(uid);
@@ -4460,11 +4572,41 @@
}
}
+ /**
+ * Updates the rules for apps allowlisted to use network while in the background.
+ */
+ @GuardedBy("mUidRulesFirstLock")
+ private void updateRulesForBackgroundChainUL() {
+ Trace.traceBegin(Trace.TRACE_TAG_NETWORK, "updateRulesForBackgroundChainUL");
+ try {
+ final SparseIntArray uidRules = mUidFirewallBackgroundRules;
+ uidRules.clear();
+
+ final List<UserInfo> users = mUserManager.getUsers();
+ for (int ui = users.size() - 1; ui >= 0; ui--) {
+ final UserInfo user = users.get(ui);
+ updateRulesForAllowlistedAppIds(uidRules, mPowerSaveTempWhitelistAppIds, user.id);
+ updateRulesForAllowlistedAppIds(uidRules, mPowerSaveWhitelistAppIds, user.id);
+ updateRulesForAllowlistedAppIds(uidRules, mPowerSaveWhitelistExceptIdleAppIds,
+ user.id);
+ }
+ for (int i = mUidState.size() - 1; i >= 0; i--) {
+ if (mBackgroundTransitioningUids.indexOfKey(mUidState.keyAt(i)) >= 0
+ || isProcStateAllowedNetworkWhileBackground(mUidState.valueAt(i))) {
+ uidRules.put(mUidState.keyAt(i), FIREWALL_RULE_ALLOW);
+ }
+ }
+ setUidFirewallRulesUL(FIREWALL_CHAIN_BACKGROUND, uidRules);
+ } finally {
+ Trace.traceEnd(TRACE_TAG_NETWORK);
+ }
+ }
+
private void updateRulesForAllowlistedAppIds(final SparseIntArray uidRules,
- final SparseBooleanArray whitelistedAppIds, int userId) {
- for (int i = whitelistedAppIds.size() - 1; i >= 0; --i) {
- if (whitelistedAppIds.valueAt(i)) {
- final int appId = whitelistedAppIds.keyAt(i);
+ final SparseBooleanArray allowlistedAppIds, int userId) {
+ for (int i = allowlistedAppIds.size() - 1; i >= 0; --i) {
+ if (allowlistedAppIds.valueAt(i)) {
+ final int appId = allowlistedAppIds.keyAt(i);
final int uid = UserHandle.getUid(userId, appId);
uidRules.put(uid, FIREWALL_RULE_ALLOW);
}
@@ -4523,12 +4665,12 @@
@GuardedBy("mUidRulesFirstLock")
private boolean isAllowlistedFromPowerSaveUL(int uid, boolean deviceIdleMode) {
final int appId = UserHandle.getAppId(uid);
- boolean isWhitelisted = mPowerSaveTempWhitelistAppIds.get(appId)
+ boolean allowlisted = mPowerSaveTempWhitelistAppIds.get(appId)
|| mPowerSaveWhitelistAppIds.get(appId);
if (!deviceIdleMode) {
- isWhitelisted = isWhitelisted || isAllowlistedFromPowerSaveExceptIdleUL(uid);
+ allowlisted = allowlisted || isAllowlistedFromPowerSaveExceptIdleUL(uid);
}
- return isWhitelisted;
+ return allowlisted;
}
/**
@@ -4617,6 +4759,38 @@
}
/**
+ * Update firewall rule for a single uid whenever there are any interesting changes in the uid.
+ * Currently, it is called when:
+ * - The uid is added to or removed from power allowlists
+ * - The uid undergoes a process-state change
+ * - A package belonging to this uid is added
+ * - The uid is evicted from memory
+ */
+ @GuardedBy("mUidRulesFirstLock")
+ void updateRuleForBackgroundUL(int uid) {
+ if (!isUidValidForAllowlistRulesUL(uid)) {
+ return;
+ }
+
+ Trace.traceBegin(Trace.TRACE_TAG_NETWORK, "updateRuleForBackgroundUL: " + uid);
+ try {
+ // The uid should be absent from mUidState and mBackgroundTransitioningUids if it is
+ // not running when this method is called. Then, the firewall state will depend on the
+ // allowlist alone. This is the desired behavior.
+ if (isAllowlistedFromPowerSaveUL(uid, false)
+ || isUidExemptFromBackgroundRestrictions(uid)) {
+ setUidFirewallRuleUL(FIREWALL_CHAIN_BACKGROUND, uid, FIREWALL_RULE_ALLOW);
+ if (LOGD) Log.d(TAG, "updateRuleForBackgroundUL ALLOW " + uid);
+ } else {
+ setUidFirewallRuleUL(FIREWALL_CHAIN_BACKGROUND, uid, FIREWALL_RULE_DEFAULT);
+ if (LOGD) Log.d(TAG, "updateRuleForBackgroundUL " + uid + " to DEFAULT");
+ }
+ } finally {
+ Trace.traceEnd(Trace.TRACE_TAG_NETWORK);
+ }
+ }
+
+ /**
* Toggle the firewall standby chain and inform listeners if the uid rules have effectively
* changed.
*/
@@ -4663,6 +4837,9 @@
"updateRulesForGlobalChangeAL: " + (restrictedNetworksChanged ? "R" : "-"));
}
try {
+ if (mBackgroundNetworkRestricted) {
+ updateRulesForBackgroundChainUL();
+ }
updateRulesForAppIdleUL();
updateRulesForRestrictPowerUL();
updateRulesForRestrictBackgroundUL();
@@ -4822,6 +4999,9 @@
updateRuleForAppIdleUL(uid, PROCESS_STATE_UNKNOWN);
updateRuleForDeviceIdleUL(uid);
updateRuleForRestrictPowerUL(uid);
+ if (mBackgroundNetworkRestricted) {
+ updateRuleForBackgroundUL(uid);
+ }
// Update internal rules.
updateRulesForPowerRestrictionsUL(uid);
}
@@ -4959,6 +5139,8 @@
mUidFirewallStandbyRules.delete(uid);
mUidFirewallDozableRules.delete(uid);
mUidFirewallPowerSaveRules.delete(uid);
+ mUidFirewallBackgroundRules.delete(uid);
+ mBackgroundTransitioningUids.delete(uid);
mPowerSaveWhitelistExceptIdleAppIds.delete(uid);
mPowerSaveWhitelistAppIds.delete(uid);
mPowerSaveTempWhitelistAppIds.delete(uid);
@@ -4992,6 +5174,9 @@
updateRuleForDeviceIdleUL(uid);
updateRuleForAppIdleUL(uid, PROCESS_STATE_UNKNOWN);
updateRuleForRestrictPowerUL(uid);
+ if (mBackgroundNetworkRestricted) {
+ updateRuleForBackgroundUL(uid);
+ }
// If the uid has the necessary permissions, then it should be added to the restricted mode
// firewall allowlist.
@@ -5176,7 +5361,6 @@
* Similar to above but ignores idle state if app standby is currently disabled by parole.
*
* @param uid the uid of the app to update rules for
- * @param oldUidRules the current rules for the uid, in order to determine if there's a change
* @param isUidIdle whether uid is idle or not
*/
@GuardedBy("mUidRulesFirstLock")
@@ -5222,6 +5406,7 @@
newBlockedReasons |= (mLowPowerStandbyActive ? BLOCKED_REASON_LOW_POWER_STANDBY : 0);
newBlockedReasons |= (isUidIdle ? BLOCKED_REASON_APP_STANDBY : 0);
newBlockedReasons |= (uidBlockedState.blockedReasons & BLOCKED_REASON_RESTRICTED_MODE);
+ newBlockedReasons |= mBackgroundNetworkRestricted ? BLOCKED_REASON_APP_BACKGROUND : 0;
newAllowedReasons |= (isSystem(uid) ? ALLOWED_REASON_SYSTEM : 0);
newAllowedReasons |= (isForeground ? ALLOWED_REASON_FOREGROUND : 0);
@@ -5234,6 +5419,9 @@
& ALLOWED_REASON_RESTRICTED_MODE_PERMISSIONS);
newAllowedReasons |= (isAllowlistedFromLowPowerStandbyUL(uid))
? ALLOWED_REASON_LOW_POWER_STANDBY_ALLOWLIST : 0;
+ newAllowedReasons |= (mBackgroundNetworkRestricted
+ && isUidExemptFromBackgroundRestrictions(uid))
+ ? ALLOWED_REASON_NOT_IN_BACKGROUND : 0;
uidBlockedState.blockedReasons = (uidBlockedState.blockedReasons
& BLOCKED_METERED_REASON_MASK) | newBlockedReasons;
@@ -5255,7 +5443,7 @@
oldEffectiveBlockedReasons = previousUidBlockedState.effectiveBlockedReasons;
newEffectiveBlockedReasons = uidBlockedState.effectiveBlockedReasons;
- uidRules = oldEffectiveBlockedReasons == newEffectiveBlockedReasons
+ uidRules = (oldEffectiveBlockedReasons == newEffectiveBlockedReasons)
? RULE_NONE
: uidBlockedState.deriveUidRules();
}
@@ -5448,6 +5636,28 @@
mContext.sendBroadcastAsUser(intent, UserHandle.ALL);
return true;
}
+ case MSG_PROCESS_BACKGROUND_TRANSITIONING_UIDS: {
+ final long now = SystemClock.uptimeMillis();
+ long nextCheckTime = Long.MAX_VALUE;
+ synchronized (mUidRulesFirstLock) {
+ for (int i = mBackgroundTransitioningUids.size() - 1; i >= 0; i--) {
+ final long completionTimeMs = mBackgroundTransitioningUids.valueAt(i);
+ if (completionTimeMs > now) {
+ nextCheckTime = Math.min(nextCheckTime, completionTimeMs);
+ continue;
+ }
+ final int uid = mBackgroundTransitioningUids.keyAt(i);
+ mBackgroundTransitioningUids.removeAt(i);
+ updateRuleForBackgroundUL(uid);
+ updateRulesForPowerRestrictionsUL(uid, false);
+ }
+ }
+ if (nextCheckTime < Long.MAX_VALUE) {
+ mHandler.sendEmptyMessageAtTime(MSG_PROCESS_BACKGROUND_TRANSITIONING_UIDS,
+ nextCheckTime);
+ }
+ return true;
+ }
case MSG_POLICIES_CHANGED: {
final int uid = msg.arg1;
final int policy = msg.arg2;
@@ -5859,6 +6069,8 @@
mUidFirewallRestrictedModeRules.put(uid, rule);
} else if (chain == FIREWALL_CHAIN_LOW_POWER_STANDBY) {
mUidFirewallLowPowerStandbyModeRules.put(uid, rule);
+ } else if (chain == FIREWALL_CHAIN_BACKGROUND) {
+ mUidFirewallBackgroundRules.put(uid, rule);
}
try {
@@ -5915,6 +6127,8 @@
FIREWALL_RULE_DEFAULT);
mNetworkManager.setFirewallUidRule(FIREWALL_CHAIN_LOW_POWER_STANDBY, uid,
FIREWALL_RULE_DEFAULT);
+ mNetworkManager.setFirewallUidRule(FIREWALL_CHAIN_BACKGROUND, uid,
+ FIREWALL_RULE_DEFAULT);
mNetworkManager.setUidOnMeteredNetworkAllowlist(uid, false);
mLogger.meteredAllowlistChanged(uid, false);
mNetworkManager.setUidOnMeteredNetworkDenylist(uid, false);
@@ -6441,10 +6655,12 @@
effectiveBlockedReasons &= ~BLOCKED_REASON_BATTERY_SAVER;
effectiveBlockedReasons &= ~BLOCKED_REASON_DOZE;
effectiveBlockedReasons &= ~BLOCKED_REASON_APP_STANDBY;
+ effectiveBlockedReasons &= ~BLOCKED_REASON_APP_BACKGROUND;
}
if ((allowedReasons & ALLOWED_REASON_POWER_SAVE_EXCEPT_IDLE_ALLOWLIST) != 0) {
effectiveBlockedReasons &= ~BLOCKED_REASON_BATTERY_SAVER;
effectiveBlockedReasons &= ~BLOCKED_REASON_APP_STANDBY;
+ effectiveBlockedReasons &= ~BLOCKED_REASON_APP_BACKGROUND;
}
if ((allowedReasons & ALLOWED_REASON_RESTRICTED_MODE_PERMISSIONS) != 0) {
effectiveBlockedReasons &= ~BLOCKED_REASON_RESTRICTED_MODE;
@@ -6455,19 +6671,24 @@
if ((allowedReasons & ALLOWED_REASON_LOW_POWER_STANDBY_ALLOWLIST) != 0) {
effectiveBlockedReasons &= ~BLOCKED_REASON_LOW_POWER_STANDBY;
}
+ if ((allowedReasons & ALLOWED_REASON_NOT_IN_BACKGROUND) != 0) {
+ effectiveBlockedReasons &= ~BLOCKED_REASON_APP_BACKGROUND;
+ }
return effectiveBlockedReasons;
}
static int getAllowedReasonsForProcState(int procState) {
- if (procState > NetworkPolicyManager.FOREGROUND_THRESHOLD_STATE) {
- return ALLOWED_REASON_NONE;
- } else if (procState <= NetworkPolicyManager.TOP_THRESHOLD_STATE) {
+ if (procState <= NetworkPolicyManager.TOP_THRESHOLD_STATE) {
return ALLOWED_REASON_TOP | ALLOWED_REASON_FOREGROUND
- | ALLOWED_METERED_REASON_FOREGROUND;
- } else {
- return ALLOWED_REASON_FOREGROUND | ALLOWED_METERED_REASON_FOREGROUND;
+ | ALLOWED_METERED_REASON_FOREGROUND | ALLOWED_REASON_NOT_IN_BACKGROUND;
+ } else if (procState <= NetworkPolicyManager.FOREGROUND_THRESHOLD_STATE) {
+ return ALLOWED_REASON_FOREGROUND | ALLOWED_METERED_REASON_FOREGROUND
+ | ALLOWED_REASON_NOT_IN_BACKGROUND;
+ } else if (procState < NetworkPolicyManager.BACKGROUND_THRESHOLD_STATE) {
+ return ALLOWED_REASON_NOT_IN_BACKGROUND;
}
+ return ALLOWED_REASON_NONE;
}
@Override
@@ -6492,6 +6713,7 @@
BLOCKED_REASON_APP_STANDBY,
BLOCKED_REASON_RESTRICTED_MODE,
BLOCKED_REASON_LOW_POWER_STANDBY,
+ BLOCKED_REASON_APP_BACKGROUND,
BLOCKED_METERED_REASON_DATA_SAVER,
BLOCKED_METERED_REASON_USER_RESTRICTED,
BLOCKED_METERED_REASON_ADMIN_DISABLED,
@@ -6505,6 +6727,7 @@
ALLOWED_REASON_POWER_SAVE_EXCEPT_IDLE_ALLOWLIST,
ALLOWED_REASON_RESTRICTED_MODE_PERMISSIONS,
ALLOWED_REASON_LOW_POWER_STANDBY_ALLOWLIST,
+ ALLOWED_REASON_NOT_IN_BACKGROUND,
ALLOWED_METERED_REASON_USER_EXEMPTED,
ALLOWED_METERED_REASON_SYSTEM,
ALLOWED_METERED_REASON_FOREGROUND,
@@ -6524,6 +6747,8 @@
return "RESTRICTED_MODE";
case BLOCKED_REASON_LOW_POWER_STANDBY:
return "LOW_POWER_STANDBY";
+ case BLOCKED_REASON_APP_BACKGROUND:
+ return "APP_BACKGROUND";
case BLOCKED_METERED_REASON_DATA_SAVER:
return "DATA_SAVER";
case BLOCKED_METERED_REASON_USER_RESTRICTED:
@@ -6554,6 +6779,8 @@
return "RESTRICTED_MODE_PERMISSIONS";
case ALLOWED_REASON_LOW_POWER_STANDBY_ALLOWLIST:
return "LOW_POWER_STANDBY_ALLOWLIST";
+ case ALLOWED_REASON_NOT_IN_BACKGROUND:
+ return "NOT_IN_BACKGROUND";
case ALLOWED_METERED_REASON_USER_EXEMPTED:
return "METERED_USER_EXEMPTED";
case ALLOWED_METERED_REASON_SYSTEM:
@@ -6621,7 +6848,8 @@
int powerBlockedReasons = BLOCKED_REASON_APP_STANDBY
| BLOCKED_REASON_DOZE
| BLOCKED_REASON_BATTERY_SAVER
- | BLOCKED_REASON_LOW_POWER_STANDBY;
+ | BLOCKED_REASON_LOW_POWER_STANDBY
+ | BLOCKED_REASON_APP_BACKGROUND;
if ((effectiveBlockedReasons & powerBlockedReasons) != 0) {
uidRule |= RULE_REJECT_ALL;
} else if ((blockedReasons & powerBlockedReasons) != 0) {
diff --git a/services/core/java/com/android/server/net/flags.aconfig b/services/core/java/com/android/server/net/flags.aconfig
new file mode 100644
index 0000000..419665a
--- /dev/null
+++ b/services/core/java/com/android/server/net/flags.aconfig
@@ -0,0 +1,8 @@
+package: "com.android.server.net"
+
+flag {
+ name: "network_blocked_for_top_sleeping_and_above"
+ namespace: "backstage_power"
+ description: "Block network access for apps in a low importance background state"
+ bug: "304347838"
+}
diff --git a/services/core/java/com/android/server/notification/NotificationAttentionHelper.java b/services/core/java/com/android/server/notification/NotificationAttentionHelper.java
index 85c4ffe..f852b81 100644
--- a/services/core/java/com/android/server/notification/NotificationAttentionHelper.java
+++ b/services/core/java/com/android/server/notification/NotificationAttentionHelper.java
@@ -57,6 +57,7 @@
import android.telephony.TelephonyManager;
import android.text.TextUtils;
import android.util.Log;
+import android.util.Pair;
import android.util.Slog;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityManager;
@@ -71,7 +72,6 @@
import com.android.server.EventLogTags;
import com.android.server.lights.LightsManager;
import com.android.server.lights.LogicalLight;
-import com.android.server.notification.Flags;
import java.io.PrintWriter;
import java.lang.annotation.Retention;
@@ -81,6 +81,7 @@
import java.util.List;
import java.util.Map;
import java.util.Objects;
+import java.util.Set;
/**
* NotificationManagerService helper for handling notification attention effects:
@@ -100,6 +101,20 @@
private static final int DEFAULT_NOTIFICATION_COOLDOWN_ALL = 1;
private static final int DEFAULT_NOTIFICATION_COOLDOWN_VIBRATE_UNLOCKED = 0;
+ @VisibleForTesting
+ static final Set<String> NOTIFICATION_AVALANCHE_TRIGGER_INTENTS = Set.of(
+ Intent.ACTION_AIRPLANE_MODE_CHANGED,
+ Intent.ACTION_BOOT_COMPLETED,
+ Intent.ACTION_USER_SWITCHED,
+ Intent.ACTION_MANAGED_PROFILE_AVAILABLE
+ );
+
+ @VisibleForTesting
+ static final Map<String, Pair<String, Boolean>> NOTIFICATION_AVALANCHE_TRIGGER_EXTRAS = Map.of(
+ Intent.ACTION_AIRPLANE_MODE_CHANGED, new Pair<>("state", false),
+ Intent.ACTION_MANAGED_PROFILE_AVAILABLE, new Pair<>(Intent.EXTRA_QUIET_MODE, false)
+ );
+
private final Context mContext;
private final PackageManager mPackageManager;
private final TelephonyManager mTelephonyManager;
@@ -191,7 +206,7 @@
mInCallNotificationVolume = resources.getFloat(R.dimen.config_inCallNotificationVolume);
if (Flags.politeNotifications()) {
- mStrategy = getPolitenessStrategy();
+ mStrategy = createPolitenessStrategy();
} else {
mStrategy = null;
}
@@ -200,7 +215,7 @@
loadUserSettings();
}
- private PolitenessStrategy getPolitenessStrategy() {
+ private PolitenessStrategy createPolitenessStrategy() {
if (Flags.crossAppPoliteNotifications()) {
PolitenessStrategy appStrategy = new StrategyPerApp(
mFlagResolver.getIntValue(NotificationFlags.NOTIF_COOLDOWN_T1),
@@ -209,11 +224,12 @@
mFlagResolver.getIntValue(NotificationFlags.NOTIF_VOLUME2),
mFlagResolver.getIntValue(NotificationFlags.NOTIF_COOLDOWN_COUNTER_RESET));
- return new StrategyGlobal(
+ return new StrategyAvalanche(
mFlagResolver.getIntValue(NotificationFlags.NOTIF_COOLDOWN_T1),
mFlagResolver.getIntValue(NotificationFlags.NOTIF_COOLDOWN_T2),
mFlagResolver.getIntValue(NotificationFlags.NOTIF_VOLUME1),
mFlagResolver.getIntValue(NotificationFlags.NOTIF_VOLUME2),
+ mFlagResolver.getIntValue(NotificationFlags.NOTIF_AVALANCHE_TIMEOUT),
appStrategy);
} else {
return new StrategyPerApp(
@@ -225,6 +241,11 @@
}
}
+ @VisibleForTesting
+ PolitenessStrategy getPolitenessStrategy() {
+ return mStrategy;
+ }
+
public void onSystemReady() {
mSystemReady = true;
@@ -259,6 +280,11 @@
filter.addAction(Intent.ACTION_USER_REMOVED);
filter.addAction(Intent.ACTION_USER_SWITCHED);
filter.addAction(Intent.ACTION_USER_UNLOCKED);
+ if (Flags.crossAppPoliteNotifications()) {
+ for (String avalancheIntent : NOTIFICATION_AVALANCHE_TRIGGER_INTENTS) {
+ filter.addAction(avalancheIntent);
+ }
+ }
mContext.registerReceiverAsUser(mIntentReceiver, UserHandle.ALL, filter, null, null);
mContext.getContentResolver().registerContentObserver(
@@ -1052,7 +1078,8 @@
}
}
- abstract private static class PolitenessStrategy {
+ @VisibleForTesting
+ abstract static class PolitenessStrategy {
static final int POLITE_STATE_DEFAULT = 0;
static final int POLITE_STATE_POLITE = 1;
static final int POLITE_STATE_MUTED = 2;
@@ -1079,6 +1106,8 @@
protected boolean mApplyPerPackage;
protected final Map<String, Long> mLastUpdatedTimestampByPackage;
+ protected boolean mIsActive = true;
+
public PolitenessStrategy(int timeoutPolite, int timeoutMuted, int volumePolite,
int volumeMuted) {
mVolumeStates = new HashMap<>();
@@ -1218,6 +1247,10 @@
}
return nextState;
}
+
+ boolean isActive() {
+ return mIsActive;
+ }
}
// TODO b/270456865: Only one of the two strategies will be released.
@@ -1289,55 +1322,60 @@
}
/**
- * Global (cross-app) strategy.
+ * Avalanche (cross-app) strategy.
*/
- private static class StrategyGlobal extends PolitenessStrategy {
+ private static class StrategyAvalanche extends PolitenessStrategy {
private static final String COMMON_KEY = "cross_app_common_key";
private final PolitenessStrategy mAppStrategy;
private long mLastNotificationTimestamp = 0;
- public StrategyGlobal(int timeoutPolite, int timeoutMuted, int volumePolite,
- int volumeMuted, PolitenessStrategy appStrategy) {
+ private final int mTimeoutAvalanche;
+ private long mLastAvalancheTriggerTimestamp = 0;
+
+ StrategyAvalanche(int timeoutPolite, int timeoutMuted, int volumePolite,
+ int volumeMuted, int timeoutAvalanche, PolitenessStrategy appStrategy) {
super(timeoutPolite, timeoutMuted, volumePolite, volumeMuted);
+ mTimeoutAvalanche = timeoutAvalanche;
mAppStrategy = appStrategy;
if (DEBUG) {
- Log.i(TAG, "StrategyGlobal: " + timeoutPolite + " " + timeoutMuted);
+ Log.i(TAG, "StrategyAvalanche: " + timeoutPolite + " " + timeoutMuted + " "
+ + timeoutAvalanche);
}
}
@Override
void onNotificationPosted(NotificationRecord record) {
- if (shouldIgnoreNotification(record)) {
- return;
- }
+ if (isAvalancheActive()) {
+ if (shouldIgnoreNotification(record)) {
+ return;
+ }
- long timeSinceLastNotif =
+ long timeSinceLastNotif =
System.currentTimeMillis() - getLastNotificationUpdateTimeMs(record);
- final String key = getChannelKey(record);
- @PolitenessState final int currState = getPolitenessState(record);
- @PolitenessState int nextState = getNextState(currState, timeSinceLastNotif);
+ final String key = getChannelKey(record);
+ @PolitenessState final int currState = getPolitenessState(record);
+ @PolitenessState int nextState = getNextState(currState, timeSinceLastNotif);
- if (DEBUG) {
- Log.i(TAG, "StrategyGlobal onNotificationPosted time delta: " + timeSinceLastNotif
- + " vol state: " + nextState + " key: " + key);
+ if (DEBUG) {
+ Log.i(TAG,
+ "StrategyAvalanche onNotificationPosted time delta: "
+ + timeSinceLastNotif
+ + " vol state: " + nextState + " key: " + key);
+ }
+
+ mVolumeStates.put(key, nextState);
}
- mVolumeStates.put(key, nextState);
-
mAppStrategy.onNotificationPosted(record);
}
@Override
public float getSoundVolume(final NotificationRecord record) {
- final @PolitenessState int globalVolState = getPolitenessState(record);
- final @PolitenessState int appVolState = mAppStrategy.getPolitenessState(record);
-
- // Prioritize the most polite outcome
- if (globalVolState > appVolState) {
+ if (isAvalancheActive()) {
return super.getSoundVolume(record);
} else {
return mAppStrategy.getSoundVolume(record);
@@ -1382,6 +1420,24 @@
super.setApplyCooldownPerPackage(applyPerPackage);
mAppStrategy.setApplyCooldownPerPackage(applyPerPackage);
}
+
+ boolean isAvalancheActive() {
+ mIsActive = (System.currentTimeMillis() - mLastAvalancheTriggerTimestamp
+ < mTimeoutAvalanche);
+ if (DEBUG) {
+ Log.i(TAG, "StrategyAvalanche: active " + mIsActive);
+ }
+ return mIsActive;
+ }
+
+ @Override
+ boolean isActive() {
+ return isAvalancheActive();
+ }
+
+ void setTriggerTimeMs(long timestamp) {
+ mLastAvalancheTriggerTimestamp = timestamp;
+ }
}
//====================== Observers =============================
@@ -1415,6 +1471,30 @@
|| action.equals(Intent.ACTION_USER_UNLOCKED)) {
loadUserSettings();
}
+
+ if (Flags.crossAppPoliteNotifications()) {
+ if (NOTIFICATION_AVALANCHE_TRIGGER_INTENTS.contains(action)) {
+ boolean enableAvalancheStrategy = true;
+ // Some actions must also match extras, ie. airplane mode => disabled
+ Pair<String, Boolean> expectedExtras =
+ NOTIFICATION_AVALANCHE_TRIGGER_EXTRAS.get(action);
+ if (expectedExtras != null) {
+ enableAvalancheStrategy =
+ intent.getBooleanExtra(expectedExtras.first, false)
+ == expectedExtras.second;
+ }
+
+ if (DEBUG) {
+ Log.i(TAG, "Avalanche trigger intent received: " + action
+ + ". Enabling avalanche strategy: " + enableAvalancheStrategy);
+ }
+
+ if (enableAvalancheStrategy && mStrategy instanceof StrategyAvalanche) {
+ ((StrategyAvalanche) mStrategy)
+ .setTriggerTimeMs(System.currentTimeMillis());
+ }
+ }
+ }
}
};
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index 308d441..425659e 100755
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -282,6 +282,7 @@
import android.service.notification.StatusBarNotification;
import android.service.notification.ZenModeConfig;
import android.service.notification.ZenModeProto;
+import android.service.notification.ZenPolicy;
import android.telecom.TelecomManager;
import android.telephony.PhoneStateListener;
import android.telephony.TelephonyManager;
@@ -5968,6 +5969,20 @@
}
}
+ /**
+ * Gets the device-default zen policy as a ZenPolicy.
+ */
+ @Override
+ public ZenPolicy getDefaultZenPolicy() {
+ enforceSystemOrSystemUI("INotificationManager.getDefaultZenPolicy");
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ return mZenModeHelper.getDefaultZenPolicy();
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
+
@Override
public List<String> getEnabledNotificationListenerPackages() {
checkCallerIsSystem();
diff --git a/services/core/java/com/android/server/notification/ZenModeHelper.java b/services/core/java/com/android/server/notification/ZenModeHelper.java
index 153af13..923be56d 100644
--- a/services/core/java/com/android/server/notification/ZenModeHelper.java
+++ b/services/core/java/com/android/server/notification/ZenModeHelper.java
@@ -604,6 +604,14 @@
ZenRule rule = newConfig.automaticRules.get(implicitRuleId(callingPkg));
if (rule == null) {
rule = newImplicitZenRule(callingPkg);
+
+ // For new implicit rules, create a policy matching the current global
+ // (manual rule) settings, for consistency with the policy that
+ // would apply if changing the global interruption filter. We only do this
+ // for newly created rules, as existing rules have a pre-existing policy
+ // (whether initialized here or set via app or user).
+ rule.zenPolicy = mConfig.toZenPolicy();
+
newConfig.automaticRules.put(rule.id, rule);
}
// If the user has changed the rule's *zenMode*, then don't let app overwrite it.
@@ -615,6 +623,7 @@
rule.condition = new Condition(rule.conditionId,
mContext.getString(R.string.zen_mode_implicit_activated),
Condition.STATE_TRUE);
+
setConfigLocked(newConfig, /* triggeringComponent= */ null, UPDATE_ORIGIN_APP,
"applyGlobalZenModeAsImplicitZenRule", callingUid);
}
@@ -643,8 +652,10 @@
return;
}
ZenModeConfig newConfig = mConfig.copy();
+ boolean isNew = false;
ZenRule rule = newConfig.automaticRules.get(implicitRuleId(callingPkg));
if (rule == null) {
+ isNew = true;
rule = newImplicitZenRule(callingPkg);
rule.zenMode = Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS;
newConfig.automaticRules.put(rule.id, rule);
@@ -652,10 +663,20 @@
// If the user has changed the rule's *ZenPolicy*, then don't let app overwrite it.
// We allow the update if the user has only changed other aspects of the rule.
if (rule.zenPolicyUserModifiedFields == 0) {
+ ZenPolicy newZenPolicy = ZenAdapters.notificationPolicyToZenPolicy(policy);
+ if (isNew) {
+ // For new rules only, fill anything underspecified in the new policy with
+ // values from the global configuration, for consistency with the policy that
+ // would take effect if changing the global policy.
+ // Note that NotificationManager.Policy cannot have any unset priority
+ // categories, but *can* have unset visual effects, which is why we do this.
+ newZenPolicy = mConfig.toZenPolicy().overwrittenWith(newZenPolicy);
+ }
updatePolicy(
rule,
- ZenAdapters.notificationPolicyToZenPolicy(policy),
- /* updateBitmask= */ false);
+ newZenPolicy,
+ /* updateBitmask= */ false,
+ isNew);
setConfigLocked(newConfig, /* triggeringComponent= */ null, UPDATE_ORIGIN_APP,
"applyGlobalPolicyAsImplicitZenRule", callingUid);
@@ -685,6 +706,11 @@
}
ZenRule implicitRule = mConfig.automaticRules.get(implicitRuleId(callingPkg));
if (implicitRule != null && implicitRule.zenPolicy != null) {
+ // toNotificationPolicy takes defaults from mConfig, and technically, those are not
+ // the defaults that would apply if any fields were unset. However, all rules should
+ // have all fields set in their ZenPolicy objects upon rule creation, so in
+ // practice, this is only filling in the areChannelsBypassingDnd field, which is a
+ // state rather than a part of the policy.
return mConfig.toNotificationPolicy(implicitRule.zenPolicy);
} else {
return getNotificationPolicy();
@@ -1072,7 +1098,8 @@
rule.zenMode = newZenMode;
// Updates the bitmask and values for all policy fields, based on the origin.
- updatePolicy(rule, automaticZenRule.getZenPolicy(), updateBitmask);
+ updatePolicy(rule, automaticZenRule.getZenPolicy(), updateBitmask, isNew);
+
// Updates the bitmask and values for all device effect fields, based on the origin.
updateZenDeviceEffects(rule, automaticZenRule.getDeviceEffects(),
origin == UPDATE_ORIGIN_APP, updateBitmask);
@@ -1111,14 +1138,19 @@
/**
* Modifies the {@link ZenPolicy} associated to a new or updated ZenRule.
*
- * <p>The new policy is {@code newPolicy}, while the user-modified bitmask is updated to reflect
- * the changes being applied (if applicable, i.e. if the update is from the user).
+ * <p>The update takes any set fields in {@code newPolicy} as new policy settings for the
+ * provided {@code ZenRule}, keeping any pre-existing settings from {@code zenRule.zenPolicy}
+ * for any unset policy fields in {@code newPolicy}. The user-modified bitmask is updated to
+ * reflect the changes being applied (if applicable, i.e. if the update is from the user).
*/
private void updatePolicy(ZenRule zenRule, @Nullable ZenPolicy newPolicy,
- boolean updateBitmask) {
+ boolean updateBitmask, boolean isNew) {
if (newPolicy == null) {
- // TODO: b/319242206 - Treat as newPolicy == default policy and continue below.
- zenRule.zenPolicy = null;
+ if (isNew) {
+ // Newly created rule with no provided policy; fill in with the default.
+ zenRule.zenPolicy = mDefaultConfig.toZenPolicy();
+ }
+ // Otherwise, a null policy means no policy changes, so we can stop here.
return;
}
@@ -1127,6 +1159,16 @@
ZenPolicy oldPolicy =
zenRule.zenPolicy != null ? zenRule.zenPolicy : mDefaultConfig.toZenPolicy();
+ // If this is updating a rule rather than creating a new one, keep any fields from the
+ // old policy if they are unspecified in the new policy. For newly created rules, oldPolicy
+ // has been set to the default settings above, so any unspecified fields in a newly created
+ // policy are filled with default values. Then use the fully-specified version of the new
+ // policy for comparison below.
+ //
+ // Although we do not expect a policy update from the user to contain any unset fields,
+ // filling in fields here also guards against any unset fields counting as a "diff" when
+ // comparing fields for bitmask editing below.
+ newPolicy = oldPolicy.overwrittenWith(newPolicy);
zenRule.zenPolicy = newPolicy;
if (updateBitmask) {
@@ -1141,7 +1183,7 @@
!= newPolicy.getPriorityConversationSenders()) {
userModifiedFields |= ZenPolicy.FIELD_CONVERSATIONS;
}
- if (oldPolicy.getPriorityChannels() != newPolicy.getPriorityChannels()) {
+ if (oldPolicy.getPriorityChannelsAllowed() != newPolicy.getPriorityChannelsAllowed()) {
userModifiedFields |= ZenPolicy.FIELD_ALLOW_CHANNELS;
}
if (oldPolicy.getPriorityCategoryReminders()
@@ -1452,11 +1494,27 @@
}
allRulesDisabled &= !automaticRule.enabled;
+
+ // Upon upgrading to a version with modes_api enabled, keep all behaviors of
+ // rules with null ZenPolicies explicitly as a copy of the global policy.
+ if (Flags.modesApi() && config.version < ZenModeConfig.XML_VERSION_MODES_API) {
+ // Keep the manual ("global") policy that from config.
+ ZenPolicy manualRulePolicy = config.toZenPolicy();
+ if (automaticRule.zenPolicy == null) {
+ automaticRule.zenPolicy = manualRulePolicy;
+ } else {
+ // newPolicy is a policy with all unset fields in the rule's zenPolicy
+ // set to their values from the values in config. Then convert that back
+ // to ZenPolicy to store with the automatic zen rule.
+ automaticRule.zenPolicy =
+ manualRulePolicy.overwrittenWith(automaticRule.zenPolicy);
+ }
+ }
}
}
if (!hasDefaultRules && allRulesDisabled
- && (forRestore || config.version < ZenModeConfig.XML_VERSION)) {
+ && (forRestore || config.version < ZenModeConfig.XML_VERSION_ZEN_UPGRADE)) {
// reset zen automatic rules to default on restore or upgrade if:
// - doesn't already have default rules and
// - all previous automatic rules were disabled
@@ -1473,7 +1531,7 @@
// Resolve user id for settings.
userId = userId == UserHandle.USER_ALL ? UserHandle.USER_SYSTEM : userId;
- if (config.version < ZenModeConfig.XML_VERSION) {
+ if (config.version < ZenModeConfig.XML_VERSION_ZEN_UPGRADE) {
Settings.Secure.putIntForUser(mContext.getContentResolver(),
Settings.Secure.SHOW_ZEN_UPGRADE_NOTIFICATION, 1, userId);
} else {
@@ -1600,6 +1658,14 @@
return mConsolidatedPolicy.copy();
}
+ /**
+ * Returns a copy of the device default policy as a ZenPolicy object.
+ */
+ @VisibleForTesting
+ protected ZenPolicy getDefaultZenPolicy() {
+ return mDefaultConfig.toZenPolicy();
+ }
+
@GuardedBy("mConfigLock")
private boolean setConfigLocked(ZenModeConfig config, ComponentName triggeringComponent,
@ConfigChangeOrigin int origin, String reason, int callingUid) {
@@ -1783,7 +1849,7 @@
}
@GuardedBy("mConfigLock")
- private void applyCustomPolicy(ZenPolicy policy, ZenRule rule) {
+ private void applyCustomPolicy(ZenPolicy policy, ZenRule rule, boolean useManualConfig) {
if (rule.zenMode == Global.ZEN_MODE_NO_INTERRUPTIONS) {
policy.apply(new ZenPolicy.Builder()
.disallowAllSounds()
@@ -1797,8 +1863,22 @@
} else if (rule.zenPolicy != null) {
policy.apply(rule.zenPolicy);
} else {
- // active rule with no specified policy inherits the default settings
- policy.apply(mConfig.toZenPolicy());
+ if (Flags.modesApi()) {
+ if (useManualConfig) {
+ // manual rule is configured using the settings stored directly in mConfig
+ policy.apply(mConfig.toZenPolicy());
+ } else {
+ // under modes_api flag, an active automatic rule with no specified policy
+ // inherits the device default settings as stored in mDefaultConfig. While the
+ // rule's policy fields should be set upon creation, this is a fallback to
+ // catch any that may have fallen through the cracks.
+ Log.wtf(TAG, "active automatic rule found with no specified policy: " + rule);
+ policy.apply(mDefaultConfig.toZenPolicy());
+ }
+ } else {
+ // active rule with no specified policy inherits the global config settings
+ policy.apply(mConfig.toZenPolicy());
+ }
}
}
@@ -1810,7 +1890,7 @@
ZenPolicy policy = new ZenPolicy();
ZenDeviceEffects.Builder deviceEffectsBuilder = new ZenDeviceEffects.Builder();
if (mConfig.manualRule != null) {
- applyCustomPolicy(policy, mConfig.manualRule);
+ applyCustomPolicy(policy, mConfig.manualRule, true);
if (Flags.modesApi()) {
deviceEffectsBuilder.add(mConfig.manualRule.zenDeviceEffects);
}
@@ -1822,7 +1902,7 @@
// policy. This is relevant in case some other active rule has a more
// restrictive INTERRUPTION_FILTER but a more lenient ZenPolicy!
if (!Flags.modesApi() || automaticRule.zenMode != Global.ZEN_MODE_OFF) {
- applyCustomPolicy(policy, automaticRule);
+ applyCustomPolicy(policy, automaticRule, false);
}
if (Flags.modesApi()) {
deviceEffectsBuilder.add(automaticRule.zenDeviceEffects);
@@ -1830,6 +1910,14 @@
}
}
+ // While mConfig.toNotificationPolicy fills in any unset fields from the provided
+ // config (which, in this case is the manual "global" config), under modes API changes,
+ // we should have no remaining unset fields: the manual policy gets every field from
+ // the global policy, and each automatic rule has all policy fields filled in on
+ // creation or update.
+ // However, the piece of information that comes from mConfig that we must keep is the
+ // areChannelsBypassingDnd bit, which is a state, rather than a policy, and even when
+ // all policy fields are set, this state comes to the new policy from this call.
Policy newPolicy = mConfig.toNotificationPolicy(policy);
if (!Objects.equals(mConsolidatedPolicy, newPolicy)) {
mConsolidatedPolicy = newPolicy;
diff --git a/services/core/java/com/android/server/om/IdmapManager.java b/services/core/java/com/android/server/om/IdmapManager.java
index 25a39cc..86d05d9 100644
--- a/services/core/java/com/android/server/om/IdmapManager.java
+++ b/services/core/java/com/android/server/om/IdmapManager.java
@@ -257,7 +257,7 @@
private boolean matchesActorSignature(@NonNull AndroidPackage targetPackage,
@NonNull AndroidPackage overlayPackage, int userId) {
String targetOverlayableName = overlayPackage.getOverlayTargetOverlayableName();
- if (targetOverlayableName != null) {
+ if (targetOverlayableName != null && !mPackageManager.getNamedActors().isEmpty()) {
try {
OverlayableInfo overlayableInfo = mPackageManager.getOverlayableForTarget(
targetPackage.getPackageName(), targetOverlayableName, userId);
diff --git a/services/core/java/com/android/server/om/OverlayManagerService.java b/services/core/java/com/android/server/om/OverlayManagerService.java
index b9464d9..a61b03f 100644
--- a/services/core/java/com/android/server/om/OverlayManagerService.java
+++ b/services/core/java/com/android/server/om/OverlayManagerService.java
@@ -32,6 +32,7 @@
import static android.os.Trace.TRACE_TAG_RRO;
import static android.os.Trace.traceBegin;
import static android.os.Trace.traceEnd;
+
import static com.android.server.om.OverlayManagerServiceImpl.OperationFailedException;
import android.annotation.NonNull;
@@ -362,7 +363,7 @@
defaultPackages.add(packageName);
}
}
- return defaultPackages.toArray(new String[defaultPackages.size()]);
+ return defaultPackages.toArray(new String[0]);
}
private final class OverlayManagerPackageMonitor extends PackageMonitor {
@@ -1143,9 +1144,10 @@
};
private static final class PackageManagerHelperImpl implements PackageManagerHelper {
- private static class PackageStateUsers {
+ private static final class PackageStateUsers {
private PackageState mPackageState;
- private final Set<Integer> mInstalledUsers = new ArraySet<>();
+ private Boolean mDefinesOverlayable = null;
+ private final ArraySet<Integer> mInstalledUsers = new ArraySet<>();
private PackageStateUsers(@NonNull PackageState packageState) {
this.mPackageState = packageState;
}
@@ -1160,7 +1162,7 @@
// state may lead to contradictions within OMS. Better then to lag
// behind until all pending intents have been processed.
private final ArrayMap<String, PackageStateUsers> mCache = new ArrayMap<>();
- private final Set<Integer> mInitializedUsers = new ArraySet<>();
+ private final ArraySet<Integer> mInitializedUsers = new ArraySet<>();
PackageManagerHelperImpl(Context context) {
mContext = context;
@@ -1176,8 +1178,7 @@
*/
@NonNull
public ArrayMap<String, PackageState> initializeForUser(final int userId) {
- if (!mInitializedUsers.contains(userId)) {
- mInitializedUsers.add(userId);
+ if (mInitializedUsers.add(userId)) {
mPackageManagerInternal.forEachPackageState((packageState -> {
if (packageState.getPkg() != null
&& packageState.getUserStateOrDefault(userId).isInstalled()) {
@@ -1196,13 +1197,11 @@
return userPackages;
}
- @Override
- @Nullable
- public PackageState getPackageStateForUser(@NonNull final String packageName,
+ private PackageStateUsers getRawPackageStateForUser(@NonNull final String packageName,
final int userId) {
final PackageStateUsers pkg = mCache.get(packageName);
if (pkg != null && pkg.mInstalledUsers.contains(userId)) {
- return pkg.mPackageState;
+ return pkg;
}
try {
if (!mPackageManager.isPackageAvailable(packageName, userId)) {
@@ -1216,8 +1215,14 @@
return addPackageUser(packageName, userId);
}
- @NonNull
- private PackageState addPackageUser(@NonNull final String packageName,
+ @Override
+ public PackageState getPackageStateForUser(@NonNull final String packageName,
+ final int userId) {
+ final PackageStateUsers pkg = getRawPackageStateForUser(packageName, userId);
+ return pkg != null ? pkg.mPackageState : null;
+ }
+
+ private PackageStateUsers addPackageUser(@NonNull final String packageName,
final int user) {
final PackageState pkg = mPackageManagerInternal.getPackageStateInternal(packageName);
if (pkg == null) {
@@ -1229,20 +1234,20 @@
}
@NonNull
- private PackageState addPackageUser(@NonNull final PackageState pkg,
+ private PackageStateUsers addPackageUser(@NonNull final PackageState pkg,
final int user) {
PackageStateUsers pkgUsers = mCache.get(pkg.getPackageName());
if (pkgUsers == null) {
pkgUsers = new PackageStateUsers(pkg);
mCache.put(pkg.getPackageName(), pkgUsers);
- } else {
+ } else if (pkgUsers.mPackageState != pkg) {
pkgUsers.mPackageState = pkg;
+ pkgUsers.mDefinesOverlayable = null;
}
pkgUsers.mInstalledUsers.add(user);
- return pkgUsers.mPackageState;
+ return pkgUsers;
}
-
@NonNull
private void removePackageUser(@NonNull final String packageName, final int user) {
final PackageStateUsers pkgUsers = mCache.get(packageName);
@@ -1260,15 +1265,15 @@
}
}
- @Nullable
public PackageState onPackageAdded(@NonNull final String packageName, final int userId) {
- return addPackageUser(packageName, userId);
+ final var pu = addPackageUser(packageName, userId);
+ return pu != null ? pu.mPackageState : null;
}
- @Nullable
public PackageState onPackageUpdated(@NonNull final String packageName,
final int userId) {
- return addPackageUser(packageName, userId);
+ final var pu = addPackageUser(packageName, userId);
+ return pu != null ? pu.mPackageState : null;
}
public void onPackageRemoved(@NonNull final String packageName, final int userId) {
@@ -1308,22 +1313,30 @@
return (pkgs.length == 0) ? null : pkgs[0];
}
- @Nullable
@Override
public OverlayableInfo getOverlayableForTarget(@NonNull String packageName,
@NonNull String targetOverlayableName, int userId)
throws IOException {
- var packageState = getPackageStateForUser(packageName, userId);
- var pkg = packageState == null ? null : packageState.getAndroidPackage();
+ final var psu = getRawPackageStateForUser(packageName, userId);
+ final var pkg = (psu == null || psu.mPackageState == null)
+ ? null : psu.mPackageState.getAndroidPackage();
if (pkg == null) {
throw new IOException("Unable to get target package");
}
+ if (Boolean.FALSE.equals(psu.mDefinesOverlayable)) {
+ return null;
+ }
+
ApkAssets apkAssets = null;
try {
apkAssets = ApkAssets.loadFromPath(pkg.getSplits().get(0).getPath(),
ApkAssets.PROPERTY_ONLY_OVERLAYABLES);
- return apkAssets.getOverlayableInfo(targetOverlayableName);
+ if (psu.mDefinesOverlayable == null) {
+ psu.mDefinesOverlayable = apkAssets.definesOverlayable();
+ }
+ return Boolean.FALSE.equals(psu.mDefinesOverlayable)
+ ? null : apkAssets.getOverlayableInfo(targetOverlayableName);
} finally {
if (apkAssets != null) {
try {
@@ -1337,24 +1350,29 @@
@Override
public boolean doesTargetDefineOverlayable(String targetPackageName, int userId)
throws IOException {
- var packageState = getPackageStateForUser(targetPackageName, userId);
- var pkg = packageState == null ? null : packageState.getAndroidPackage();
+ final var psu = getRawPackageStateForUser(targetPackageName, userId);
+ var pkg = (psu == null || psu.mPackageState == null)
+ ? null : psu.mPackageState.getAndroidPackage();
if (pkg == null) {
throw new IOException("Unable to get target package");
}
- ApkAssets apkAssets = null;
- try {
- apkAssets = ApkAssets.loadFromPath(pkg.getSplits().get(0).getPath());
- return apkAssets.definesOverlayable();
- } finally {
- if (apkAssets != null) {
- try {
- apkAssets.close();
- } catch (Throwable ignored) {
+ if (psu.mDefinesOverlayable == null) {
+ ApkAssets apkAssets = null;
+ try {
+ apkAssets = ApkAssets.loadFromPath(pkg.getSplits().get(0).getPath(),
+ ApkAssets.PROPERTY_ONLY_OVERLAYABLES);
+ psu.mDefinesOverlayable = apkAssets.definesOverlayable();
+ } finally {
+ if (apkAssets != null) {
+ try {
+ apkAssets.close();
+ } catch (Throwable ignored) {
+ }
}
}
}
+ return psu.mDefinesOverlayable;
}
@Override
@@ -1545,8 +1563,7 @@
final OverlayPaths frameworkOverlays =
mImpl.getEnabledOverlayPaths("android", userId, false);
for (final String targetPackageName : targetPackageNames) {
- final OverlayPaths.Builder list = new OverlayPaths.Builder();
- list.addAll(frameworkOverlays);
+ final var list = new OverlayPaths.Builder(frameworkOverlays);
if (!"android".equals(targetPackageName)) {
list.addAll(mImpl.getEnabledOverlayPaths(targetPackageName, userId, true));
}
@@ -1558,17 +1575,21 @@
final HashSet<String> invalidPackages = new HashSet<>();
pm.setEnabledOverlayPackages(userId, pendingChanges, updatedPackages, invalidPackages);
- for (final String targetPackageName : targetPackageNames) {
- if (DEBUG) {
- Slog.d(TAG, "-> Updating overlay: target=" + targetPackageName + " overlays=["
- + pendingChanges.get(targetPackageName)
- + "] userId=" + userId);
- }
+ if (DEBUG || !invalidPackages.isEmpty()) {
+ for (final String targetPackageName : targetPackageNames) {
+ if (DEBUG) {
+ Slog.d(TAG,
+ "-> Updating overlay: target=" + targetPackageName + " overlays=["
+ + pendingChanges.get(targetPackageName)
+ + "] userId=" + userId);
+ }
- if (invalidPackages.contains(targetPackageName)) {
- Slog.e(TAG, TextUtils.formatSimple(
- "Failed to change enabled overlays for %s user %d", targetPackageName,
- userId));
+ if (invalidPackages.contains(targetPackageName)) {
+ Slog.e(TAG, TextUtils.formatSimple(
+ "Failed to change enabled overlays for %s user %d",
+ targetPackageName,
+ userId));
+ }
}
}
return new ArrayList<>(updatedPackages);
diff --git a/services/core/java/com/android/server/om/OverlayManagerServiceImpl.java b/services/core/java/com/android/server/om/OverlayManagerServiceImpl.java
index 972c78d..c1b6ccc 100644
--- a/services/core/java/com/android/server/om/OverlayManagerServiceImpl.java
+++ b/services/core/java/com/android/server/om/OverlayManagerServiceImpl.java
@@ -772,24 +772,20 @@
OverlayPaths getEnabledOverlayPaths(@NonNull final String targetPackageName,
final int userId, boolean includeImmutableOverlays) {
- final List<OverlayInfo> overlays = mSettings.getOverlaysForTarget(targetPackageName,
- userId);
- final OverlayPaths.Builder paths = new OverlayPaths.Builder();
- final int n = overlays.size();
- for (int i = 0; i < n; i++) {
- final OverlayInfo oi = overlays.get(i);
+ final var paths = new OverlayPaths.Builder();
+ mSettings.forEachMatching(userId, null, targetPackageName, oi -> {
if (!oi.isEnabled()) {
- continue;
+ return;
}
if (!includeImmutableOverlays && !oi.isMutable) {
- continue;
+ return;
}
if (oi.isFabricated()) {
paths.addNonApkPath(oi.baseCodePath);
} else {
paths.addApkPath(oi.baseCodePath);
}
- }
+ });
return paths.build();
}
diff --git a/services/core/java/com/android/server/om/OverlayManagerSettings.java b/services/core/java/com/android/server/om/OverlayManagerSettings.java
index eae614a..b8b49f3e 100644
--- a/services/core/java/com/android/server/om/OverlayManagerSettings.java
+++ b/services/core/java/com/android/server/om/OverlayManagerSettings.java
@@ -47,6 +47,7 @@
import java.util.List;
import java.util.Objects;
import java.util.Set;
+import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.stream.Stream;
@@ -182,6 +183,23 @@
return CollectionUtils.map(items, SettingsItem::getOverlayInfo);
}
+ void forEachMatching(int userId, String overlayName, String targetPackageName,
+ @NonNull Consumer<OverlayInfo> consumer) {
+ for (int i = 0, n = mItems.size(); i < n; i++) {
+ final SettingsItem item = mItems.get(i);
+ if (item.getUserId() != userId) {
+ continue;
+ }
+ if (overlayName != null && !item.mOverlay.getPackageName().equals(overlayName)) {
+ continue;
+ }
+ if (targetPackageName != null && !item.mTargetPackageName.equals(targetPackageName)) {
+ continue;
+ }
+ consumer.accept(item.getOverlayInfo());
+ }
+ }
+
ArrayMap<String, List<OverlayInfo>> getOverlaysForUser(final int userId) {
final List<SettingsItem> items = selectWhereUser(userId);
diff --git a/services/core/java/com/android/server/os/BugreportManagerServiceImpl.java b/services/core/java/com/android/server/os/BugreportManagerServiceImpl.java
index 1660c3e..8452c0e 100644
--- a/services/core/java/com/android/server/os/BugreportManagerServiceImpl.java
+++ b/services/core/java/com/android/server/os/BugreportManagerServiceImpl.java
@@ -152,14 +152,13 @@
@RequiresPermission(value = android.Manifest.permission.INTERACT_ACROSS_USERS,
conditional = true)
void ensureCallerPreviouslyGeneratedFile(
- Context context, Pair<Integer, String> callingInfo, int userId,
- String bugreportFile, boolean forceUpdateMapping) {
+ Context context, PackageManager packageManager, Pair<Integer, String> callingInfo,
+ int userId, String bugreportFile, boolean forceUpdateMapping) {
synchronized (mLock) {
if (onboardingBugreportV2Enabled()) {
final int uidForUser = Binder.withCleanCallingIdentity(() -> {
try {
- return context.getPackageManager()
- .getPackageUidAsUser(callingInfo.second, userId);
+ return packageManager.getPackageUidAsUser(callingInfo.second, userId);
} catch (PackageManager.NameNotFoundException exception) {
throwInvalidBugreportFileForCallerException(
bugreportFile, callingInfo.second);
@@ -441,8 +440,8 @@
Slogf.i(TAG, "Retrieving bugreport for %s / %d", callingPackage, callingUid);
try {
mBugreportFileManager.ensureCallerPreviouslyGeneratedFile(
- mContext, new Pair<>(callingUid, callingPackage), userId, bugreportFile,
- /* forceUpdateMapping= */ false);
+ mContext, mContext.getPackageManager(), new Pair<>(callingUid, callingPackage),
+ userId, bugreportFile, /* forceUpdateMapping= */ false);
} catch (IllegalArgumentException e) {
Slog.e(TAG, e.getMessage());
reportError(listener, IDumpstateListener.BUGREPORT_ERROR_NO_BUGREPORT_TO_RETRIEVE);
diff --git a/services/core/java/com/android/server/pm/AppsFilterImpl.java b/services/core/java/com/android/server/pm/AppsFilterImpl.java
index f3df424..cc4c2b5 100644
--- a/services/core/java/com/android/server/pm/AppsFilterImpl.java
+++ b/services/core/java/com/android/server/pm/AppsFilterImpl.java
@@ -1031,12 +1031,12 @@
private void recomputeComponentVisibility(
ArrayMap<String, ? extends PackageStateInternal> existingSettings) {
final WatchedArraySet<String> protectedBroadcasts;
- final WatchedArraySet<Integer> forceQueryable;
+ final ArraySet<Integer> forceQueryable;
synchronized (mProtectedBroadcastsLock) {
- protectedBroadcasts = mProtectedBroadcasts.snapshot();
+ protectedBroadcasts = new WatchedArraySet<String>(mProtectedBroadcasts);
}
synchronized (mForceQueryableLock) {
- forceQueryable = mForceQueryable.snapshot();
+ forceQueryable = new ArraySet<Integer>(mForceQueryable.untrackedStorage());
}
final ParallelComputeComponentVisibility computer = new ParallelComputeComponentVisibility(
existingSettings, forceQueryable, protectedBroadcasts);
diff --git a/services/core/java/com/android/server/pm/AppsFilterUtils.java b/services/core/java/com/android/server/pm/AppsFilterUtils.java
index 200734b..a02a1bc 100644
--- a/services/core/java/com/android/server/pm/AppsFilterUtils.java
+++ b/services/core/java/com/android/server/pm/AppsFilterUtils.java
@@ -198,12 +198,12 @@
private static final int MAX_THREADS = 4;
private final ArrayMap<String, ? extends PackageStateInternal> mExistingSettings;
- private final WatchedArraySet<Integer> mForceQueryable;
+ private final ArraySet<Integer> mForceQueryable;
private final WatchedArraySet<String> mProtectedBroadcasts;
ParallelComputeComponentVisibility(
@NonNull ArrayMap<String, ? extends PackageStateInternal> existingSettings,
- @NonNull WatchedArraySet<Integer> forceQueryable,
+ @NonNull ArraySet<Integer> forceQueryable,
@NonNull WatchedArraySet<String> protectedBroadcasts) {
mExistingSettings = existingSettings;
mForceQueryable = forceQueryable;
diff --git a/services/core/java/com/android/server/pm/InstallPackageHelper.java b/services/core/java/com/android/server/pm/InstallPackageHelper.java
index 4f9eab9..ada79ae 100644
--- a/services/core/java/com/android/server/pm/InstallPackageHelper.java
+++ b/services/core/java/com/android/server/pm/InstallPackageHelper.java
@@ -2900,6 +2900,12 @@
// code is loaded by a new Activity before ApplicationInfo changes have
// propagated to all application threads.
mPm.scheduleDeferredNoKillPostDelete(args);
+ if (Flags.improveInstallDontKill()) {
+ synchronized (mPm.mInstallLock) {
+ PackageManagerServiceUtils.linkSplitsToOldDirs(mPm.mInstaller,
+ packageName, pkgSetting.getPath(), pkgSetting.getOldPaths());
+ }
+ }
} else {
mRemovePackageHelper.cleanUpResources(packageName, args.getCodeFile(),
args.getInstructionSets());
@@ -4211,8 +4217,10 @@
}
}
+ final long firstInstallTime = Flags.fixSystemAppsFirstInstallTime()
+ ? System.currentTimeMillis() : 0;
final ScanResult scanResult = scanPackageNewLI(parsedPackage, parseFlags,
- scanFlags | SCAN_UPDATE_SIGNATURE, 0 /* currentTime */, user, null);
+ scanFlags | SCAN_UPDATE_SIGNATURE, firstInstallTime, user, null);
return new Pair<>(scanResult, shouldHideSystemApp);
}
diff --git a/services/core/java/com/android/server/pm/LauncherAppsService.java b/services/core/java/com/android/server/pm/LauncherAppsService.java
index 68c95b1..b5346a3 100644
--- a/services/core/java/com/android/server/pm/LauncherAppsService.java
+++ b/services/core/java/com/android/server/pm/LauncherAppsService.java
@@ -18,10 +18,6 @@
import static android.Manifest.permission.READ_FRAME_BUFFER;
import static android.app.ActivityOptions.KEY_SPLASH_SCREEN_THEME;
-import static android.app.AppOpsManager.MODE_ALLOWED;
-import static android.app.AppOpsManager.MODE_IGNORED;
-import static android.app.AppOpsManager.OP_ARCHIVE_ICON_OVERLAY;
-import static android.app.AppOpsManager.OP_UNARCHIVAL_CONFIRMATION;
import static android.app.ComponentOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED;
import static android.app.ComponentOptions.MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED;
import static android.app.PendingIntent.FLAG_IMMUTABLE;
@@ -47,7 +43,6 @@
import android.app.ActivityManagerInternal;
import android.app.ActivityOptions;
import android.app.AppGlobals;
-import android.app.AppOpsManager;
import android.app.IApplicationThread;
import android.app.PendingIntent;
import android.app.admin.DevicePolicyCache;
@@ -218,7 +213,6 @@
private final ActivityTaskManagerInternal mActivityTaskManagerInternal;
private final ShortcutServiceInternal mShortcutServiceInternal;
private final PackageManagerInternal mPackageManagerInternal;
- private final AppOpsManager mAppOpsManager;
private final PackageCallbackList<IOnAppsChangedListener> mListeners
= new PackageCallbackList<IOnAppsChangedListener>();
private final DevicePolicyManager mDpm;
@@ -259,7 +253,6 @@
LocalServices.getService(ShortcutServiceInternal.class));
mPackageManagerInternal = Objects.requireNonNull(
LocalServices.getService(PackageManagerInternal.class));
- mAppOpsManager = mContext.getSystemService(AppOpsManager.class);
mShortcutServiceInternal.addListener(mPackageMonitor);
mShortcutChangeHandler = new ShortcutChangeHandler(mUserManagerInternal);
mShortcutServiceInternal.addShortcutChangeCallback(mShortcutChangeHandler);
@@ -1687,9 +1680,8 @@
mContext,
/* requestCode */ 0,
intent,
- PendingIntent.FLAG_ONE_SHOT
- | PendingIntent.FLAG_IMMUTABLE
- | PendingIntent.FLAG_CANCEL_CURRENT,
+ PendingIntent.FLAG_IMMUTABLE
+ | FLAG_UPDATE_CURRENT,
/* options */ null,
user);
return pi == null ? null : pi.getIntentSender();
@@ -2005,23 +1997,6 @@
}
}
- @Override
- public void setArchiveCompatibilityOptions(boolean enableIconOverlay,
- boolean enableUnarchivalConfirmation) {
- int callingUid = Binder.getCallingUid();
- Binder.withCleanCallingIdentity(
- () -> {
- mAppOpsManager.setUidMode(
- OP_ARCHIVE_ICON_OVERLAY,
- callingUid,
- enableIconOverlay ? MODE_ALLOWED : MODE_IGNORED);
- mAppOpsManager.setUidMode(
- OP_UNARCHIVAL_CONFIRMATION,
- callingUid,
- enableUnarchivalConfirmation ? MODE_ALLOWED : MODE_IGNORED);
- });
- }
-
/** Checks if user is a profile of or same as listeningUser.
* and the user is enabled. */
private boolean isEnabledProfileOf(UserHandle listeningUser, UserHandle user,
diff --git a/services/core/java/com/android/server/pm/PackageArchiver.java b/services/core/java/com/android/server/pm/PackageArchiver.java
index c1b3673..32f5646 100644
--- a/services/core/java/com/android/server/pm/PackageArchiver.java
+++ b/services/core/java/com/android/server/pm/PackageArchiver.java
@@ -20,7 +20,6 @@
import static android.app.ActivityManager.START_CLASS_NOT_FOUND;
import static android.app.ActivityManager.START_PERMISSION_DENIED;
import static android.app.ActivityManager.START_SUCCESS;
-import static android.app.AppOpsManager.MODE_ALLOWED;
import static android.app.AppOpsManager.MODE_IGNORED;
import static android.app.ComponentOptions.MODE_BACKGROUND_ACTIVITY_START_DENIED;
import static android.content.pm.ArchivedActivityInfo.bytesFromBitmap;
@@ -196,6 +195,7 @@
Computer snapshot = mPm.snapshotComputer();
int userId = userHandle.getIdentifier();
int binderUid = Binder.getCallingUid();
+ int binderPid = Binder.getCallingPid();
if (!PackageManagerServiceUtils.isSystemOrRootOrShell(binderUid)) {
verifyCaller(snapshot.getPackageUid(callerPackageName, 0, userId), binderUid);
}
@@ -230,7 +230,8 @@
DELETE_ARCHIVE | DELETE_KEEP_DATA,
intentSender,
userId,
- binderUid);
+ binderUid,
+ binderPid);
})
.exceptionally(
e -> {
@@ -274,12 +275,11 @@
Slog.i(TAG, TextUtils.formatSimple("Unarchival is starting for: %s", packageName));
try {
+ // TODO(b/311709794) Make showUnarchivalConfirmation dependent on the compat options.
requestUnarchive(packageName, callerPackageName,
getOrCreateLauncherListener(userId, packageName),
UserHandle.of(userId),
- getAppOpsManager().checkOp(
- AppOpsManager.OP_UNARCHIVAL_CONFIRMATION, callingUid, callerPackageName)
- == MODE_ALLOWED);
+ false /* showUnarchivalConfirmation= */);
} catch (Throwable t) {
Slog.e(TAG, TextUtils.formatSimple(
"Unexpected error occurred while unarchiving package %s: %s.", packageName,
@@ -796,8 +796,7 @@
* <p> The icon is returned without any treatment/overlay. In the rare case the app had multiple
* launcher activities, only one of the icons is returned arbitrarily.
*/
- public Bitmap getArchivedAppIcon(@NonNull String packageName, @NonNull UserHandle user,
- String callingPackageName) {
+ public Bitmap getArchivedAppIcon(@NonNull String packageName, @NonNull UserHandle user) {
Objects.requireNonNull(packageName);
Objects.requireNonNull(user);
@@ -820,13 +819,7 @@
// TODO(b/298452477) Handle monochrome icons.
// In the rare case the archived app defined more than two launcher activities, we choose
// the first one arbitrarily.
- Bitmap icon = decodeIcon(archiveState.getActivityInfos().get(0));
- if (getAppOpsManager().checkOp(
- AppOpsManager.OP_ARCHIVE_ICON_OVERLAY, callingUid, callingPackageName)
- == MODE_ALLOWED) {
- icon = includeCloudOverlay(icon);
- }
- return icon;
+ return includeCloudOverlay(decodeIcon(archiveState.getActivityInfos().get(0)));
}
/**
diff --git a/services/core/java/com/android/server/pm/PackageHandler.java b/services/core/java/com/android/server/pm/PackageHandler.java
index ee5875e..68f6ca1 100644
--- a/services/core/java/com/android/server/pm/PackageHandler.java
+++ b/services/core/java/com/android/server/pm/PackageHandler.java
@@ -88,6 +88,13 @@
final boolean didRestore = (msg.arg2 != 0);
mPm.mRunningInstalls.delete(msg.arg1);
+ if (request == null) {
+ if (DEBUG_INSTALL) {
+ Slog.i(TAG, "InstallRequest is null. Nothing to do for post-install "
+ + "token " + msg.arg1);
+ }
+ break;
+ }
request.closeFreezer();
request.onInstallCompleted();
request.runPostInstallRunnable();
@@ -116,10 +123,19 @@
}
} break;
case WRITE_SETTINGS: {
- mPm.writeSettings(/*sync=*/false);
+ if (!mPm.tryWriteSettings(/*sync=*/false)) {
+ // Failed to write.
+ this.removeMessages(WRITE_SETTINGS);
+ mPm.scheduleWriteSettings();
+ }
} break;
case WRITE_PACKAGE_LIST: {
- mPm.writePackageList(msg.arg1);
+ int userId = msg.arg1;
+ if (!mPm.tryWritePackageList(userId)) {
+ // Failed to write.
+ this.removeMessages(WRITE_PACKAGE_LIST);
+ mPm.scheduleWritePackageList(userId);
+ }
} break;
case CHECK_PENDING_VERIFICATION: {
final int verificationId = msg.arg1;
diff --git a/services/core/java/com/android/server/pm/PackageInstallerService.java b/services/core/java/com/android/server/pm/PackageInstallerService.java
index 7bf9fe7..c6d448d 100644
--- a/services/core/java/com/android/server/pm/PackageInstallerService.java
+++ b/services/core/java/com/android/server/pm/PackageInstallerService.java
@@ -788,6 +788,24 @@
}
}
+ if (Flags.recoverabilityDetection()) {
+ if (params.rollbackImpactLevel == PackageManager.ROLLBACK_USER_IMPACT_HIGH
+ || params.rollbackImpactLevel
+ == PackageManager.ROLLBACK_USER_IMPACT_ONLY_MANUAL) {
+ if ((params.installFlags & PackageManager.INSTALL_ENABLE_ROLLBACK) == 0) {
+ throw new IllegalArgumentException(
+ "Can't set rollbackImpactLevel when rollback is not enabled");
+ }
+ if (mContext.checkCallingOrSelfPermission(Manifest.permission.MANAGE_ROLLBACKS)
+ != PackageManager.PERMISSION_GRANTED) {
+ throw new SecurityException(
+ "Setting rollbackImpactLevel requires the MANAGE_ROLLBACKS permission");
+ }
+ } else if (params.rollbackImpactLevel < 0) {
+ throw new IllegalArgumentException("rollbackImpactLevel can't be negative.");
+ }
+ }
+
boolean isApex = (params.installFlags & PackageManager.INSTALL_APEX) != 0;
if (isApex) {
if (mContext.checkCallingOrSelfPermission(Manifest.permission.INSTALL_PACKAGE_UPDATES)
@@ -1387,11 +1405,12 @@
flags,
statusReceiver,
userId,
- Binder.getCallingUid());
+ Binder.getCallingUid(),
+ Binder.getCallingPid());
}
void uninstall(VersionedPackage versionedPackage, String callerPackageName, int flags,
- IntentSender statusReceiver, int userId, int callingUid) {
+ IntentSender statusReceiver, int userId, int callingUid, int callingPid) {
final Computer snapshot = mPm.snapshotComputer();
snapshot.enforceCrossUserPermission(callingUid, userId, true, true, "uninstall");
if (!PackageManagerServiceUtils.isRootOrShell(callingUid)) {
@@ -1408,7 +1427,7 @@
final PackageDeleteObserverAdapter adapter = new PackageDeleteObserverAdapter(mContext,
statusReceiver, versionedPackage.getPackageName(),
canSilentlyInstallPackage, userId, mPackageArchiver, flags);
- if (mContext.checkCallingOrSelfPermission(Manifest.permission.DELETE_PACKAGES)
+ if (mContext.checkPermission(Manifest.permission.DELETE_PACKAGES, callingPid, callingUid)
== PackageManager.PERMISSION_GRANTED) {
// Sweet, call straight through!
mPm.deletePackageVersioned(versionedPackage, adapter.getBinder(), userId, flags);
@@ -1428,8 +1447,8 @@
} else {
ApplicationInfo appInfo = snapshot.getApplicationInfo(callerPackageName, 0, userId);
if (appInfo.targetSdkVersion >= Build.VERSION_CODES.P) {
- mContext.enforceCallingOrSelfPermission(Manifest.permission.REQUEST_DELETE_PACKAGES,
- null);
+ mContext.enforcePermission(Manifest.permission.REQUEST_DELETE_PACKAGES, callingPid,
+ callingUid, null);
}
// Take a short detour to confirm with user
diff --git a/services/core/java/com/android/server/pm/PackageInstallerSession.java b/services/core/java/com/android/server/pm/PackageInstallerSession.java
index 117d03f..27c3dad 100644
--- a/services/core/java/com/android/server/pm/PackageInstallerSession.java
+++ b/services/core/java/com/android/server/pm/PackageInstallerSession.java
@@ -54,6 +54,7 @@
import static com.android.internal.util.XmlUtils.writeUriAttribute;
import static com.android.server.pm.PackageInstallerService.prepareStageDir;
import static com.android.server.pm.PackageManagerService.APP_METADATA_FILE_NAME;
+import static com.android.server.pm.PackageManagerService.DEFAULT_FILE_ACCESS_MODE;
import static com.android.server.pm.PackageManagerServiceUtils.isInstalledByAdb;
import static com.android.server.pm.PackageManagerShellCommandDataLoader.Metadata;
@@ -1294,6 +1295,7 @@
info.autoRevokePermissionsMode = params.autoRevokePermissionsMode;
info.installFlags = params.installFlags;
info.rollbackLifetimeMillis = params.rollbackLifetimeMillis;
+ info.rollbackImpactLevel = params.rollbackImpactLevel;
info.isMultiPackage = params.isMultiPackage;
info.isStaged = params.isStaged;
info.rollbackDataPolicy = params.rollbackDataPolicy;
@@ -1831,7 +1833,7 @@
try {
Os.link(path, sourcePath);
// Grant READ access for APK to be read successfully
- Os.chmod(sourcePath, 0644);
+ Os.chmod(sourcePath, DEFAULT_FILE_ACCESS_MODE);
} catch (ErrnoException e) {
e.rethrowAsIOException();
}
@@ -1900,7 +1902,8 @@
// If file is app metadata then set permission to 0640 to deny user read access since it
// might contain sensitive information.
- int mode = name.equals(APP_METADATA_FILE_NAME) ? APP_METADATA_FILE_ACCESS_MODE : 0644;
+ int mode = name.equals(APP_METADATA_FILE_NAME)
+ ? APP_METADATA_FILE_ACCESS_MODE : DEFAULT_FILE_ACCESS_MODE;
ParcelFileDescriptor targetPfd = openTargetInternal(target.getAbsolutePath(),
O_CREAT | O_WRONLY, mode);
Os.chmod(target.getAbsolutePath(), mode);
@@ -4245,7 +4248,7 @@
throw new IOException("Failed to copy " + fromFile + " to " + tmpFile);
}
try {
- Os.chmod(tmpFile.getAbsolutePath(), 0644);
+ Os.chmod(tmpFile.getAbsolutePath(), DEFAULT_FILE_ACCESS_MODE);
} catch (ErrnoException e) {
throw new IOException("Failed to chmod " + tmpFile);
}
diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java
index f09fa21..135bd4f 100644
--- a/services/core/java/com/android/server/pm/PackageManagerService.java
+++ b/services/core/java/com/android/server/pm/PackageManagerService.java
@@ -489,6 +489,9 @@
*/
static final long WATCHDOG_TIMEOUT = 1000*60*10; // ten minutes
+ // How long to wait for Lock in async writeSettings and writePackageList.
+ private static final long WRITE_LOCK_TIMEOUT_MS = 1000 * 10; // 10 seconds
+
/**
* Default IncFs timeouts. Maximum values in IncFs is 1hr.
*
@@ -593,6 +596,8 @@
static final String APP_METADATA_FILE_NAME = "app.metadata";
+ static final int DEFAULT_FILE_ACCESS_MODE = 0644;
+
final Handler mHandler;
final Handler mBackgroundHandler;
@@ -1562,7 +1567,7 @@
}
}
- private void scheduleWritePackageListLocked(int userId) {
+ void scheduleWritePackageList(int userId) {
invalidatePackageInfoCache();
if (!mHandler.hasMessages(WRITE_PACKAGE_LIST)) {
Message msg = mHandler.obtainMessage(WRITE_PACKAGE_LIST);
@@ -1614,22 +1619,41 @@
mSettings.writePackageRestrictions(dirtyUsers);
}
- void writeSettings(boolean sync) {
- synchronized (mLock) {
+ private boolean tryUnderLock(boolean sync, long timeoutMs, Runnable runnable) {
+ try {
+ if (sync) {
+ mLock.lock();
+ } else if (!mLock.tryLock(timeoutMs, TimeUnit.MILLISECONDS)) {
+ return false;
+ }
+ try {
+ runnable.run();
+ return true;
+ } finally {
+ mLock.unlock();
+ }
+ } catch (InterruptedException e) {
+ Slog.e(TAG, "Failed to obtain mLock", e);
+ }
+ return false;
+ }
+
+ boolean tryWriteSettings(boolean sync) {
+ return tryUnderLock(sync, WRITE_LOCK_TIMEOUT_MS, () -> {
mHandler.removeMessages(WRITE_SETTINGS);
mBackgroundHandler.removeMessages(WRITE_DIRTY_PACKAGE_RESTRICTIONS);
writeSettingsLPrTEMP(sync);
synchronized (mDirtyUsers) {
mDirtyUsers.clear();
}
- }
+ });
}
- void writePackageList(int userId) {
- synchronized (mLock) {
+ boolean tryWritePackageList(int userId) {
+ return tryUnderLock(/*sync=*/false, WRITE_LOCK_TIMEOUT_MS, () -> {
mHandler.removeMessages(WRITE_PACKAGE_LIST);
mSettings.writePackageListLPr(userId);
- }
+ });
}
private static final Handler.Callback BACKGROUND_HANDLER_CALLBACK = new Handler.Callback() {
@@ -3054,7 +3078,9 @@
if (mHandler.hasMessages(WRITE_SETTINGS)
|| mBackgroundHandler.hasMessages(WRITE_DIRTY_PACKAGE_RESTRICTIONS)
|| mHandler.hasMessages(WRITE_PACKAGE_LIST)) {
- writeSettings(/*sync=*/true);
+ while (!tryWriteSettings(/*sync=*/true)) {
+ Slog.wtf(TAG, "Failed to write settings on shutdown");
+ }
}
}
}
@@ -4334,11 +4360,11 @@
mDirtyUsers.remove(userId);
}
mUserNeedsBadging.delete(userId);
- mPermissionManager.onUserRemoved(userId);
+ mDeletePackageHelper.removeUnusedPackagesLPw(userManager, userId);
mSettings.removeUserLPw(userId);
mPendingBroadcasts.remove(userId);
- mDeletePackageHelper.removeUnusedPackagesLPw(userManager, userId);
mAppsFilter.onUserDeleted(snapshotComputer(), userId);
+ mPermissionManager.onUserRemoved(userId);
}
mInstantAppRegistry.onUserRemoved(userId);
mPackageMonitorCallbackHelper.onUserRemoved(userId);
@@ -4361,7 +4387,7 @@
}
synchronized (mLock) {
scheduleWritePackageRestrictions(userId);
- scheduleWritePackageListLocked(userId);
+ scheduleWritePackageList(userId);
mAppsFilter.onUserCreated(snapshotComputer(), userId);
}
}
@@ -6388,10 +6414,8 @@
}
@Override
- public Bitmap getArchivedAppIcon(@NonNull String packageName, @NonNull UserHandle user,
- @NonNull String callingPackageName) {
- return mInstallerService.mPackageArchiver.getArchivedAppIcon(packageName, user,
- callingPackageName);
+ public Bitmap getArchivedAppIcon(@NonNull String packageName, @NonNull UserHandle user) {
+ return mInstallerService.mPackageArchiver.getArchivedAppIcon(packageName, user);
}
@Override
diff --git a/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java b/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java
index cd34163..8531692 100644
--- a/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java
+++ b/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java
@@ -30,6 +30,7 @@
import static com.android.server.pm.PackageManagerService.DEBUG_COMPRESSION;
import static com.android.server.pm.PackageManagerService.DEBUG_INTENT_MATCHING;
import static com.android.server.pm.PackageManagerService.DEBUG_PREFERRED;
+import static com.android.server.pm.PackageManagerService.DEFAULT_FILE_ACCESS_MODE;
import static com.android.server.pm.PackageManagerService.RANDOM_CODEPATH_PREFIX;
import static com.android.server.pm.PackageManagerService.RANDOM_DIR_PREFIX;
import static com.android.server.pm.PackageManagerService.SHELL_PACKAGE_NAME;
@@ -69,6 +70,7 @@
import android.os.Environment;
import android.os.FileUtils;
import android.os.Process;
+import android.os.SELinux;
import android.os.SystemProperties;
import android.os.UserHandle;
import android.os.incremental.IncrementalManager;
@@ -129,10 +131,12 @@
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
import java.text.SimpleDateFormat;
+import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.Objects;
+import java.util.Set;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.zip.GZIPInputStream;
@@ -853,7 +857,7 @@
FileUtils.copy(fileIn, outputStream);
// Flush anything in buffer before chmod, because any writes after chmod will fail.
outputStream.flush();
- Os.fchmod(outputStream.getFD(), 0644);
+ Os.fchmod(outputStream.getFD(), DEFAULT_FILE_ACCESS_MODE);
atomicFile.finishWrite(outputStream);
return PackageManager.INSTALL_SUCCEEDED;
} catch (IOException e) {
@@ -1081,8 +1085,8 @@
final File targetFile = new File(targetDir, targetName);
final FileDescriptor targetFd = Os.open(targetFile.getAbsolutePath(),
- O_RDWR | O_CREAT, 0644);
- Os.chmod(targetFile.getAbsolutePath(), 0644);
+ O_RDWR | O_CREAT, DEFAULT_FILE_ACCESS_MODE);
+ Os.chmod(targetFile.getAbsolutePath(), DEFAULT_FILE_ACCESS_MODE);
FileInputStream source = null;
try {
source = new FileInputStream(sourcePath);
@@ -1552,4 +1556,72 @@
public static boolean isInstalledByAdb(String initiatingPackageName) {
return initiatingPackageName == null || SHELL_PACKAGE_NAME.equals(initiatingPackageName);
}
+
+ public static void linkSplitsToOldDirs(@NonNull Installer installer,
+ @NonNull String packageName,
+ @NonNull File newPath,
+ @Nullable Set<File> oldPaths) {
+ if (oldPaths == null || oldPaths.isEmpty()) {
+ return;
+ }
+ if (IncrementalManager.isIncrementalPath(newPath.getPath())) {
+ //TODO(b/291212866): handle incremental installs
+ return;
+ }
+ final File[] filesInNewPath = newPath.listFiles();
+ if (filesInNewPath == null || filesInNewPath.length == 0) {
+ return;
+ }
+ final List<String> splitApkNames = new ArrayList<String>();
+ for (int i = 0; i < filesInNewPath.length; i++) {
+ if (!filesInNewPath[i].isDirectory() && filesInNewPath[i].toString().endsWith(".apk")) {
+ splitApkNames.add(filesInNewPath[i].getName());
+ }
+ }
+ final int numSplits = splitApkNames.size();
+ if (numSplits == 0) {
+ return;
+ }
+ for (File oldPath : oldPaths) {
+ if (!oldPath.exists()) {
+ continue;
+ }
+ for (int i = 0; i < numSplits; i++) {
+ final String splitApkName = splitApkNames.get(i);
+ final File linkedSplit = new File(oldPath, splitApkName);
+ if (linkedSplit.exists()) {
+ if (DEBUG) {
+ Slog.d(PackageManagerService.TAG, "Skipping existing linked split <"
+ + linkedSplit + ">");
+ }
+ continue;
+ }
+ final File sourceSplit = new File(newPath, splitApkName);
+ try {
+ installer.linkFile(packageName, splitApkName,
+ newPath.getAbsolutePath(), oldPath.getAbsolutePath());
+ if (DEBUG) {
+ Slog.d(PackageManagerService.TAG, "Linked <"
+ + sourceSplit + "> to <" + linkedSplit + ">");
+ }
+ } catch (Installer.InstallerException e) {
+ Slog.w(PackageManagerService.TAG, "Failed to link split <"
+ + sourceSplit + " > to <" + linkedSplit + ">", e);
+ continue;
+ }
+ try {
+ Os.chmod(linkedSplit.getAbsolutePath(), DEFAULT_FILE_ACCESS_MODE);
+ } catch (ErrnoException e) {
+ Slog.w(PackageManagerService.TAG, "Failed to set mode for linked split <"
+ + linkedSplit + ">", e);
+ continue;
+ }
+ if (!SELinux.restorecon(linkedSplit)) {
+ Slog.w(PackageManagerService.TAG, "Failed to restorecon for linked split <"
+ + linkedSplit + ">");
+ }
+ }
+ }
+ //TODO(b/291212866): support native libs
+ }
}
diff --git a/services/core/java/com/android/server/pm/PackageManagerShellCommand.java b/services/core/java/com/android/server/pm/PackageManagerShellCommand.java
index ca00c84..5c9c8c6 100644
--- a/services/core/java/com/android/server/pm/PackageManagerShellCommand.java
+++ b/services/core/java/com/android/server/pm/PackageManagerShellCommand.java
@@ -28,6 +28,7 @@
import static android.content.pm.PackageManager.RESTRICTION_NONE;
import static com.android.server.LocalManagerRegistry.ManagerNotFoundException;
+import static com.android.server.pm.PackageManagerService.DEFAULT_FILE_ACCESS_MODE;
import static com.android.server.pm.PackageManagerService.PLATFORM_PACKAGE_NAME;
import android.accounts.IAccountManager;
@@ -297,6 +298,8 @@
return runSetHiddenSetting(true);
case "unhide":
return runSetHiddenSetting(false);
+ case "unstop":
+ return runSetStoppedState(false);
case "suspend":
return runSuspend(true, 0);
case "suspend-quarantine":
@@ -2347,7 +2350,7 @@
Streams.copy(inStream, outStream);
}
// Give read permissions to the other group.
- Os.chmod(outputProfilePath, /*mode*/ 0644 );
+ Os.chmod(outputProfilePath, /*mode*/ DEFAULT_FILE_ACCESS_MODE);
} catch (IOException | ErrnoException e) {
pw.println("Error when reading the profile fd: " + e.getMessage());
e.printStackTrace(pw);
@@ -2662,6 +2665,26 @@
return 0;
}
+ private int runSetStoppedState(boolean state) throws RemoteException {
+ int userId = UserHandle.USER_SYSTEM;
+ String option = getNextOption();
+ if (option != null && option.equals("--user")) {
+ userId = UserHandle.parseUserArg(getNextArgRequired());
+ }
+
+ String pkg = getNextArg();
+ if (pkg == null) {
+ getErrPrintWriter().println("Error: no package specified");
+ return 1;
+ }
+ final int translatedUserId =
+ translateUserId(userId, UserHandle.USER_NULL, "runSetStoppedState");
+ mInterface.setPackageStoppedState(pkg, state, translatedUserId);
+ getOutPrintWriter().println("Package " + pkg + " new stopped state: "
+ + mInterface.isPackageStoppedForUser(pkg, translatedUserId));
+ return 0;
+ }
+
private int runSetDistractingRestriction() {
final PrintWriter pw = getOutPrintWriter();
int userId = UserHandle.USER_SYSTEM;
@@ -3694,7 +3717,19 @@
// remember to set it themselves.
params.installerPackageName = "com.android.shell";
}
- sessionParams.installFlags |= PackageManager.INSTALL_ENABLE_ROLLBACK;
+ int rollbackStrategy = PackageManager.ROLLBACK_DATA_POLICY_RESTORE;
+ try {
+ rollbackStrategy = Integer.parseInt(peekNextArg());
+ if (rollbackStrategy < PackageManager.ROLLBACK_DATA_POLICY_RESTORE
+ || rollbackStrategy > PackageManager.ROLLBACK_DATA_POLICY_RETAIN) {
+ throw new IllegalArgumentException(
+ rollbackStrategy + " is not a valid rollback data policy.");
+ }
+ getNextArg(); // pop the argument
+ } catch (NumberFormatException e) {
+ // not followed by a number assume ROLLBACK_DATA_POLICY_RESTORE.
+ }
+ sessionParams.setEnableRollback(true, rollbackStrategy);
break;
case "--staged-ready-timeout":
params.stagedReadyTimeoutMs = Long.parseLong(getNextArgRequired());
@@ -3729,6 +3764,11 @@
} else if (staged) {
sessionParams.setStaged();
}
+ if ((sessionParams.installFlags & PackageManager.INSTALL_APEX) != 0
+ && (sessionParams.installFlags & PackageManager.INSTALL_ENABLE_ROLLBACK) != 0
+ && sessionParams.rollbackDataPolicy == PackageManager.ROLLBACK_DATA_POLICY_WIPE) {
+ throw new IllegalArgumentException("Data policy 'wipe' is not supported for apex.");
+ }
return params;
}
@@ -4807,7 +4847,7 @@
pw.println(" [--install-reason 0/1/2/3/4] [--originating-uri URI]");
pw.println(" [--referrer URI] [--abi ABI_NAME] [--force-sdk]");
pw.println(" [--preload] [--instant] [--full] [--dont-kill]");
- pw.println(" [--enable-rollback]");
+ pw.println(" [--enable-rollback [0/1/2]]");
pw.println(" [--force-uuid internal|UUID] [--pkg PACKAGE] [-S BYTES]");
pw.println(" [--apex] [--non-staged] [--force-non-staged]");
pw.println(" [--staged-ready-timeout TIMEOUT] [--ignore-dexopt-profile]");
@@ -4831,6 +4871,8 @@
pw.println(" --abi: override the default ABI of the platform");
pw.println(" --instant: cause the app to be installed as an ephemeral install app");
pw.println(" --full: cause the app to be installed as a non-ephemeral full app");
+ pw.println(" --enable-rollback: enable rollbacks for the upgrade.");
+ pw.println(" 0=restore (default), 1=wipe, 2=retain");
pw.println(" --install-location: force the install location:");
pw.println(" 0=auto, 1=internal only, 2=prefer external");
pw.println(" --install-reason: indicates why the app is being installed:");
@@ -4934,6 +4976,8 @@
pw.println(" hide [--user USER_ID] PACKAGE_OR_COMPONENT");
pw.println(" unhide [--user USER_ID] PACKAGE_OR_COMPONENT");
pw.println("");
+ pw.println(" unstop [--user USER_ID] PACKAGE");
+ pw.println("");
pw.println(" suspend [--user USER_ID] PACKAGE [PACKAGE...]");
pw.println(" Suspends the specified package(s) (as user).");
pw.println("");
diff --git a/services/core/java/com/android/server/pm/PackageManagerTracedLock.java b/services/core/java/com/android/server/pm/PackageManagerTracedLock.java
index e15e8a8..75e1803f 100644
--- a/services/core/java/com/android/server/pm/PackageManagerTracedLock.java
+++ b/services/core/java/com/android/server/pm/PackageManagerTracedLock.java
@@ -16,9 +16,11 @@
package com.android.server.pm;
+import java.util.concurrent.locks.ReentrantLock;
+
/**
* This is a unique class that is used as the PackageManager lock. It can be targeted for lock
* injection, similar to {@link ActivityManagerGlobalLock}.
*/
-public class PackageManagerTracedLock {
+public class PackageManagerTracedLock extends ReentrantLock {
}
diff --git a/services/core/java/com/android/server/pm/PreferredComponent.java b/services/core/java/com/android/server/pm/PreferredComponent.java
index 18caafd..f3b1464 100644
--- a/services/core/java/com/android/server/pm/PreferredComponent.java
+++ b/services/core/java/com/android/server/pm/PreferredComponent.java
@@ -28,7 +28,8 @@
import com.android.modules.utils.TypedXmlPullParser;
import com.android.modules.utils.TypedXmlSerializer;
import com.android.server.LocalServices;
-import com.android.server.pm.pkg.PackageUserState;
+import com.android.server.pm.pkg.PackageStateInternal;
+import com.android.server.pm.pkg.PackageUserStateInternal;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
@@ -218,11 +219,15 @@
continue;
}
- // Avoid showing the disambiguation dialog if the package which is installed with
- // reason INSTALL_REASON_DEVICE_SETUP.
- final PackageUserState pkgUserState =
- pmi.getPackageStateInternal(ai.packageName).getUserStates().get(userId);
- if (pkgUserState != null && pkgUserState.getInstallReason()
+ // Avoid showing the disambiguation dialog if the package is not installed or
+ // installed with reason INSTALL_REASON_DEVICE_SETUP.
+ final PackageStateInternal ps = pmi.getPackageStateInternal(ai.packageName);
+ if (ps == null) {
+ continue;
+ }
+ final PackageUserStateInternal pkgUserState = ps.getUserStates().get(userId);
+ if (pkgUserState == null
+ || pkgUserState.getInstallReason()
== PackageManager.INSTALL_REASON_DEVICE_SETUP) {
continue;
}
diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java
index a6598d6..c0596bb 100644
--- a/services/core/java/com/android/server/pm/UserManagerService.java
+++ b/services/core/java/com/android/server/pm/UserManagerService.java
@@ -295,8 +295,6 @@
private static final int USER_VERSION = 11;
- private static final int MAX_USER_STRING_LENGTH = 500;
-
private static final long EPOCH_PLUS_30_YEARS = 30L * 365 * 24 * 60 * 60 * 1000L; // ms
static final int WRITE_USER_MSG = 1;
@@ -4692,16 +4690,18 @@
if (userData.persistSeedData) {
if (userData.seedAccountName != null) {
serializer.attribute(null, ATTR_SEED_ACCOUNT_NAME,
- truncateString(userData.seedAccountName));
+ truncateString(userData.seedAccountName,
+ UserManager.MAX_ACCOUNT_STRING_LENGTH));
}
if (userData.seedAccountType != null) {
serializer.attribute(null, ATTR_SEED_ACCOUNT_TYPE,
- truncateString(userData.seedAccountType));
+ truncateString(userData.seedAccountType,
+ UserManager.MAX_ACCOUNT_STRING_LENGTH));
}
}
if (userInfo.name != null) {
serializer.startTag(null, TAG_NAME);
- serializer.text(truncateString(userInfo.name));
+ serializer.text(truncateString(userInfo.name, UserManager.MAX_USER_NAME_LENGTH));
serializer.endTag(null, TAG_NAME);
}
synchronized (mRestrictionsLock) {
@@ -4765,11 +4765,11 @@
serializer.endDocument();
}
- private String truncateString(String original) {
- if (original == null || original.length() <= MAX_USER_STRING_LENGTH) {
+ private String truncateString(String original, int limit) {
+ if (original == null || original.length() <= limit) {
return original;
}
- return original.substring(0, MAX_USER_STRING_LENGTH);
+ return original.substring(0, limit);
}
/*
@@ -5236,7 +5236,7 @@
@UserIdInt int parentId, boolean preCreate, @Nullable String[] disallowedPackages,
@NonNull TimingsTraceAndSlog t, @Nullable Object token)
throws UserManager.CheckedUserOperationException {
- String truncatedName = truncateString(name);
+ String truncatedName = truncateString(name, UserManager.MAX_USER_NAME_LENGTH);
final UserTypeDetails userTypeDetails = mUserTypes.get(userType);
if (userTypeDetails == null) {
throwCheckedUserOperationException(
@@ -6821,9 +6821,14 @@
Slog.e(LOG_TAG, "No such user for settings seed data u=" + userId);
return;
}
- userData.seedAccountName = truncateString(accountName);
- userData.seedAccountType = truncateString(accountType);
- userData.seedAccountOptions = accountOptions;
+ userData.seedAccountName = truncateString(accountName,
+ UserManager.MAX_ACCOUNT_STRING_LENGTH);
+ userData.seedAccountType = truncateString(accountType,
+ UserManager.MAX_ACCOUNT_STRING_LENGTH);
+ if (accountOptions != null && accountOptions.isBundleContentsWithinLengthLimit(
+ UserManager.MAX_ACCOUNT_OPTIONS_LENGTH)) {
+ userData.seedAccountOptions = accountOptions;
+ }
userData.persistSeedData = persist;
}
if (persist) {
diff --git a/services/core/java/com/android/server/pm/permission/PermissionAllowlist.java b/services/core/java/com/android/server/pm/permission/PermissionAllowlist.java
index 3efac81..d138606 100644
--- a/services/core/java/com/android/server/pm/permission/PermissionAllowlist.java
+++ b/services/core/java/com/android/server/pm/permission/PermissionAllowlist.java
@@ -26,6 +26,7 @@
public final class PermissionAllowlist {
@NonNull
private final ArrayMap<String, ArrayMap<String, Boolean>> mOemAppAllowlist = new ArrayMap<>();
+
@NonNull
private final ArrayMap<String, ArrayMap<String, Boolean>> mPrivilegedAppAllowlist =
new ArrayMap<>();
@@ -43,6 +44,19 @@
mApexPrivilegedAppAllowlists = new ArrayMap<>();
@NonNull
+ private final ArrayMap<String, ArrayMap<String, Boolean>> mSignatureAppAllowlist =
+ new ArrayMap<>();
+ @NonNull
+ private final ArrayMap<String, ArrayMap<String, Boolean>> mVendorSignatureAppAllowlist =
+ new ArrayMap<>();
+ @NonNull
+ private final ArrayMap<String, ArrayMap<String, Boolean>> mProductSignatureAppAllowlist =
+ new ArrayMap<>();
+ @NonNull
+ private final ArrayMap<String, ArrayMap<String, Boolean>> mSystemExtSignatureAppAllowlist =
+ new ArrayMap<>();
+
+ @NonNull
public ArrayMap<String, ArrayMap<String, Boolean>> getOemAppAllowlist() {
return mOemAppAllowlist;
}
@@ -73,6 +87,26 @@
return mApexPrivilegedAppAllowlists;
}
+ @NonNull
+ public ArrayMap<String, ArrayMap<String, Boolean>> getSignatureAppAllowlist() {
+ return mSignatureAppAllowlist;
+ }
+
+ @NonNull
+ public ArrayMap<String, ArrayMap<String, Boolean>> getVendorSignatureAppAllowlist() {
+ return mVendorSignatureAppAllowlist;
+ }
+
+ @NonNull
+ public ArrayMap<String, ArrayMap<String, Boolean>> getProductSignatureAppAllowlist() {
+ return mProductSignatureAppAllowlist;
+ }
+
+ @NonNull
+ public ArrayMap<String, ArrayMap<String, Boolean>> getSystemExtSignatureAppAllowlist() {
+ return mSystemExtSignatureAppAllowlist;
+ }
+
@Nullable
public Boolean getOemAppAllowlistState(@NonNull String packageName,
@NonNull String permissionName) {
@@ -137,4 +171,44 @@
}
return permissions.get(permissionName);
}
+
+ @Nullable
+ public Boolean getSignatureAppAllowlistState(@NonNull String packageName,
+ @NonNull String permissionName) {
+ ArrayMap<String, Boolean> permissions = mSignatureAppAllowlist.get(packageName);
+ if (permissions == null) {
+ return null;
+ }
+ return permissions.get(permissionName);
+ }
+
+ @Nullable
+ public Boolean getVendorSignatureAppAllowlistState(@NonNull String packageName,
+ @NonNull String permissionName) {
+ ArrayMap<String, Boolean> permissions = mVendorSignatureAppAllowlist.get(packageName);
+ if (permissions == null) {
+ return null;
+ }
+ return permissions.get(permissionName);
+ }
+
+ @Nullable
+ public Boolean getProductSignatureAppAllowlistState(@NonNull String packageName,
+ @NonNull String permissionName) {
+ ArrayMap<String, Boolean> permissions = mProductSignatureAppAllowlist.get(packageName);
+ if (permissions == null) {
+ return null;
+ }
+ return permissions.get(permissionName);
+ }
+
+ @Nullable
+ public Boolean getSystemExtSignatureAppAllowlistState(@NonNull String packageName,
+ @NonNull String permissionName) {
+ ArrayMap<String, Boolean> permissions = mSystemExtSignatureAppAllowlist.get(packageName);
+ if (permissions == null) {
+ return null;
+ }
+ return permissions.get(permissionName);
+ }
}
diff --git a/services/core/java/com/android/server/pm/pkg/PackageStateInternal.java b/services/core/java/com/android/server/pm/pkg/PackageStateInternal.java
index c737283..f7603b5 100644
--- a/services/core/java/com/android/server/pm/pkg/PackageStateInternal.java
+++ b/services/core/java/com/android/server/pm/pkg/PackageStateInternal.java
@@ -27,6 +27,8 @@
import com.android.server.pm.PackageKeySetData;
import com.android.server.pm.permission.LegacyPermissionState;
+import java.io.File;
+import java.util.Set;
import java.util.UUID;
/**
@@ -111,4 +113,7 @@
*/
@Nullable
String getAppMetadataFilePath();
+
+ @Nullable
+ Set<File> getOldPaths();
}
diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java
index bf669fb..1fdcc64 100644
--- a/services/core/java/com/android/server/policy/PhoneWindowManager.java
+++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java
@@ -2185,6 +2185,10 @@
TalkbackShortcutController getTalkbackShortcutController() {
return new TalkbackShortcutController(mContext);
}
+
+ WindowWakeUpPolicy getWindowWakeUpPolicy() {
+ return new WindowWakeUpPolicy(mContext);
+ }
}
/** {@inheritDoc} */
@@ -2433,7 +2437,7 @@
com.android.internal.R.integer.config_keyguardDrawnTimeout);
mKeyguardDelegate = injector.getKeyguardServiceDelegate();
mTalkbackShortcutController = injector.getTalkbackShortcutController();
- mWindowWakeUpPolicy = new WindowWakeUpPolicy(mContext);
+ mWindowWakeUpPolicy = injector.getWindowWakeUpPolicy();
initKeyCombinationRules();
initSingleKeyGestureRules(injector.getLooper());
mButtonOverridePermissionChecker = injector.getButtonOverridePermissionChecker();
@@ -5634,7 +5638,7 @@
Trace.asyncTraceBegin(Trace.TRACE_TAG_WINDOW_MANAGER, "screenTurningOn",
0 /* cookie */);
updateScreenOffSleepToken(false /* acquire */, false /* isSwappingDisplay */);
- mDefaultDisplayPolicy.screenTurnedOn(screenOnListener);
+ mDefaultDisplayPolicy.screenTurningOn(screenOnListener);
mBootAnimationDismissable = false;
synchronized (mLock) {
@@ -5676,6 +5680,7 @@
mKeyguardDelegate.onScreenTurnedOn();
}
}
+ mDefaultDisplayPolicy.screenTurnedOn();
reportScreenStateToVrManager(true);
}
diff --git a/services/core/java/com/android/server/policy/role/RoleServicePlatformHelperImpl.java b/services/core/java/com/android/server/policy/role/RoleServicePlatformHelperImpl.java
index 5e8b4de..7808c4e 100644
--- a/services/core/java/com/android/server/policy/role/RoleServicePlatformHelperImpl.java
+++ b/services/core/java/com/android/server/policy/role/RoleServicePlatformHelperImpl.java
@@ -30,6 +30,7 @@
import android.content.pm.ResolveInfo;
import android.content.pm.Signature;
import android.os.Environment;
+import android.permission.flags.Flags;
import android.provider.Settings;
import android.text.TextUtils;
import android.util.ArrayMap;
@@ -368,6 +369,7 @@
dataOutputStream.writeUTF(profileOwner);
dataOutputStream.writeInt(Settings.Global.getInt(mContext.getContentResolver(),
Settings.Global.DEVICE_DEMO_MODE, 0));
+ dataOutputStream.writeBoolean(Flags.walletRoleEnabled());
dataOutputStream.flush();
} catch (IOException e) {
// Never happens for MessageDigestOutputStream and DataOutputStream.
diff --git a/services/core/java/com/android/server/power/ShutdownThread.java b/services/core/java/com/android/server/power/ShutdownThread.java
index 871e98b..4bf8a78 100644
--- a/services/core/java/com/android/server/power/ShutdownThread.java
+++ b/services/core/java/com/android/server/power/ShutdownThread.java
@@ -319,6 +319,11 @@
pd.setMax(100);
pd.setProgress(0);
pd.setIndeterminate(false);
+ boolean showPercent = context.getResources().getBoolean(
+ com.android.internal.R.bool.config_showPercentageTextDuringRebootToUpdate);
+ if (!showPercent) {
+ pd.setProgressPercentFormat(null);
+ }
pd.setProgressNumberFormat(null);
pd.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
pd.setMessage(context.getText(
@@ -911,4 +916,4 @@
com.android.internal.R.string.config_defaultShutdownVibrationFile);
}
}
-}
\ No newline at end of file
+}
diff --git a/services/core/java/com/android/server/power/feature/power_flags.aconfig b/services/core/java/com/android/server/power/feature/power_flags.aconfig
index c8c16db..f5dfb5c 100644
--- a/services/core/java/com/android/server/power/feature/power_flags.aconfig
+++ b/services/core/java/com/android/server/power/feature/power_flags.aconfig
@@ -4,7 +4,7 @@
flag {
name: "enable_early_screen_timeout_detector"
- namespace: "power_manager"
+ namespace: "power"
description: "Feature flag for Early Screen Timeout detector"
bug: "309861917"
is_fixed_read_only: true
diff --git a/services/core/java/com/android/server/rollback/README.md b/services/core/java/com/android/server/rollback/README.md
index 08800da..f6bcbd0 100644
--- a/services/core/java/com/android/server/rollback/README.md
+++ b/services/core/java/com/android/server/rollback/README.md
@@ -187,13 +187,22 @@
### Installing an App with Rollback Enabled
-The `adb install` command accepts the `--enable-rollback` flag to install an app
+The `adb install` command accepts the `--enable-rollback [0/1/2]` flag to install an app
with rollback enabled. For example:
```
$ adb install --enable-rollback FooV2.apk
```
+The default rollback data policy is `ROLLBACK_DATA_POLICY_RESTORE` (0). To use
+a different `RollbackDataPolicy`, like `ROLLBACK_DATA_POLICY_RETAIN` (1) or
+`ROLLBACK_DATA_POLICY_WIPE` (2), provide the int value after
+`--enable-rollback`. For example:
+
+```
+$ adb install --enable-rollback 1 FooV2.apk
+```
+
### Triggering Rollback Manually
If rollback is available for an application, the pm command can be used to
@@ -217,7 +226,7 @@
-state: committed
-timestamp: 2019-04-23T14:57:35.944Z
-packages:
- com.android.tests.rollback.testapp.B 2 -> 1
+ com.android.tests.rollback.testapp.B 2 -> 1 [0]
-causePackages:
-committedSessionId: 1845805640
649899517:
@@ -225,7 +234,7 @@
-timestamp: 2019-04-23T12:55:21.342Z
-stagedSessionId: 343374391
-packages:
- com.android.tests.rollback.testapex 2 -> 1
+ com.android.tests.rollback.testapex 2 -> 1 [0]
-causePackages:
-committedSessionId: 2096717281
```
@@ -233,7 +242,8 @@
The example above shows two recently committed rollbacks. The update of
com.android.tests.rollback.testapp.B from version 1 to version 2 was rolled
back, and the update of com.android.tests.rollback.testapex from version 1 to
-version 2 was rolled back.
+version 2 was rolled back. For each package the value inside '[' and ']'
+indicates the `RollbackDataPolicy` for the rollback back.
The state is 'available' or 'committed'. The timestamp gives the time when the
rollback was first made available. If a stagedSessionId is present, then the
diff --git a/services/core/java/com/android/server/rollback/Rollback.java b/services/core/java/com/android/server/rollback/Rollback.java
index a5b90f1..d1f91c8 100644
--- a/services/core/java/com/android/server/rollback/Rollback.java
+++ b/services/core/java/com/android/server/rollback/Rollback.java
@@ -215,7 +215,8 @@
/* packages */ new ArrayList<>(),
/* isStaged */ isStaged,
/* causePackages */ new ArrayList<>(),
- /* committedSessionId */ -1);
+ /* committedSessionId */ -1,
+ /* rollbackImpactLevel */ PackageManager.ROLLBACK_USER_IMPACT_LOW);
mUserId = userId;
mInstallerPackageName = installerPackageName;
mBackupDir = backupDir;
@@ -394,7 +395,8 @@
*/
@WorkerThread
boolean enableForPackage(String packageName, long newVersion, long installedVersion,
- boolean isApex, String sourceDir, String[] splitSourceDirs, int rollbackDataPolicy) {
+ boolean isApex, String sourceDir, String[] splitSourceDirs, int rollbackDataPolicy,
+ @PackageManager.RollbackImpactLevel int rollbackImpactLevel) {
assertInWorkerThread();
try {
RollbackStore.backupPackageCodePath(this, packageName, sourceDir);
@@ -415,6 +417,10 @@
isApex, false /* isApkInApex */, new ArrayList<>(), rollbackDataPolicy);
info.getPackages().add(packageRollbackInfo);
+
+ if (info.getRollbackImpactLevel() < rollbackImpactLevel) {
+ info.setRollbackImpactLevel(rollbackImpactLevel);
+ }
return true;
}
@@ -961,7 +967,8 @@
for (PackageRollbackInfo pkg : info.getPackages()) {
ipw.println(pkg.getPackageName()
+ " " + pkg.getVersionRolledBackFrom().getLongVersionCode()
- + " -> " + pkg.getVersionRolledBackTo().getLongVersionCode());
+ + " -> " + pkg.getVersionRolledBackTo().getLongVersionCode()
+ + " [" + pkg.getRollbackDataPolicy() + "]");
}
ipw.decreaseIndent();
if (isCommitted()) {
diff --git a/services/core/java/com/android/server/rollback/RollbackManagerServiceImpl.java b/services/core/java/com/android/server/rollback/RollbackManagerServiceImpl.java
index 13f1141..359678b 100644
--- a/services/core/java/com/android/server/rollback/RollbackManagerServiceImpl.java
+++ b/services/core/java/com/android/server/rollback/RollbackManagerServiceImpl.java
@@ -954,7 +954,7 @@
ApplicationInfo appInfo = pkgInfo.applicationInfo;
return rollback.enableForPackage(packageName, newPackage.getVersionCode(),
pkgInfo.getLongVersionCode(), isApex, appInfo.sourceDir,
- appInfo.splitSourceDirs, rollbackDataPolicy);
+ appInfo.splitSourceDirs, rollbackDataPolicy, session.rollbackImpactLevel);
}
@ExtThread
diff --git a/services/core/java/com/android/server/rollback/RollbackStore.java b/services/core/java/com/android/server/rollback/RollbackStore.java
index 0af137f..14539d5 100644
--- a/services/core/java/com/android/server/rollback/RollbackStore.java
+++ b/services/core/java/com/android/server/rollback/RollbackStore.java
@@ -193,16 +193,27 @@
json.put("isStaged", rollback.isStaged());
json.put("causePackages", versionedPackagesToJson(rollback.getCausePackages()));
json.put("committedSessionId", rollback.getCommittedSessionId());
+ if (Flags.recoverabilityDetection()) {
+ json.put("rollbackImpactLevel", rollback.getRollbackImpactLevel());
+ }
return json;
}
private static RollbackInfo rollbackInfoFromJson(JSONObject json) throws JSONException {
- return new RollbackInfo(
+ RollbackInfo rollbackInfo = new RollbackInfo(
json.getInt("rollbackId"),
packageRollbackInfosFromJson(json.getJSONArray("packages")),
json.getBoolean("isStaged"),
versionedPackagesFromJson(json.getJSONArray("causePackages")),
json.getInt("committedSessionId"));
+
+ if (Flags.recoverabilityDetection()) {
+ // to make it backward compatible.
+ rollbackInfo.setRollbackImpactLevel(json.optInt("rollbackImpactLevel",
+ PackageManager.ROLLBACK_USER_IMPACT_LOW));
+ }
+
+ return rollbackInfo;
}
/**
diff --git a/services/core/java/com/android/server/selinux/QuotaLimiter.java b/services/core/java/com/android/server/selinux/QuotaLimiter.java
new file mode 100644
index 0000000..e89ddfd
--- /dev/null
+++ b/services/core/java/com/android/server/selinux/QuotaLimiter.java
@@ -0,0 +1,78 @@
+/*
+ * 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.server.selinux;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.os.Clock;
+
+import java.time.Duration;
+import java.time.Instant;
+
+/**
+ * A QuotaLimiter allows to define a maximum number of Atom pushes within a specific time window.
+ *
+ * <p>The limiter divides the time line in windows of a fixed size. Every time a new permit is
+ * requested, the limiter checks whether the previous request was in the same time window as the
+ * current one. If the two windows are the same, it grants a permit only if the number of permits
+ * granted within the window does not exceed the quota. If the two windows are different, it resets
+ * the quota.
+ */
+public class QuotaLimiter {
+
+ private final Clock mClock;
+ private final Duration mWindowSize;
+ private final int mMaxPermits;
+
+ private long mCurrentWindow = 0;
+ private int mPermitsGranted = 0;
+
+ @VisibleForTesting
+ QuotaLimiter(Clock clock, Duration windowSize, int maxPermits) {
+ mClock = clock;
+ mWindowSize = windowSize;
+ mMaxPermits = maxPermits;
+ }
+
+ public QuotaLimiter(Duration windowSize, int maxPermits) {
+ this(Clock.SYSTEM_CLOCK, windowSize, maxPermits);
+ }
+
+ public QuotaLimiter(int maxPermitsPerDay) {
+ this(Clock.SYSTEM_CLOCK, Duration.ofDays(1), maxPermitsPerDay);
+ }
+
+ /**
+ * Acquires a permit if there is one available in the current time window.
+ *
+ * @return true if a permit was acquired.
+ */
+ boolean acquire() {
+ long nowWindow =
+ Duration.between(Instant.EPOCH, Instant.ofEpochMilli(mClock.currentTimeMillis()))
+ .dividedBy(mWindowSize);
+ if (nowWindow > mCurrentWindow) {
+ mCurrentWindow = nowWindow;
+ mPermitsGranted = 0;
+ }
+
+ if (mPermitsGranted < mMaxPermits) {
+ mPermitsGranted++;
+ return true;
+ }
+
+ return false;
+ }
+}
diff --git a/services/core/java/com/android/server/selinux/RateLimiter.java b/services/core/java/com/android/server/selinux/RateLimiter.java
new file mode 100644
index 0000000..599b840
--- /dev/null
+++ b/services/core/java/com/android/server/selinux/RateLimiter.java
@@ -0,0 +1,85 @@
+/*
+ * 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.server.selinux;
+
+import android.os.SystemClock;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.os.Clock;
+
+import java.time.Duration;
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+
+/**
+ * Rate limiter to ensure Atoms are pushed only within the allowed QPS window. This class is not
+ * thread-safe.
+ *
+ * <p>The rate limiter is smoothed, meaning that a rate limiter allowing X permits per second (or X
+ * QPS) will grant permits at a ratio of one every 1/X seconds.
+ */
+public final class RateLimiter {
+
+ private Instant mNextPermit = Instant.EPOCH;
+
+ private final Clock mClock;
+ private final Duration mWindow;
+
+ @VisibleForTesting
+ RateLimiter(Clock clock, Duration window) {
+ mClock = clock;
+ // Truncating because the system clock does not support units smaller than milliseconds.
+ mWindow = window;
+ }
+
+ /**
+ * Create a rate limiter generating one permit every {@code window} of time, using the {@link
+ * Clock.SYSTEM_CLOCK}.
+ */
+ public RateLimiter(Duration window) {
+ this(Clock.SYSTEM_CLOCK, window);
+ }
+
+ /**
+ * Acquire a permit if allowed by the rate limiter. If not, wait until a permit becomes
+ * available.
+ */
+ public void acquire() {
+ Instant now = Instant.ofEpochMilli(mClock.currentTimeMillis());
+
+ if (mNextPermit.isAfter(now)) { // Sleep until we can acquire.
+ SystemClock.sleep(ChronoUnit.MILLIS.between(now, mNextPermit));
+ mNextPermit = mNextPermit.plus(mWindow);
+ } else {
+ mNextPermit = now.plus(mWindow);
+ }
+ }
+
+ /**
+ * Try to acquire a permit if allowed by the rate limiter. Non-blocking.
+ *
+ * @return true if a permit was acquired. Otherwise, return false.
+ */
+ public boolean tryAcquire() {
+ final Instant now = Instant.ofEpochMilli(mClock.currentTimeMillis());
+
+ if (mNextPermit.isAfter(now)) {
+ return false;
+ }
+ mNextPermit = now.plus(mWindow);
+ return true;
+ }
+}
diff --git a/services/core/java/com/android/server/selinux/SelinuxAuditLogBuilder.java b/services/core/java/com/android/server/selinux/SelinuxAuditLogBuilder.java
new file mode 100644
index 0000000..8d8d596
--- /dev/null
+++ b/services/core/java/com/android/server/selinux/SelinuxAuditLogBuilder.java
@@ -0,0 +1,155 @@
+/*
+ * 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.server.selinux;
+
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.Optional;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.Stream;
+
+/** Builder for SelinuxAuditLogs. */
+class SelinuxAuditLogBuilder {
+
+ // Currently logs collection is hardcoded for the sdk_sandbox_audit.
+ private static final String SDK_SANDBOX_AUDIT = "sdk_sandbox_audit";
+ static final Matcher SCONTEXT_MATCHER =
+ Pattern.compile(
+ "u:r:(?<stype>"
+ + SDK_SANDBOX_AUDIT
+ + "):s0(:c)?(?<scategories>((,c)?\\d+)+)*")
+ .matcher("");
+
+ static final Matcher TCONTEXT_MATCHER =
+ Pattern.compile("u:object_r:(?<ttype>\\w+):s0(:c)?(?<tcategories>((,c)?\\d+)+)*")
+ .matcher("");
+
+ static final Matcher PATH_MATCHER =
+ Pattern.compile("\"(?<path>/\\w+(/\\w+)?)(/\\w+)*\"").matcher("");
+
+ private Iterator<String> mTokens;
+ private final SelinuxAuditLog mAuditLog = new SelinuxAuditLog();
+
+ void reset(String denialString) {
+ mTokens =
+ Arrays.asList(
+ Optional.ofNullable(denialString)
+ .map(s -> s.split("\\s+|="))
+ .orElse(new String[0]))
+ .iterator();
+ mAuditLog.reset();
+ }
+
+ SelinuxAuditLog build() {
+ while (mTokens.hasNext()) {
+ final String token = mTokens.next();
+
+ switch (token) {
+ case "granted":
+ mAuditLog.mGranted = true;
+ break;
+ case "denied":
+ mAuditLog.mGranted = false;
+ break;
+ case "{":
+ Stream.Builder<String> permissionsStream = Stream.builder();
+ boolean closed = false;
+ while (!closed && mTokens.hasNext()) {
+ String permission = mTokens.next();
+ if ("}".equals(permission)) {
+ closed = true;
+ } else {
+ permissionsStream.add(permission);
+ }
+ }
+ if (!closed) {
+ return null;
+ }
+ mAuditLog.mPermissions = permissionsStream.build().toArray(String[]::new);
+ break;
+ case "scontext":
+ if (!nextTokenMatches(SCONTEXT_MATCHER)) {
+ return null;
+ }
+ mAuditLog.mSType = SCONTEXT_MATCHER.group("stype");
+ mAuditLog.mSCategories = toCategories(SCONTEXT_MATCHER.group("scategories"));
+ break;
+ case "tcontext":
+ if (!nextTokenMatches(TCONTEXT_MATCHER)) {
+ return null;
+ }
+ mAuditLog.mTType = TCONTEXT_MATCHER.group("ttype");
+ mAuditLog.mTCategories = toCategories(TCONTEXT_MATCHER.group("tcategories"));
+ break;
+ case "tclass":
+ if (!mTokens.hasNext()) {
+ return null;
+ }
+ mAuditLog.mTClass = mTokens.next();
+ break;
+ case "path":
+ if (nextTokenMatches(PATH_MATCHER)) {
+ mAuditLog.mPath = PATH_MATCHER.group("path");
+ }
+ break;
+ case "permissive":
+ if (!mTokens.hasNext()) {
+ return null;
+ }
+ mAuditLog.mPermissive = "1".equals(mTokens.next());
+ break;
+ default:
+ break;
+ }
+ }
+ return mAuditLog;
+ }
+
+ boolean nextTokenMatches(Matcher matcher) {
+ return mTokens.hasNext() && matcher.reset(mTokens.next()).matches();
+ }
+
+ static int[] toCategories(String categories) {
+ return categories == null
+ ? null
+ : Arrays.stream(categories.split(",c")).mapToInt(Integer::parseInt).toArray();
+ }
+
+ static class SelinuxAuditLog {
+ boolean mGranted = false;
+ String[] mPermissions = null;
+ String mSType = null;
+ int[] mSCategories = null;
+ String mTType = null;
+ int[] mTCategories = null;
+ String mTClass = null;
+ String mPath = null;
+ boolean mPermissive = false;
+
+ private void reset() {
+ mGranted = false;
+ mPermissions = null;
+ mSType = null;
+ mSCategories = null;
+ mTType = null;
+ mTCategories = null;
+ mTClass = null;
+ mPath = null;
+ mPermissive = false;
+ }
+ }
+}
diff --git a/services/core/java/com/android/server/selinux/SelinuxAuditLogsCollector.java b/services/core/java/com/android/server/selinux/SelinuxAuditLogsCollector.java
new file mode 100644
index 0000000..0219645
--- /dev/null
+++ b/services/core/java/com/android/server/selinux/SelinuxAuditLogsCollector.java
@@ -0,0 +1,144 @@
+/*
+ * 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.server.selinux;
+
+import android.util.EventLog;
+import android.util.EventLog.Event;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.FrameworkStatsLog;
+import com.android.server.selinux.SelinuxAuditLogBuilder.SelinuxAuditLog;
+
+import java.io.IOException;
+import java.time.Instant;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Queue;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/** Class in charge of collecting SELinux audit logs and push the SELinux atoms. */
+class SelinuxAuditLogsCollector {
+
+ private static final String TAG = "SelinuxAuditLogs";
+
+ private static final String SELINUX_PATTERN = "^.*\\bavc:\\s+(?<denial>.*)$";
+
+ @VisibleForTesting
+ static final Matcher SELINUX_MATCHER = Pattern.compile(SELINUX_PATTERN).matcher("");
+
+ private final RateLimiter mRateLimiter;
+ private final QuotaLimiter mQuotaLimiter;
+
+ @VisibleForTesting Instant mLastWrite = Instant.MIN;
+
+ final AtomicBoolean mStopRequested = new AtomicBoolean(false);
+
+ SelinuxAuditLogsCollector(RateLimiter rateLimiter, QuotaLimiter quotaLimiter) {
+ mRateLimiter = rateLimiter;
+ mQuotaLimiter = quotaLimiter;
+ }
+
+ /**
+ * Collect and push SELinux audit logs for the provided {@code tagCode}.
+ *
+ * @return true if the job was completed. If the job was interrupted, return false.
+ */
+ boolean collect(int tagCode) {
+ Queue<Event> logLines = new ArrayDeque<>();
+ Instant latestTimestamp = collectLogLines(tagCode, logLines);
+
+ boolean quotaExceeded = writeAuditLogs(logLines);
+ if (quotaExceeded) {
+ Log.w(TAG, "Too many SELinux logs in the queue, I am giving up.");
+ mLastWrite = latestTimestamp; // next run we will ignore all these logs.
+ logLines.clear();
+ }
+
+ return logLines.isEmpty();
+ }
+
+ private Instant collectLogLines(int tagCode, Queue<Event> logLines) {
+ List<Event> events = new ArrayList<>();
+ try {
+ EventLog.readEvents(new int[] {tagCode}, events);
+ } catch (IOException e) {
+ Log.e(TAG, "Error reading event logs", e);
+ }
+
+ Instant latestTimestamp = mLastWrite;
+ for (Event event : events) {
+ Instant eventTime = Instant.ofEpochSecond(0, event.getTimeNanos());
+ if (eventTime.isAfter(latestTimestamp)) {
+ latestTimestamp = eventTime;
+ }
+ if (eventTime.isBefore(mLastWrite)) {
+ continue;
+ }
+ Object eventData = event.getData();
+ if (!(eventData instanceof String)) {
+ continue;
+ }
+ logLines.add(event);
+ }
+ return latestTimestamp;
+ }
+
+ private boolean writeAuditLogs(Queue<Event> logLines) {
+ final SelinuxAuditLogBuilder auditLogBuilder = new SelinuxAuditLogBuilder();
+
+ while (!mStopRequested.get() && !logLines.isEmpty()) {
+ Event event = logLines.poll();
+ String logLine = (String) event.getData();
+ Instant logTime = Instant.ofEpochSecond(0, event.getTimeNanos());
+ if (!SELINUX_MATCHER.reset(logLine).matches()) {
+ continue;
+ }
+
+ auditLogBuilder.reset(SELINUX_MATCHER.group("denial"));
+ final SelinuxAuditLog auditLog = auditLogBuilder.build();
+ if (auditLog == null) {
+ continue;
+ }
+
+ if (!mQuotaLimiter.acquire()) {
+ return true;
+ }
+ mRateLimiter.acquire();
+
+ FrameworkStatsLog.write(
+ FrameworkStatsLog.SELINUX_AUDIT_LOG,
+ auditLog.mGranted,
+ auditLog.mPermissions,
+ auditLog.mSType,
+ auditLog.mSCategories,
+ auditLog.mTType,
+ auditLog.mTCategories,
+ auditLog.mTClass,
+ auditLog.mPath,
+ auditLog.mPermissive);
+
+ if (logTime.isAfter(mLastWrite)) {
+ mLastWrite = logTime;
+ }
+ }
+
+ return false;
+ }
+}
diff --git a/services/core/java/com/android/server/selinux/SelinuxAuditLogsService.java b/services/core/java/com/android/server/selinux/SelinuxAuditLogsService.java
new file mode 100644
index 0000000..8a661bc
--- /dev/null
+++ b/services/core/java/com/android/server/selinux/SelinuxAuditLogsService.java
@@ -0,0 +1,132 @@
+/*
+ * 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.server.selinux;
+
+import static com.android.sdksandbox.flags.Flags.selinuxSdkSandboxAudit;
+
+import android.app.job.JobInfo;
+import android.app.job.JobParameters;
+import android.app.job.JobScheduler;
+import android.app.job.JobService;
+import android.content.ComponentName;
+import android.content.Context;
+import android.util.EventLog;
+import android.util.Log;
+
+import java.time.Duration;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * Scheduled jobs related to logging of SELinux denials and audits. The job runs daily on idle
+ * devices.
+ */
+public class SelinuxAuditLogsService extends JobService {
+
+ private static final String TAG = "SelinuxAuditLogs";
+ private static final String SELINUX_AUDIT_NAMESPACE = "SelinuxAuditLogsNamespace";
+
+ static final int AUDITD_TAG_CODE = EventLog.getTagCode("auditd");
+
+ private static final int SELINUX_AUDIT_JOB_ID = 25327386;
+ private static final JobInfo SELINUX_AUDIT_JOB =
+ new JobInfo.Builder(
+ SELINUX_AUDIT_JOB_ID,
+ new ComponentName("android", SelinuxAuditLogsService.class.getName()))
+ .setPeriodic(TimeUnit.DAYS.toMillis(1))
+ .setRequiresDeviceIdle(true)
+ .setRequiresCharging(true)
+ .setRequiresBatteryNotLow(true)
+ .build();
+
+ private static final ExecutorService EXECUTOR_SERVICE = Executors.newSingleThreadExecutor();
+ private static final AtomicReference<Boolean> IS_RUNNING = new AtomicReference<>(false);
+
+ // Audit logging is subject to both rate and quota limiting. We can only push one atom every 10
+ // milliseconds, and no more than 50K atoms can be pushed each day.
+ private static final SelinuxAuditLogsCollector AUDIT_LOGS_COLLECTOR =
+ new SelinuxAuditLogsCollector(
+ new RateLimiter(/* window= */ Duration.ofMillis(10)),
+ new QuotaLimiter(/* maxPermitsPerDay= */ 50000));
+
+ /** Schedule jobs with the {@link JobScheduler}. */
+ public static void schedule(Context context) {
+ if (!selinuxSdkSandboxAudit()) {
+ Log.d(TAG, "SelinuxAuditLogsService not enabled");
+ return;
+ }
+
+ if (AUDITD_TAG_CODE == -1) {
+ Log.e(TAG, "auditd is not a registered tag on this system");
+ return;
+ }
+
+ if (context.getSystemService(JobScheduler.class)
+ .forNamespace(SELINUX_AUDIT_NAMESPACE)
+ .schedule(SELINUX_AUDIT_JOB)
+ == JobScheduler.RESULT_FAILURE) {
+ Log.e(TAG, "SelinuxAuditLogsService could not be started.");
+ }
+ }
+
+ @Override
+ public boolean onStartJob(JobParameters params) {
+ if (params.getJobId() != SELINUX_AUDIT_JOB_ID) {
+ Log.e(TAG, "The job id does not match the expected selinux job id.");
+ return false;
+ }
+
+ AUDIT_LOGS_COLLECTOR.mStopRequested.set(false);
+ IS_RUNNING.set(true);
+ EXECUTOR_SERVICE.execute(new LogsCollectorJob(this, params));
+
+ return true; // the job is running
+ }
+
+ @Override
+ public boolean onStopJob(JobParameters params) {
+ if (params.getJobId() != SELINUX_AUDIT_JOB_ID) {
+ return false;
+ }
+
+ AUDIT_LOGS_COLLECTOR.mStopRequested.set(true);
+ return IS_RUNNING.get();
+ }
+
+ private static class LogsCollectorJob implements Runnable {
+ private final JobService mAuditLogService;
+ private final JobParameters mParams;
+
+ LogsCollectorJob(JobService auditLogService, JobParameters params) {
+ mAuditLogService = auditLogService;
+ mParams = params;
+ }
+
+ @Override
+ public void run() {
+ IS_RUNNING.updateAndGet(
+ isRunning -> {
+ boolean done = AUDIT_LOGS_COLLECTOR.collect(AUDITD_TAG_CODE);
+ if (done) {
+ mAuditLogService.jobFinished(mParams, /* wantsReschedule= */ false);
+ }
+ return !done;
+ });
+ }
+ }
+}
diff --git a/services/core/java/com/android/server/stats/Android.bp b/services/core/java/com/android/server/stats/Android.bp
new file mode 100644
index 0000000..e597c3a
--- /dev/null
+++ b/services/core/java/com/android/server/stats/Android.bp
@@ -0,0 +1,12 @@
+aconfig_declarations {
+ name: "stats_flags",
+ package: "com.android.server.stats",
+ srcs: [
+ "stats_flags.aconfig",
+ ],
+}
+
+java_aconfig_library {
+ name: "stats_flags_lib",
+ aconfig_declarations: "stats_flags",
+}
diff --git a/services/core/java/com/android/server/stats/pull/AggregatedMobileDataStatsPuller.java b/services/core/java/com/android/server/stats/pull/AggregatedMobileDataStatsPuller.java
new file mode 100644
index 0000000..0de73a5
--- /dev/null
+++ b/services/core/java/com/android/server/stats/pull/AggregatedMobileDataStatsPuller.java
@@ -0,0 +1,285 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.stats.pull;
+
+import android.app.ActivityManager;
+import android.app.StatsManager;
+import android.app.usage.NetworkStatsManager;
+import android.net.NetworkStats;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Trace;
+import android.util.ArrayMap;
+import android.util.Slog;
+import android.util.SparseIntArray;
+import android.util.StatsEvent;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.util.FrameworkStatsLog;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Aggregates Mobile Data Usage by process state per uid
+ */
+class AggregatedMobileDataStatsPuller {
+ private static final String TAG = "AggregatedMobileDataStatsPuller";
+
+ private static final boolean DEBUG = false;
+
+ private static class UidProcState {
+
+ private final int mUid;
+ private final int mState;
+
+ UidProcState(int uid, int state) {
+ mUid = uid;
+ mState = state;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof UidProcState key)) return false;
+ return mUid == key.mUid && mState == key.mState;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = mUid;
+ result = 31 * result + mState;
+ return result;
+ }
+
+ public int getUid() {
+ return mUid;
+ }
+
+ public int getState() {
+ return mState;
+ }
+
+ }
+
+ private static class MobileDataStats {
+ private long mRxPackets = 0;
+ private long mTxPackets = 0;
+ private long mRxBytes = 0;
+ private long mTxBytes = 0;
+
+ public long getRxPackets() {
+ return mRxPackets;
+ }
+
+ public long getTxPackets() {
+ return mTxPackets;
+ }
+
+ public long getRxBytes() {
+ return mRxBytes;
+ }
+
+ public long getTxBytes() {
+ return mTxBytes;
+ }
+
+ public void addRxPackets(long rxPackets) {
+ mRxPackets += rxPackets;
+ }
+
+ public void addTxPackets(long txPackets) {
+ mTxPackets += txPackets;
+ }
+
+ public void addRxBytes(long rxBytes) {
+ mRxBytes += rxBytes;
+ }
+
+ public void addTxBytes(long txBytes) {
+ mTxBytes += txBytes;
+ }
+
+ public boolean isEmpty() {
+ return mRxPackets == 0 && mTxPackets == 0 && mRxBytes == 0 && mTxBytes == 0;
+ }
+ }
+
+ private final Object mLock = new Object();
+ @GuardedBy("mLock")
+ private final Map<UidProcState, MobileDataStats> mUidStats;
+
+ private final SparseIntArray mUidPreviousState;
+
+ private NetworkStats mLastMobileUidStats = new NetworkStats(0, -1);
+
+ private final NetworkStatsManager mNetworkStatsManager;
+
+ private final Handler mMobileDataStatsHandler;
+
+ AggregatedMobileDataStatsPuller(NetworkStatsManager networkStatsManager) {
+ mUidStats = new ArrayMap<>();
+ mUidPreviousState = new SparseIntArray();
+
+ mNetworkStatsManager = networkStatsManager;
+
+ if (mNetworkStatsManager != null) {
+ updateNetworkStats(mNetworkStatsManager);
+ }
+
+ HandlerThread mMobileDataStatsHandlerThread = new HandlerThread("MobileDataStatsHandler");
+ mMobileDataStatsHandlerThread.start();
+ mMobileDataStatsHandler = new Handler(mMobileDataStatsHandlerThread.getLooper());
+ }
+
+ public void noteUidProcessState(int uid, int state, long unusedElapsedRealtime,
+ long unusedUptime) {
+ mMobileDataStatsHandler.post(
+ () -> {
+ noteUidProcessStateImpl(uid, state);
+ });
+ }
+
+ public int pullDataBytesTransfer(List<StatsEvent> data) {
+ synchronized (mLock) {
+ return pullDataBytesTransferLocked(data);
+ }
+ }
+
+ @GuardedBy("mLock")
+ private MobileDataStats getUidStatsForPreviousStateLocked(int uid) {
+ final int previousState = mUidPreviousState.get(uid, ActivityManager.PROCESS_STATE_UNKNOWN);
+ if (DEBUG && previousState == ActivityManager.PROCESS_STATE_UNKNOWN) {
+ Slog.d(TAG, "getUidStatsForPreviousStateLocked() no prev state info for uid "
+ + uid + ". Tracking stats with ActivityManager.PROCESS_STATE_UNKNOWN");
+ }
+
+ final UidProcState statsKey = new UidProcState(uid, previousState);
+ MobileDataStats stats;
+ if (mUidStats.containsKey(statsKey)) {
+ stats = mUidStats.get(statsKey);
+ } else {
+ stats = new MobileDataStats();
+ mUidStats.put(statsKey, stats);
+ }
+ return stats;
+ }
+
+ private void noteUidProcessStateImpl(int uid, int state) {
+ // noteUidProcessStateLocked can be called back to back several times while
+ // the updateNetworkStatsLocked loops over several stats for multiple uids
+ // and during the first call in a batch of proc state change event it can
+ // contain info for uid with unknown previous state yet which can happen due to a few
+ // reasons:
+ // - app was just started
+ // - app was started before the ActivityManagerService
+ // as result stats would be created with state == ActivityManager.PROCESS_STATE_UNKNOWN
+ if (mNetworkStatsManager != null) {
+ updateNetworkStats(mNetworkStatsManager);
+ } else {
+ Slog.w(TAG, "noteUidProcessStateLocked() can not get mNetworkStatsManager");
+ }
+ mUidPreviousState.put(uid, state);
+ }
+
+ private void updateNetworkStats(NetworkStatsManager networkStatsManager) {
+ if (DEBUG) {
+ if (Trace.isTagEnabled(Trace.TRACE_TAG_SYSTEM_SERVER)) {
+ Trace.traceBegin(Trace.TRACE_TAG_SYSTEM_SERVER, TAG + "-updateNetworkStats");
+ }
+ }
+
+ final NetworkStats latestStats = networkStatsManager.getMobileUidStats();
+ if (isEmpty(latestStats)) {
+ if (DEBUG) {
+ Slog.w(TAG, "getMobileUidStats() failed");
+ Trace.traceEnd(Trace.TRACE_TAG_SYSTEM_SERVER);
+ }
+ return;
+ }
+ NetworkStats delta = latestStats.subtract(mLastMobileUidStats);
+ mLastMobileUidStats = latestStats;
+
+ if (!isEmpty(delta)) {
+ updateNetworkStatsDelta(delta);
+ } else if (DEBUG) {
+ Slog.w(TAG, "updateNetworkStats() no delta");
+ }
+ if (DEBUG) {
+ Trace.traceEnd(Trace.TRACE_TAG_SYSTEM_SERVER);
+ }
+ }
+
+ private void updateNetworkStatsDelta(NetworkStats delta) {
+ synchronized (mLock) {
+ for (NetworkStats.Entry entry : delta) {
+ if (entry.getRxPackets() == 0 && entry.getTxPackets() == 0) {
+ continue;
+ }
+ MobileDataStats stats = getUidStatsForPreviousStateLocked(entry.getUid());
+ stats.addTxBytes(entry.getTxBytes());
+ stats.addRxBytes(entry.getRxBytes());
+ stats.addTxPackets(entry.getTxPackets());
+ stats.addRxPackets(entry.getRxPackets());
+ }
+ }
+ }
+
+ @GuardedBy("mLock")
+ private int pullDataBytesTransferLocked(List<StatsEvent> pulledData) {
+ if (DEBUG) {
+ Slog.d(TAG, "pullDataBytesTransferLocked() start");
+ }
+ for (Map.Entry<UidProcState, MobileDataStats> uidStats : mUidStats.entrySet()) {
+ if (!uidStats.getValue().isEmpty()) {
+ MobileDataStats stats = uidStats.getValue();
+ pulledData.add(FrameworkStatsLog.buildStatsEvent(
+ FrameworkStatsLog.MOBILE_BYTES_TRANSFER_BY_PROC_STATE,
+ uidStats.getKey().getUid(),
+ ActivityManager.processStateAmToProto(uidStats.getKey().getState()),
+ stats.getRxBytes(),
+ stats.getRxPackets(),
+ stats.getTxBytes(),
+ stats.getTxPackets()));
+ }
+ }
+ if (DEBUG) {
+ Slog.d(TAG,
+ "pullDataBytesTransferLocked() done. results count " + pulledData.size());
+ }
+ if (!pulledData.isEmpty()) {
+ return StatsManager.PULL_SUCCESS;
+ }
+ return StatsManager.PULL_SKIP;
+ }
+
+ private static boolean isEmpty(NetworkStats stats) {
+ long totalRxPackets = 0;
+ long totalTxPackets = 0;
+ for (NetworkStats.Entry entry : stats) {
+ if (entry.getRxPackets() == 0 && entry.getTxPackets() == 0) {
+ continue;
+ }
+ totalRxPackets += entry.getRxPackets();
+ totalTxPackets += entry.getTxPackets();
+ // at least one non empty entry located
+ break;
+ }
+ final long totalPackets = totalRxPackets + totalTxPackets;
+ return totalPackets == 0;
+ }
+}
diff --git a/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java b/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java
index e876241..285bcc3 100644
--- a/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java
+++ b/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java
@@ -59,6 +59,7 @@
import static com.android.internal.util.FrameworkStatsLog.TIME_ZONE_DETECTOR_STATE__DETECTION_MODE__TELEPHONY;
import static com.android.internal.util.FrameworkStatsLog.TIME_ZONE_DETECTOR_STATE__DETECTION_MODE__UNKNOWN;
import static com.android.server.am.MemoryStatUtil.readMemoryStatFromFilesystem;
+import static com.android.server.stats.Flags.addMobileBytesTransferByProcStatePuller;
import static com.android.server.stats.pull.IonMemoryUtil.readProcessSystemIonHeapSizesFromDebugfs;
import static com.android.server.stats.pull.IonMemoryUtil.readSystemIonHeapSizeFromDebugfs;
import static com.android.server.stats.pull.ProcfsMemoryUtil.getProcessCmdlines;
@@ -409,6 +410,15 @@
@GuardedBy("mKeystoreLock")
private IKeystoreMetrics mIKeystoreMetrics;
+ private AggregatedMobileDataStatsPuller mAggregatedMobileDataStatsPuller = null;
+
+ /**
+ * Whether or not to enable the new puller with aggregation by process state per uid on a
+ * system server side.
+ */
+ public static final boolean ENABLE_MOBILE_DATA_STATS_AGGREGATED_PULLER =
+ addMobileBytesTransferByProcStatePuller();
+
// Puller locks
private final Object mDataBytesTransferLock = new Object();
private final Object mBluetoothBytesTransferLock = new Object();
@@ -469,6 +479,20 @@
mContext = context;
}
+ private final class StatsPullAtomServiceInternalImpl extends StatsPullAtomServiceInternal {
+
+ @Override
+ public void noteUidProcessState(int uid, int state) {
+ if (ENABLE_MOBILE_DATA_STATS_AGGREGATED_PULLER
+ && mAggregatedMobileDataStatsPuller != null) {
+ final long elapsedRealtime = SystemClock.elapsedRealtime();
+ final long uptime = SystemClock.uptimeMillis();
+ mAggregatedMobileDataStatsPuller.noteUidProcessState(uid, state, elapsedRealtime,
+ uptime);
+ }
+ }
+ }
+
private native void initializeNativePullers();
/**
@@ -486,6 +510,11 @@
}
try {
switch (atomTag) {
+ case FrameworkStatsLog.MOBILE_BYTES_TRANSFER_BY_PROC_STATE:
+ if (ENABLE_MOBILE_DATA_STATS_AGGREGATED_PULLER
+ && mAggregatedMobileDataStatsPuller != null) {
+ return mAggregatedMobileDataStatsPuller.pullDataBytesTransfer(data);
+ }
case FrameworkStatsLog.WIFI_BYTES_TRANSFER:
case FrameworkStatsLog.WIFI_BYTES_TRANSFER_BY_FG_BG:
case FrameworkStatsLog.MOBILE_BYTES_TRANSFER:
@@ -776,7 +805,10 @@
@Override
public void onStart() {
- // no op
+ if (ENABLE_MOBILE_DATA_STATS_AGGREGATED_PULLER) {
+ LocalServices.addService(StatsPullAtomServiceInternal.class,
+ new StatsPullAtomServiceInternalImpl());
+ }
}
@Override
@@ -811,6 +843,9 @@
mStatsSubscriptionsListener = new StatsSubscriptionsListener(mSubscriptionManager);
mStorageManager = (StorageManager) mContext.getSystemService(StorageManager.class);
mNetworkStatsManager = mContext.getSystemService(NetworkStatsManager.class);
+
+ initMobileDataStatsPuller();
+
// Initialize DiskIO
mStoragedUidIoStatsReader = new StoragedUidIoStatsReader();
@@ -972,6 +1007,18 @@
registerCachedAppsHighWatermarkPuller();
}
+ private void initMobileDataStatsPuller() {
+ if (DEBUG) {
+ Slog.d(TAG,
+ "ENABLE_MOBILE_DATA_STATS_AGGREGATED_PULLER = "
+ + ENABLE_MOBILE_DATA_STATS_AGGREGATED_PULLER);
+ }
+ if (ENABLE_MOBILE_DATA_STATS_AGGREGATED_PULLER) {
+ mAggregatedMobileDataStatsPuller =
+ new AggregatedMobileDataStatsPuller(mNetworkStatsManager);
+ }
+ }
+
private void initAndRegisterNetworkStatsPullers() {
if (DEBUG) {
Slog.d(TAG, "Registering NetworkStats pullers with statsd");
@@ -1013,6 +1060,9 @@
registerWifiBytesTransferBackground();
registerMobileBytesTransfer();
registerMobileBytesTransferBackground();
+ if (ENABLE_MOBILE_DATA_STATS_AGGREGATED_PULLER) {
+ registerMobileBytesTransferByProcState();
+ }
registerBytesTransferByTagAndMetered();
registerDataUsageBytesTransfer();
registerOemManagedBytesTransfer();
@@ -1021,6 +1071,13 @@
}
}
+ private void registerMobileBytesTransferByProcState() {
+ final int tagId = FrameworkStatsLog.MOBILE_BYTES_TRANSFER_BY_PROC_STATE;
+ PullAtomMetadata metadata =
+ new PullAtomMetadata.Builder().setAdditiveFields(new int[] {3, 4, 5, 6}).build();
+ mStatsManager.setPullAtomCallback(tagId, metadata, DIRECT_EXECUTOR, mStatsCallbackImpl);
+ }
+
private void initAndRegisterDeferredPullers() {
mUwbManager = mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_UWB)
? mContext.getSystemService(UwbManager.class) : null;
diff --git a/services/core/java/com/android/server/stats/pull/StatsPullAtomServiceInternal.java b/services/core/java/com/android/server/stats/pull/StatsPullAtomServiceInternal.java
new file mode 100644
index 0000000..06adbfc
--- /dev/null
+++ b/services/core/java/com/android/server/stats/pull/StatsPullAtomServiceInternal.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.stats.pull;
+
+/**
+ * System-server internal interface to the {@link StatsPullAtomService}.
+ *
+ * @hide Only for use within the system server.
+ */
+public abstract class StatsPullAtomServiceInternal {
+
+ /**
+ * @param state Process state from ActivityManager.java.
+ */
+ public abstract void noteUidProcessState(int uid, int state);
+
+}
diff --git a/services/core/java/com/android/server/stats/stats_flags.aconfig b/services/core/java/com/android/server/stats/stats_flags.aconfig
new file mode 100644
index 0000000..5101a69
--- /dev/null
+++ b/services/core/java/com/android/server/stats/stats_flags.aconfig
@@ -0,0 +1,9 @@
+package: "com.android.server.stats"
+
+flag {
+ name: "add_mobile_bytes_transfer_by_proc_state_puller"
+ namespace: "statsd"
+ description: "Adds mobile_bytes_transfer_by_proc_state atom with system server side aggregation"
+ bug: "309512867"
+ is_fixed_read_only: true
+}
\ No newline at end of file
diff --git a/services/core/java/com/android/server/tv/TvInputHardwareManager.java b/services/core/java/com/android/server/tv/TvInputHardwareManager.java
index b384725..92b5764 100755
--- a/services/core/java/com/android/server/tv/TvInputHardwareManager.java
+++ b/services/core/java/com/android/server/tv/TvInputHardwareManager.java
@@ -309,6 +309,24 @@
}
}
+ public SparseArray<String> getHardwareInputIdMap() {
+ synchronized (mLock) {
+ return mHardwareInputIdMap.clone();
+ }
+ }
+
+ public SparseArray<String> getHdmiInputIdMap() {
+ synchronized (mLock) {
+ return mHdmiInputIdMap.clone();
+ }
+ }
+
+ public Map<String, TvInputInfo> getInputMap() {
+ synchronized (mLock) {
+ return Collections.unmodifiableMap(mInputMap);
+ }
+ }
+
public Map<String, List<String>> getHdmiParentInputMap() {
synchronized (mLock) {
return Collections.unmodifiableMap(mHdmiParentInputMap);
diff --git a/services/core/java/com/android/server/tv/TvInputManagerService.java b/services/core/java/com/android/server/tv/TvInputManagerService.java
index 73fc8e9..e434df7 100644
--- a/services/core/java/com/android/server/tv/TvInputManagerService.java
+++ b/services/core/java/com/android/server/tv/TvInputManagerService.java
@@ -135,6 +135,7 @@
private static final int APP_TAG_SELF = TunedInfo.APP_TAG_SELF;
private static final String PERMISSION_ACCESS_WATCHED_PROGRAMS =
"com.android.providers.tv.permission.ACCESS_WATCHED_PROGRAMS";
+ private static final long UPDATE_HARDWARE_TIS_BINDING_DELAY_IN_MILLIS = 10 * 1000; // 10 seconds
// There are two different formats of DVB frontend devices. One is /dev/dvb%d.frontend%d,
// another one is /dev/dvb/adapter%d/frontend%d. Followings are the patterns for selecting the
@@ -174,7 +175,7 @@
@GuardedBy("mLock")
private final Map<String, SessionState> mSessionIdToSessionStateMap = new HashMap<>();
- private final WatchLogHandler mWatchLogHandler;
+ private final MessageHandler mMessageHandler;
private final ActivityManager mActivityManager;
@@ -187,8 +188,8 @@
super(context);
mContext = context;
- mWatchLogHandler = new WatchLogHandler(mContext.getContentResolver(),
- IoThread.get().getLooper());
+ mMessageHandler =
+ new MessageHandler(mContext.getContentResolver(), IoThread.get().getLooper());
mTvInputHardwareManager = new TvInputHardwareManager(context, new HardwareListener());
mActivityManager =
@@ -372,10 +373,10 @@
// service to populate the hardware list.
serviceState = new ServiceState(component, userId);
userState.serviceStateMap.put(component, serviceState);
+ updateServiceConnectionLocked(component, userId);
} else {
inputList.addAll(serviceState.hardwareInputMap.values());
}
- updateServiceConnectionLocked(component, userId);
} else {
try {
TvInputInfo info = new TvInputInfo.Builder(mContext, ri).build();
@@ -510,6 +511,7 @@
}
}
+ @GuardedBy("mLock")
private void startProfileLocked(int userId) {
mRunningProfiles.add(userId);
buildTvInputListLocked(userId, null);
@@ -538,8 +540,10 @@
mCurrentUserId = userId;
buildTvInputListLocked(userId, null);
buildTvContentRatingSystemListLocked(userId);
- mWatchLogHandler.obtainMessage(WatchLogHandler.MSG_SWITCH_CONTENT_RESOLVER,
- getContentResolverForUser(userId)).sendToTarget();
+ mMessageHandler
+ .obtainMessage(MessageHandler.MSG_SWITCH_CONTENT_RESOLVER,
+ getContentResolverForUser(userId))
+ .sendToTarget();
}
}
@@ -593,7 +597,7 @@
Slog.e(TAG, "error in unregisterCallback", e);
}
}
- mContext.unbindService(serviceState.connection);
+ unbindService(serviceState);
it.remove();
}
}
@@ -661,7 +665,7 @@
Slog.e(TAG, "error in unregisterCallback", e);
}
}
- mContext.unbindService(serviceState.connection);
+ unbindService(serviceState);
}
}
userState.serviceStateMap.clear();
@@ -774,7 +778,8 @@
boolean shouldBind;
if (userId == mCurrentUserId || mRunningProfiles.contains(userId)) {
- shouldBind = !serviceState.sessionTokens.isEmpty() || serviceState.isHardware;
+ shouldBind = !serviceState.sessionTokens.isEmpty()
+ || (serviceState.isHardware && serviceState.neverConnected);
} else {
// For a non-current user,
// if sessionTokens is not empty, it contains recording sessions only
@@ -783,31 +788,14 @@
shouldBind = !serviceState.sessionTokens.isEmpty();
}
- if (serviceState.service == null && shouldBind) {
- // This means that the service is not yet connected but its state indicates that we
- // have pending requests. Then, connect the service.
- if (serviceState.bound) {
- // We have already bound to the service so we don't try to bind again until after we
- // unbind later on.
- return;
+ // only bind/unbind when necessary.
+ if (shouldBind && !serviceState.bound) {
+ bindService(serviceState, userId);
+ } else if (!shouldBind && serviceState.bound) {
+ unbindService(serviceState);
+ if (!serviceState.isHardware) {
+ userState.serviceStateMap.remove(component);
}
- if (DEBUG) {
- Slog.d(TAG, "bindServiceAsUser(service=" + component + ", userId=" + userId + ")");
- }
-
- Intent i = new Intent(TvInputService.SERVICE_INTERFACE).setComponent(component);
- serviceState.bound = mContext.bindServiceAsUser(
- i, serviceState.connection,
- Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE_WHILE_AWAKE,
- new UserHandle(userId));
- } else if (serviceState.service != null && !shouldBind) {
- // This means that the service is already connected but its state indicates that we have
- // nothing to do with it. Then, disconnect the service.
- if (DEBUG) {
- Slog.d(TAG, "unbindService(service=" + component + ")");
- }
- mContext.unbindService(serviceState.connection);
- userState.serviceStateMap.remove(component);
}
}
@@ -829,7 +817,11 @@
sendSessionTokenToClientLocked(sessionState.client,
sessionState.inputId, null, null, sessionState.seq);
}
- updateServiceConnectionLocked(serviceState.component, userId);
+ if (!serviceState.isHardware) {
+ updateServiceConnectionLocked(serviceState.component, userId);
+ } else {
+ updateHardwareServiceConnectionDelayed(userId);
+ }
}
@GuardedBy("mLock")
@@ -948,13 +940,17 @@
if (serviceState != null) {
serviceState.sessionTokens.remove(sessionToken);
}
- updateServiceConnectionLocked(sessionState.componentName, userId);
+ if (!serviceState.isHardware) {
+ updateServiceConnectionLocked(sessionState.componentName, userId);
+ } else {
+ updateHardwareServiceConnectionDelayed(userId);
+ }
// Log the end of watch.
SomeArgs args = SomeArgs.obtain();
args.arg1 = sessionToken;
args.arg2 = System.currentTimeMillis();
- mWatchLogHandler.obtainMessage(WatchLogHandler.MSG_LOG_WATCH_END, args).sendToTarget();
+ mMessageHandler.obtainMessage(MessageHandler.MSG_LOG_WATCH_END, args).sendToTarget();
}
@GuardedBy("mLock")
@@ -1153,8 +1149,7 @@
ServiceState serviceState = userState.serviceStateMap.get(inputState.info.getComponent());
int oldState = inputState.state;
inputState.state = state;
- if (serviceState != null && serviceState.service == null
- && (!serviceState.sessionTokens.isEmpty() || serviceState.isHardware)) {
+ if (serviceState != null && serviceState.reconnecting) {
// We don't notify state change while reconnecting. It should remain disconnected.
return;
}
@@ -1881,7 +1876,7 @@
args.arg3 = ContentUris.parseId(channelUri);
args.arg4 = params;
args.arg5 = sessionToken;
- mWatchLogHandler.obtainMessage(WatchLogHandler.MSG_LOG_WATCH_START, args)
+ mMessageHandler.obtainMessage(MessageHandler.MSG_LOG_WATCH_START, args)
.sendToTarget();
} catch (RemoteException | SessionNotFoundException e) {
Slog.e(TAG, "error in tune", e);
@@ -3327,16 +3322,21 @@
private final ComponentName component;
private final boolean isHardware;
private final Map<String, TvInputInfo> hardwareInputMap = new HashMap<>();
+ private final List<TvInputHardwareInfo> hardwareDeviceRemovedBuffer = new ArrayList<>();
+ private final List<HdmiDeviceInfo> hdmiDeviceRemovedBuffer = new ArrayList<>();
+ private final List<HdmiDeviceInfo> hdmiDeviceUpdatedBuffer = new ArrayList<>();
private ITvInputService service;
private ServiceCallback callback;
private boolean bound;
private boolean reconnecting;
+ private boolean neverConnected;
private ServiceState(ComponentName component, int userId) {
this.component = component;
this.connection = new InputServiceConnection(component, userId);
this.isHardware = hasHardwarePermission(mContext.getPackageManager(), component);
+ this.neverConnected = true;
}
}
@@ -3449,6 +3449,97 @@
}
}
+ @GuardedBy("mLock")
+ private void bindService(ServiceState serviceState, int userId) {
+ if (serviceState.bound) {
+ // We have already bound to the service so we don't try to bind again until after we
+ // unbind later on.
+ // For hardware services, call updateHardwareServiceConnectionDelayed() to delay the
+ // possible unbinding.
+ if (serviceState.isHardware) {
+ updateHardwareServiceConnectionDelayed(userId);
+ }
+ return;
+ }
+ if (DEBUG) {
+ Slog.d(TAG,
+ "bindServiceAsUser(service=" + serviceState.component + ", userId=" + userId
+ + ")");
+ }
+ Intent i =
+ new Intent(TvInputService.SERVICE_INTERFACE).setComponent(serviceState.component);
+ serviceState.bound = mContext.bindServiceAsUser(i, serviceState.connection,
+ Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE_WHILE_AWAKE,
+ new UserHandle(userId));
+ if (!serviceState.bound) {
+ Slog.e(TAG, "failed to bind " + serviceState.component + " for userId " + userId);
+ mContext.unbindService(serviceState.connection);
+ }
+ }
+
+ @GuardedBy("mLock")
+ private void unbindService(ServiceState serviceState) {
+ if (!serviceState.bound) {
+ return;
+ }
+ if (DEBUG) {
+ Slog.d(TAG, "unbindService(service=" + serviceState.component + ")");
+ }
+ mContext.unbindService(serviceState.connection);
+ serviceState.bound = false;
+ serviceState.service = null;
+ serviceState.callback = null;
+ }
+
+ @GuardedBy("mLock")
+ private void updateHardwareTvInputServiceBindingLocked(int userId) {
+ PackageManager pm = mContext.getPackageManager();
+ List<ResolveInfo> services =
+ pm.queryIntentServicesAsUser(new Intent(TvInputService.SERVICE_INTERFACE),
+ PackageManager.GET_SERVICES | PackageManager.GET_META_DATA, userId);
+ for (ResolveInfo ri : services) {
+ ServiceInfo si = ri.serviceInfo;
+ if (!android.Manifest.permission.BIND_TV_INPUT.equals(si.permission)) {
+ continue;
+ }
+ ComponentName component = new ComponentName(si.packageName, si.name);
+ if (hasHardwarePermission(pm, component)) {
+ updateServiceConnectionLocked(component, userId);
+ }
+ }
+ }
+
+ private void updateHardwareServiceConnectionDelayed(int userId) {
+ mMessageHandler.removeMessages(MessageHandler.MSG_UPDATE_HARDWARE_TIS_BINDING);
+ SomeArgs args = SomeArgs.obtain();
+ args.arg1 = userId;
+ Message msg =
+ mMessageHandler.obtainMessage(MessageHandler.MSG_UPDATE_HARDWARE_TIS_BINDING, args);
+ mMessageHandler.sendMessageDelayed(msg, UPDATE_HARDWARE_TIS_BINDING_DELAY_IN_MILLIS);
+ }
+
+ @GuardedBy("mLock")
+ private void addHardwareInputLocked(
+ TvInputInfo inputInfo, ComponentName component, int userId) {
+ ServiceState serviceState = getServiceStateLocked(component, userId);
+ serviceState.hardwareInputMap.put(inputInfo.getId(), inputInfo);
+ buildTvInputListLocked(userId, null);
+ }
+
+ @GuardedBy("mLock")
+ private void removeHardwareInputLocked(String inputId, int userId) {
+ if (!mTvInputHardwareManager.getInputMap().containsKey(inputId)) {
+ return;
+ }
+ ComponentName component = mTvInputHardwareManager.getInputMap().get(inputId).getComponent();
+ ServiceState serviceState = getServiceStateLocked(component, userId);
+ boolean removed = serviceState.hardwareInputMap.remove(inputId) != null;
+ if (removed) {
+ buildTvInputListLocked(userId, null);
+ mTvInputHardwareManager.removeHardwareInput(inputId);
+ }
+ }
+
private final class InputServiceConnection implements ServiceConnection {
private final ComponentName mComponent;
private final int mUserId;
@@ -3472,6 +3563,7 @@
}
ServiceState serviceState = userState.serviceStateMap.get(mComponent);
serviceState.service = ITvInputService.Stub.asInterface(service);
+ serviceState.neverConnected = false;
// Register a callback, if we need to.
if (serviceState.isHardware && serviceState.callback == null) {
@@ -3483,6 +3575,58 @@
}
}
+ for (TvInputState inputState : userState.inputMap.values()) {
+ if (inputState.info.getComponent().equals(component)
+ && inputState.state != INPUT_STATE_CONNECTED) {
+ notifyInputStateChangedLocked(userState, inputState.info.getId(),
+ inputState.state, null);
+ }
+ }
+
+ if (serviceState.isHardware) {
+ for (TvInputHardwareInfo hardwareToBeRemoved :
+ serviceState.hardwareDeviceRemovedBuffer) {
+ try {
+ serviceState.service.notifyHardwareRemoved(hardwareToBeRemoved);
+ } catch (RemoteException e) {
+ Slog.e(TAG, "error in hardwareDeviceRemovedBuffer", e);
+ }
+ }
+ serviceState.hardwareDeviceRemovedBuffer.clear();
+ for (HdmiDeviceInfo hdmiDeviceToBeRemoved :
+ serviceState.hdmiDeviceRemovedBuffer) {
+ try {
+ serviceState.service.notifyHdmiDeviceRemoved(hdmiDeviceToBeRemoved);
+ } catch (RemoteException e) {
+ Slog.e(TAG, "error in hdmiDeviceRemovedBuffer", e);
+ }
+ }
+ serviceState.hdmiDeviceRemovedBuffer.clear();
+ for (TvInputHardwareInfo hardware : mTvInputHardwareManager.getHardwareList()) {
+ try {
+ serviceState.service.notifyHardwareAdded(hardware);
+ } catch (RemoteException e) {
+ Slog.e(TAG, "error in notifyHardwareAdded", e);
+ }
+ }
+ for (HdmiDeviceInfo device : mTvInputHardwareManager.getHdmiDeviceList()) {
+ try {
+ serviceState.service.notifyHdmiDeviceAdded(device);
+ } catch (RemoteException e) {
+ Slog.e(TAG, "error in notifyHdmiDeviceAdded", e);
+ }
+ }
+ for (HdmiDeviceInfo hdmiDeviceToBeUpdated :
+ serviceState.hdmiDeviceUpdatedBuffer) {
+ try {
+ serviceState.service.notifyHdmiDeviceUpdated(hdmiDeviceToBeUpdated);
+ } catch (RemoteException e) {
+ Slog.e(TAG, "error in hdmiDeviceUpdatedBuffer", e);
+ }
+ }
+ serviceState.hdmiDeviceUpdatedBuffer.clear();
+ }
+
List<IBinder> tokensToBeRemoved = new ArrayList<>();
// And create sessions, if any.
@@ -3496,30 +3640,8 @@
removeSessionStateLocked(sessionToken, mUserId);
}
- for (TvInputState inputState : userState.inputMap.values()) {
- if (inputState.info.getComponent().equals(component)
- && inputState.state != INPUT_STATE_CONNECTED) {
- notifyInputStateChangedLocked(userState, inputState.info.getId(),
- inputState.state, null);
- }
- }
-
if (serviceState.isHardware) {
- serviceState.hardwareInputMap.clear();
- for (TvInputHardwareInfo hardware : mTvInputHardwareManager.getHardwareList()) {
- try {
- serviceState.service.notifyHardwareAdded(hardware);
- } catch (RemoteException e) {
- Slog.e(TAG, "error in notifyHardwareAdded", e);
- }
- }
- for (HdmiDeviceInfo device : mTvInputHardwareManager.getHdmiDeviceList()) {
- try {
- serviceState.service.notifyHdmiDeviceAdded(device);
- } catch (RemoteException e) {
- Slog.e(TAG, "error in notifyHdmiDeviceAdded", e);
- }
- }
+ updateHardwareServiceConnectionDelayed(mUserId);
}
}
}
@@ -3570,13 +3692,6 @@
}
}
- @GuardedBy("mLock")
- private void addHardwareInputLocked(TvInputInfo inputInfo) {
- ServiceState serviceState = getServiceStateLocked(mComponent, mUserId);
- serviceState.hardwareInputMap.put(inputInfo.getId(), inputInfo);
- buildTvInputListLocked(mUserId, null);
- }
-
public void addHardwareInput(int deviceId, TvInputInfo inputInfo) {
ensureHardwarePermission();
ensureValidInput(inputInfo);
@@ -3587,8 +3702,11 @@
if (serviceState.hardwareInputMap.containsKey(inputInfo.getId())) {
return;
}
+ Slog.d("ServiceCallback",
+ "addHardwareInput: device id " + deviceId + ", "
+ + inputInfo.toString());
mTvInputHardwareManager.addHardwareInput(deviceId, inputInfo);
- addHardwareInputLocked(inputInfo);
+ addHardwareInputLocked(inputInfo, mComponent, mUserId);
}
} finally {
Binder.restoreCallingIdentity(identity);
@@ -3606,7 +3724,7 @@
return;
}
mTvInputHardwareManager.addHdmiInput(id, inputInfo);
- addHardwareInputLocked(inputInfo);
+ addHardwareInputLocked(inputInfo, mComponent, mUserId);
if (mOnScreenInputId != null && mOnScreenSessionState != null) {
if (TextUtils.equals(mOnScreenInputId, inputInfo.getParentId())) {
// catch the use case when a CEC device is plugged in an HDMI port,
@@ -3635,14 +3753,9 @@
final long identity = Binder.clearCallingIdentity();
try {
synchronized (mLock) {
- ServiceState serviceState = getServiceStateLocked(mComponent, mUserId);
- boolean removed = serviceState.hardwareInputMap.remove(inputId) != null;
- if (removed) {
- buildTvInputListLocked(mUserId, null);
- mTvInputHardwareManager.removeHardwareInput(inputId);
- } else {
- Slog.e(TAG, "failed to remove input " + inputId);
- }
+ Slog.d("ServiceCallback",
+ "removeHardwareInput " + inputId + " by " + mComponent);
+ removeHardwareInputLocked(inputId, mUserId);
}
} finally {
Binder.restoreCallingIdentity(identity);
@@ -3860,6 +3973,23 @@
}
@Override
+ public void onVideoFreezeUpdated(boolean isFrozen) {
+ synchronized (mLock) {
+ if (DEBUG) {
+ Slog.d(TAG, "onVideoFreezeUpdated(" + isFrozen + ")");
+ }
+ if (mSessionState.session == null || mSessionState.client == null) {
+ return;
+ }
+ try {
+ mSessionState.client.onVideoFreezeUpdated(isFrozen, mSessionState.seq);
+ } catch (RemoteException e) {
+ Slog.e(TAG, "error in onVideoFreezeUpdated", e);
+ }
+ }
+ }
+
+ @Override
public void onContentAllowed() {
synchronized (mLock) {
if (DEBUG) {
@@ -4209,11 +4339,12 @@
return loggedReason;
}
+ @GuardedBy("mLock")
private UserState getUserStateLocked(int userId) {
return mUserStates.get(userId);
}
- private static final class WatchLogHandler extends Handler {
+ private final class MessageHandler extends Handler {
// There are only two kinds of watch events that can happen on the system:
// 1. The current TV input session is tuned to a new channel.
// 2. The session is released for some reason.
@@ -4225,10 +4356,11 @@
static final int MSG_LOG_WATCH_START = 1;
static final int MSG_LOG_WATCH_END = 2;
static final int MSG_SWITCH_CONTENT_RESOLVER = 3;
+ static final int MSG_UPDATE_HARDWARE_TIS_BINDING = 4;
private ContentResolver mContentResolver;
- WatchLogHandler(ContentResolver contentResolver, Looper looper) {
+ MessageHandler(ContentResolver contentResolver, Looper looper) {
super(looper);
mContentResolver = contentResolver;
}
@@ -4287,6 +4419,14 @@
mContentResolver = (ContentResolver) msg.obj;
break;
}
+ case MSG_UPDATE_HARDWARE_TIS_BINDING:
+ SomeArgs args = (SomeArgs) msg.obj;
+ int userId = (int) args.arg1;
+ synchronized (mLock) {
+ updateHardwareTvInputServiceBindingLocked(userId);
+ }
+ args.recycle();
+ break;
default: {
Slog.w(TAG, "unhandled message code: " + msg.what);
break;
@@ -4342,29 +4482,46 @@
UserState userState = getOrCreateUserStateLocked(mCurrentUserId);
// Broadcast the event to all hardware inputs.
for (ServiceState serviceState : userState.serviceStateMap.values()) {
- if (!serviceState.isHardware || serviceState.service == null) continue;
+ if (!serviceState.isHardware) {
+ continue;
+ }
try {
- serviceState.service.notifyHardwareAdded(info);
+ bindService(serviceState, mCurrentUserId);
+ if (serviceState.service != null) {
+ serviceState.service.notifyHardwareAdded(info);
+ }
} catch (RemoteException e) {
Slog.e(TAG, "error in notifyHardwareAdded", e);
}
}
+ updateHardwareServiceConnectionDelayed(mCurrentUserId);
}
}
@Override
public void onHardwareDeviceRemoved(TvInputHardwareInfo info) {
synchronized (mLock) {
+ String relatedInputId =
+ mTvInputHardwareManager.getHardwareInputIdMap().get(info.getDeviceId());
+ removeHardwareInputLocked(relatedInputId, mCurrentUserId);
UserState userState = getOrCreateUserStateLocked(mCurrentUserId);
// Broadcast the event to all hardware inputs.
for (ServiceState serviceState : userState.serviceStateMap.values()) {
- if (!serviceState.isHardware || serviceState.service == null) continue;
+ if (!serviceState.isHardware) {
+ continue;
+ }
try {
- serviceState.service.notifyHardwareRemoved(info);
+ bindService(serviceState, mCurrentUserId);
+ if (serviceState.service != null) {
+ serviceState.service.notifyHardwareRemoved(info);
+ } else {
+ serviceState.hardwareDeviceRemovedBuffer.add(info);
+ }
} catch (RemoteException e) {
Slog.e(TAG, "error in notifyHardwareRemoved", e);
}
}
+ updateHardwareServiceConnectionDelayed(mCurrentUserId);
}
}
@@ -4374,29 +4531,46 @@
UserState userState = getOrCreateUserStateLocked(mCurrentUserId);
// Broadcast the event to all hardware inputs.
for (ServiceState serviceState : userState.serviceStateMap.values()) {
- if (!serviceState.isHardware || serviceState.service == null) continue;
+ if (!serviceState.isHardware) {
+ continue;
+ }
try {
- serviceState.service.notifyHdmiDeviceAdded(deviceInfo);
+ bindService(serviceState, mCurrentUserId);
+ if (serviceState.service != null) {
+ serviceState.service.notifyHdmiDeviceAdded(deviceInfo);
+ }
} catch (RemoteException e) {
Slog.e(TAG, "error in notifyHdmiDeviceAdded", e);
}
}
+ updateHardwareServiceConnectionDelayed(mCurrentUserId);
}
}
@Override
public void onHdmiDeviceRemoved(HdmiDeviceInfo deviceInfo) {
synchronized (mLock) {
+ String relatedInputId =
+ mTvInputHardwareManager.getHdmiInputIdMap().get(deviceInfo.getId());
+ removeHardwareInputLocked(relatedInputId, mCurrentUserId);
UserState userState = getOrCreateUserStateLocked(mCurrentUserId);
// Broadcast the event to all hardware inputs.
for (ServiceState serviceState : userState.serviceStateMap.values()) {
- if (!serviceState.isHardware || serviceState.service == null) continue;
+ if (!serviceState.isHardware) {
+ continue;
+ }
try {
- serviceState.service.notifyHdmiDeviceRemoved(deviceInfo);
+ bindService(serviceState, mCurrentUserId);
+ if (serviceState.service != null) {
+ serviceState.service.notifyHdmiDeviceRemoved(deviceInfo);
+ } else {
+ serviceState.hdmiDeviceRemovedBuffer.add(deviceInfo);
+ }
} catch (RemoteException e) {
Slog.e(TAG, "error in notifyHdmiDeviceRemoved", e);
}
}
+ updateHardwareServiceConnectionDelayed(mCurrentUserId);
}
}
@@ -4424,13 +4598,21 @@
UserState userState = getOrCreateUserStateLocked(mCurrentUserId);
// Broadcast the event to all hardware inputs.
for (ServiceState serviceState : userState.serviceStateMap.values()) {
- if (!serviceState.isHardware || serviceState.service == null) continue;
+ if (!serviceState.isHardware) {
+ continue;
+ }
try {
- serviceState.service.notifyHdmiDeviceUpdated(deviceInfo);
+ bindService(serviceState, mCurrentUserId);
+ if (serviceState.service != null) {
+ serviceState.service.notifyHdmiDeviceUpdated(deviceInfo);
+ } else {
+ serviceState.hdmiDeviceUpdatedBuffer.add(deviceInfo);
+ }
} catch (RemoteException e) {
Slog.e(TAG, "error in notifyHdmiDeviceUpdated", e);
}
}
+ updateHardwareServiceConnectionDelayed(mCurrentUserId);
}
}
diff --git a/services/core/java/com/android/server/tv/interactive/TvInteractiveAppManagerService.java b/services/core/java/com/android/server/tv/interactive/TvInteractiveAppManagerService.java
index 7ab075e..b6d0ca1 100644
--- a/services/core/java/com/android/server/tv/interactive/TvInteractiveAppManagerService.java
+++ b/services/core/java/com/android/server/tv/interactive/TvInteractiveAppManagerService.java
@@ -1038,7 +1038,7 @@
}
} finally {
if (surface != null) {
- // surface is not used in TvInteractiveAppManagerService.
+ // surface is not used in TvAdManagerService.
surface.release();
}
Binder.restoreCallingIdentity(identity);
@@ -1070,6 +1070,253 @@
@Override
public void startAdService(IBinder sessionToken, int userId) {
+ if (DEBUG) {
+ Slogf.d(TAG, "startAdService(userId=%d)", userId);
+ }
+ final int callingUid = Binder.getCallingUid();
+ final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(), callingUid,
+ userId, "startAdService");
+ AdSessionState sessionState = null;
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ synchronized (mLock) {
+ try {
+ sessionState = getAdSessionStateLocked(sessionToken, callingUid,
+ resolvedUserId);
+ getAdSessionLocked(sessionState).startAdService();
+ } catch (RemoteException | SessionNotFoundException e) {
+ Slogf.e(TAG, "error in start", e);
+ }
+ }
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
+
+ @Override
+ public void stopAdService(IBinder sessionToken, int userId) {
+ if (DEBUG) {
+ Slogf.d(TAG, "stopAdService(userId=%d)", userId);
+ }
+ final int callingUid = Binder.getCallingUid();
+ final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(), callingUid,
+ userId, "stopAdService");
+ AdSessionState sessionState = null;
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ synchronized (mLock) {
+ try {
+ sessionState = getAdSessionStateLocked(sessionToken, callingUid,
+ resolvedUserId);
+ getAdSessionLocked(sessionState).stopAdService();
+ } catch (RemoteException | SessionNotFoundException e) {
+ Slogf.e(TAG, "error in stop", e);
+ }
+ }
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
+
+ @Override
+ public void resetAdService(IBinder sessionToken, int userId) {
+ if (DEBUG) {
+ Slogf.d(TAG, "resetAdService(userId=%d)", userId);
+ }
+ final int callingUid = Binder.getCallingUid();
+ final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(), callingUid,
+ userId, "resetAdService");
+ AdSessionState sessionState = null;
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ synchronized (mLock) {
+ try {
+ sessionState = getAdSessionStateLocked(sessionToken, callingUid,
+ resolvedUserId);
+ getAdSessionLocked(sessionState).resetAdService();
+ } catch (RemoteException | SessionNotFoundException e) {
+ Slogf.e(TAG, "error in reset", e);
+ }
+ }
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
+
+ @Override
+ public void sendCurrentVideoBounds(IBinder sessionToken, Rect bounds, int userId) {
+ if (DEBUG) {
+ Slogf.d(TAG, "sendCurrentVideoBounds(bounds=%s)", bounds.toString());
+ }
+ final int callingUid = Binder.getCallingUid();
+ final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(), callingUid,
+ userId, "sendCurrentVideoBounds");
+ AdSessionState sessionState = null;
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ synchronized (mLock) {
+ try {
+ sessionState = getAdSessionStateLocked(sessionToken, callingUid,
+ resolvedUserId);
+ getAdSessionLocked(sessionState).sendCurrentVideoBounds(bounds);
+ } catch (RemoteException | SessionNotFoundException e) {
+ Slogf.e(TAG, "error in sendCurrentVideoBounds", e);
+ }
+ }
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
+
+ @Override
+ public void sendCurrentChannelUri(IBinder sessionToken, Uri channelUri, int userId) {
+ if (DEBUG) {
+ Slogf.d(TAG, "sendCurrentChannelUri(channelUri=%s)", channelUri.toString());
+ }
+ final int callingUid = Binder.getCallingUid();
+ final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(), callingUid,
+ userId, "sendCurrentChannelUri");
+ AdSessionState sessionState = null;
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ synchronized (mLock) {
+ try {
+ sessionState = getAdSessionStateLocked(sessionToken, callingUid,
+ resolvedUserId);
+ getAdSessionLocked(sessionState).sendCurrentChannelUri(channelUri);
+ } catch (RemoteException | SessionNotFoundException e) {
+ Slogf.e(TAG, "error in sendCurrentChannelUri", e);
+ }
+ }
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
+
+ @Override
+ public void sendTrackInfoList(IBinder sessionToken, List<TvTrackInfo> tracks, int userId) {
+ if (DEBUG) {
+ Slogf.d(TAG, "sendTrackInfoList(tracks=%s)", tracks.toString());
+ }
+ final int callingUid = Binder.getCallingUid();
+ final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(), callingUid,
+ userId, "sendTrackInfoList");
+ AdSessionState sessionState = null;
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ synchronized (mLock) {
+ try {
+ sessionState = getAdSessionStateLocked(sessionToken, callingUid,
+ resolvedUserId);
+ getAdSessionLocked(sessionState).sendTrackInfoList(tracks);
+ } catch (RemoteException | SessionNotFoundException e) {
+ Slogf.e(TAG, "error in sendTrackInfoList", e);
+ }
+ }
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
+
+ @Override
+ public void sendCurrentTvInputId(IBinder sessionToken, String inputId, int userId) {
+ if (DEBUG) {
+ Slogf.d(TAG, "sendCurrentTvInputId(inputId=%s)", inputId);
+ }
+ final int callingUid = Binder.getCallingUid();
+ final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(), callingUid,
+ userId, "sendCurrentTvInputId");
+ AdSessionState sessionState = null;
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ synchronized (mLock) {
+ try {
+ sessionState = getAdSessionStateLocked(sessionToken, callingUid,
+ resolvedUserId);
+ getAdSessionLocked(sessionState).sendCurrentTvInputId(inputId);
+ } catch (RemoteException | SessionNotFoundException e) {
+ Slogf.e(TAG, "error in sendCurrentTvInputId", e);
+ }
+ }
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
+
+ @Override
+ public void sendSigningResult(
+ IBinder sessionToken, String signingId, byte[] result, int userId) {
+ if (DEBUG) {
+ Slogf.d(TAG, "sendSigningResult(signingId=%s)", signingId);
+ }
+ final int callingUid = Binder.getCallingUid();
+ final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(), callingUid,
+ userId, "sendSigningResult");
+ AdSessionState sessionState = null;
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ synchronized (mLock) {
+ try {
+ sessionState = getAdSessionStateLocked(sessionToken, callingUid,
+ resolvedUserId);
+ getAdSessionLocked(sessionState).sendSigningResult(signingId, result);
+ } catch (RemoteException | SessionNotFoundException e) {
+ Slogf.e(TAG, "error in sendSigningResult", e);
+ }
+ }
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
+
+ @Override
+ public void notifyError(IBinder sessionToken, String errMsg, Bundle params, int userId) {
+ if (DEBUG) {
+ Slogf.d(TAG, "notifyError(errMsg=%s)", errMsg);
+ }
+ final int callingUid = Binder.getCallingUid();
+ final int resolvedUserId =
+ resolveCallingUserId(Binder.getCallingPid(), callingUid, userId, "notifyError");
+ AdSessionState sessionState = null;
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ synchronized (mLock) {
+ try {
+ sessionState =
+ getAdSessionStateLocked(sessionToken, callingUid, resolvedUserId);
+ getAdSessionLocked(sessionState).notifyError(errMsg, params);
+ } catch (RemoteException | SessionNotFoundException e) {
+ Slogf.e(TAG, "error in notifyError", e);
+ }
+ }
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
+
+ @Override
+ public void notifyTvMessage(IBinder sessionToken, int type, Bundle data, int userId) {
+ if (DEBUG) {
+ Slogf.d(TAG, "notifyTvMessage(type=%d)", type);
+ }
+ final int callingUid = Binder.getCallingUid();
+ final int callingPid = Binder.getCallingPid();
+ final int resolvedUserId = resolveCallingUserId(callingPid, callingUid, userId,
+ "notifyTvMessage");
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ synchronized (mLock) {
+ try {
+ AdSessionState sessionState =
+ getAdSessionStateLocked(sessionToken, callingUid, resolvedUserId);
+ getAdSessionLocked(sessionState).notifyTvMessage(type, data);
+ } catch (RemoteException | SessionNotFoundException e) {
+ Slogf.e(TAG, "error in notifyTvMessage", e);
+ }
+ }
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
}
@Override
@@ -1106,6 +1353,67 @@
}
}
+ @Override
+ public void createMediaView(IBinder sessionToken, IBinder windowToken, Rect frame,
+ int userId) {
+ final int callingUid = Binder.getCallingUid();
+ final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(), callingUid,
+ userId, "createMediaView");
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ synchronized (mLock) {
+ try {
+ getAdSessionLocked(sessionToken, callingUid, resolvedUserId)
+ .createMediaView(windowToken, frame);
+ } catch (RemoteException | SessionNotFoundException e) {
+ Slog.e(TAG, "error in createMediaView", e);
+ }
+ }
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
+
+ @Override
+ public void relayoutMediaView(IBinder sessionToken, Rect frame, int userId) {
+ final int callingUid = Binder.getCallingUid();
+ final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(), callingUid,
+ userId, "relayoutMediaView");
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ synchronized (mLock) {
+ try {
+ getAdSessionLocked(sessionToken, callingUid, resolvedUserId)
+ .relayoutMediaView(frame);
+ } catch (RemoteException | SessionNotFoundException e) {
+ Slog.e(TAG, "error in relayoutMediaView", e);
+ }
+ }
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
+
+ @Override
+ public void removeMediaView(IBinder sessionToken, int userId) {
+ final int callingUid = Binder.getCallingUid();
+ final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(), callingUid,
+ userId, "removeMediaView");
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ synchronized (mLock) {
+ try {
+ getAdSessionLocked(sessionToken, callingUid, resolvedUserId)
+ .removeMediaView();
+ } catch (RemoteException | SessionNotFoundException e) {
+ Slog.e(TAG, "error in removeMediaView", e);
+ }
+ }
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
+
}
private final class BinderService extends ITvInteractiveAppManager.Stub {
@@ -1470,6 +1778,28 @@
}
@Override
+ public void notifyVideoFreezeUpdated(IBinder sessionToken, boolean isFrozen, int userId) {
+ final int callingUid = Binder.getCallingUid();
+ final int callingPid = Binder.getCallingPid();
+ final int resolvedUserId = resolveCallingUserId(callingPid, callingUid, userId,
+ "notifyVideoFreezeUpdated");
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ synchronized (mLock) {
+ try {
+ SessionState sessionState = getSessionStateLocked(sessionToken, callingUid,
+ resolvedUserId);
+ getSessionLocked(sessionState).notifyVideoFreezeUpdated(isFrozen);
+ } catch (RemoteException | SessionNotFoundException e) {
+ Slogf.e(TAG, "error in notifyVideoFreezeUpdated", e);
+ }
+ }
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
+
+ @Override
public void notifyContentAllowed(IBinder sessionToken, int userId) {
final int callingUid = Binder.getCallingUid();
final int callingPid = Binder.getCallingPid();
@@ -1557,7 +1887,6 @@
}
}
-
@Override
public void notifyRecordingStarted(IBinder sessionToken, String recordingId,
String requestId, int userId) {
@@ -2061,6 +2390,33 @@
}
@Override
+ public void sendCertificate(IBinder sessionToken, String host, int port,
+ Bundle certBundle, int userId) {
+ if (DEBUG) {
+ Slogf.d(TAG, "sendCertificate(host=%s port=%d cert=%s)", host, port,
+ certBundle);
+ }
+ final int callingUid = Binder.getCallingUid();
+ final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(), callingUid,
+ userId, "sendCertificate");
+ SessionState sessionState = null;
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ synchronized (mLock) {
+ try {
+ sessionState = getSessionStateLocked(sessionToken, callingUid,
+ resolvedUserId);
+ getSessionLocked(sessionState).sendCertificate(host, port, certBundle);
+ } catch (RemoteException | SessionNotFoundException e) {
+ Slogf.e(TAG, "error in sendCertificate", e);
+ }
+ }
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
+
+ @Override
public void notifyError(IBinder sessionToken, String errMsg, Bundle params, int userId) {
if (DEBUG) {
Slogf.d(TAG, "notifyError(errMsg=%s)", errMsg);
@@ -3796,6 +4152,24 @@
}
@Override
+ public void onRequestCertificate(String host, int port) {
+ synchronized (mLock) {
+ if (DEBUG) {
+ Slogf.d(TAG, "onRequestCertificate");
+ }
+ if (mSessionState.mSession == null || mSessionState.mClient == null) {
+ return;
+ }
+ try {
+ mSessionState.mClient.onRequestCertificate(host, port, mSessionState.mSeq);
+ } catch (RemoteException e) {
+ Slogf.e(TAG, "error in onRequestCertificate", e);
+ }
+ }
+ }
+
+
+ @Override
public void onAdRequest(AdRequest request) {
synchronized (mLock) {
if (DEBUG) {
diff --git a/services/core/java/com/android/server/wallpaper/WallpaperCropper.java b/services/core/java/com/android/server/wallpaper/WallpaperCropper.java
index 51acc8e..8549957 100644
--- a/services/core/java/com/android/server/wallpaper/WallpaperCropper.java
+++ b/services/core/java/com/android/server/wallpaper/WallpaperCropper.java
@@ -410,9 +410,10 @@
// adapt the entries in wallpaper.mCropHints for the actual display
SparseArray<Rect> updatedCropHints = new SparseArray<>();
for (int i = 0; i < wallpaper.mCropHints.size(); i++) {
- Rect defaultCrop = defaultDisplayCrops.valueAt(i);
+ int orientation = wallpaper.mCropHints.keyAt(i);
+ Rect defaultCrop = defaultDisplayCrops.get(orientation);
if (defaultCrop != null) {
- updatedCropHints.put(defaultDisplayCrops.keyAt(i), defaultCrop);
+ updatedCropHints.put(orientation, defaultCrop);
}
}
wallpaper.mCropHints = updatedCropHints;
diff --git a/services/core/java/com/android/server/wallpaper/WallpaperDisplayHelper.java b/services/core/java/com/android/server/wallpaper/WallpaperDisplayHelper.java
index 19fd9a9..9e1b5d2 100644
--- a/services/core/java/com/android/server/wallpaper/WallpaperDisplayHelper.java
+++ b/services/core/java/com/android/server/wallpaper/WallpaperDisplayHelper.java
@@ -96,7 +96,8 @@
}
if (populateOrientationPairs) {
int orientation = WallpaperManager.getOrientation(displaySize);
- float newSurface = displaySize.x * displaySize.y * metric.getDensity();
+ float newSurface = displaySize.x * displaySize.y
+ / (metric.getDensity() * metric.getDensity());
if (surface <= 0) {
surface = newSurface;
firstOrientation = orientation;
diff --git a/services/core/java/com/android/server/wearable/WearableSensingManagerService.java b/services/core/java/com/android/server/wearable/WearableSensingManagerService.java
index 106be5f..4cc2c02 100644
--- a/services/core/java/com/android/server/wearable/WearableSensingManagerService.java
+++ b/services/core/java/com/android/server/wearable/WearableSensingManagerService.java
@@ -48,6 +48,7 @@
import java.io.FileDescriptor;
import java.util.Objects;
import java.util.Set;
+import java.util.function.Consumer;
/**
* System service for managing sensing {@link AmbientContextEvent}s on Wearables.
@@ -191,9 +192,23 @@
}
}
+ private void callPerUserServiceIfExist(
+ Consumer<WearableSensingManagerPerUserService> serviceConsumer,
+ RemoteCallback statusCallback) {
+ int userId = UserHandle.getCallingUserId();
+ synchronized (mLock) {
+ WearableSensingManagerPerUserService service = getServiceForUserLocked(userId);
+ if (service == null) {
+ Slog.w(TAG, "Service not available for userId " + userId);
+ WearableSensingManagerPerUserService.notifyStatusCallback(statusCallback,
+ WearableSensingManager.STATUS_SERVICE_UNAVAILABLE);
+ return;
+ }
+ serviceConsumer.accept(service);
+ }
+ }
+
private final class WearableSensingManagerInternal extends IWearableSensingManager.Stub {
- final WearableSensingManagerPerUserService mService = getServiceForUserLocked(
- UserHandle.getCallingUserId());
@Override
public void provideDataStream(
@@ -210,7 +225,9 @@
WearableSensingManager.STATUS_SERVICE_UNAVAILABLE);
return;
}
- mService.onProvideDataStream(parcelFileDescriptor, callback);
+ callPerUserServiceIfExist(
+ service -> service.onProvideDataStream(parcelFileDescriptor, callback),
+ callback);
}
@Override
@@ -229,7 +246,9 @@
WearableSensingManager.STATUS_SERVICE_UNAVAILABLE);
return;
}
- mService.onProvidedData(data, sharedMemory, callback);
+ callPerUserServiceIfExist(
+ service -> service.onProvidedData(data, sharedMemory, callback),
+ callback);
}
@Override
diff --git a/services/core/java/com/android/server/wm/AccessibilityController.java b/services/core/java/com/android/server/wm/AccessibilityController.java
index 2d584c4..f2d9bf8 100644
--- a/services/core/java/com/android/server/wm/AccessibilityController.java
+++ b/services/core/java/com/android/server/wm/AccessibilityController.java
@@ -462,17 +462,16 @@
}
}
- // TODO(b/318327737): Remove parameter 't' when removing flag DRAW_IN_WM_LOCK.
- void drawMagnifiedRegionBorderIfNeeded(int displayId, SurfaceControl.Transaction t) {
+ void drawMagnifiedRegionBorderIfNeeded(int displayId) {
if (mAccessibilityTracing.isTracingEnabled(FLAGS_MAGNIFICATION_CALLBACK)) {
mAccessibilityTracing.logTrace(
TAG + ".drawMagnifiedRegionBorderIfNeeded",
FLAGS_MAGNIFICATION_CALLBACK,
- "displayId=" + displayId + "; transaction={" + t + "}");
+ "displayId=" + displayId);
}
final DisplayMagnifier displayMagnifier = mDisplayMagnifiers.get(displayId);
if (displayMagnifier != null) {
- displayMagnifier.drawMagnifiedRegionBorderIfNeeded(t);
+ displayMagnifier.drawMagnifiedRegionBorderIfNeeded();
}
// Not relevant for the window observer.
}
@@ -870,12 +869,12 @@
.sendToTarget();
}
- void drawMagnifiedRegionBorderIfNeeded(SurfaceControl.Transaction t) {
+ void drawMagnifiedRegionBorderIfNeeded() {
if (mAccessibilityTracing.isTracingEnabled(FLAGS_MAGNIFICATION_CALLBACK)) {
mAccessibilityTracing.logTrace(LOG_TAG + ".drawMagnifiedRegionBorderIfNeeded",
- FLAGS_MAGNIFICATION_CALLBACK, "transition={" + t + "}");
+ FLAGS_MAGNIFICATION_CALLBACK);
}
- mMagnifedViewport.drawWindowIfNeeded(t);
+ mMagnifedViewport.drawWindowIfNeeded();
}
void dump(PrintWriter pw, String prefix) {
@@ -1121,14 +1120,6 @@
}
void setMagnifiedRegionBorderShown(boolean shown, boolean animate) {
- if (ViewportWindow.DRAW_IN_WM_LOCK) {
- if (shown) {
- mFullRedrawNeeded = true;
- mOldMagnificationRegion.set(0, 0, 0, 0);
- }
- mWindow.setShown(shown, animate);
- return;
- }
if (mWindow.setShown(shown, animate)) {
mFullRedrawNeeded = true;
// Clear the old region, so recomputeBounds will refresh the current region.
@@ -1151,12 +1142,8 @@
return mMagnificationSpec;
}
- void drawWindowIfNeeded(SurfaceControl.Transaction t) {
+ void drawWindowIfNeeded() {
recomputeBounds();
- if (ViewportWindow.DRAW_IN_WM_LOCK) {
- mWindow.drawOrRemoveIfNeeded(t);
- return;
- }
mWindow.postDrawIfNeeded();
}
@@ -1187,8 +1174,6 @@
private final class ViewportWindow implements Runnable {
private static final String SURFACE_TITLE = "Magnification Overlay";
- // TODO(b/318327737): Remove if it is stable.
- static final boolean DRAW_IN_WM_LOCK = !Flags.drawMagnifierBorderOutsideWmlock();
private final Region mBounds = new Region();
private final Rect mDirtyRect = new Rect();
@@ -1328,14 +1313,14 @@
@Override
public void run() {
- drawOrRemoveIfNeeded(mTransaction);
+ drawOrRemoveIfNeeded();
}
/**
* This method must only be called by animation handler directly to make sure
* thread safe and there is no lock held outside.
*/
- private void drawOrRemoveIfNeeded(SurfaceControl.Transaction t) {
+ private void drawOrRemoveIfNeeded() {
// Drawing variables (alpha, dirty rect, and bounds) access is synchronized
// using WindowManagerGlobalLock. Grab copies of these values before
// drawing on the canvas so that drawing can be performed outside of the lock.
@@ -1343,7 +1328,7 @@
Rect drawingRect = null;
Region drawingBounds = null;
synchronized (mService.mGlobalLock) {
- if (!DRAW_IN_WM_LOCK && mBlastBufferQueue.mNativeObject == 0) {
+ if (mBlastBufferQueue.mNativeObject == 0) {
// Complete removal since releaseSurface has been called.
if (mSurface.isValid()) {
mTransaction.remove(mSurfaceControl).apply();
@@ -1388,16 +1373,8 @@
mPaint.setAlpha(alpha);
canvas.drawPath(drawingBounds.getBoundaryPath(), mPaint);
mSurface.unlockCanvasAndPost(canvas);
- if (DRAW_IN_WM_LOCK) {
- t.show(mSurfaceControl);
- return;
- }
showSurface = true;
} else {
- if (DRAW_IN_WM_LOCK) {
- t.hide(mSurfaceControl);
- return;
- }
showSurface = false;
}
@@ -1413,11 +1390,6 @@
@GuardedBy("mService.mGlobalLock")
void releaseSurface() {
mBlastBufferQueue.destroy();
- if (DRAW_IN_WM_LOCK) {
- mService.mTransactionFactory.get().remove(mSurfaceControl).apply();
- mSurface.release();
- return;
- }
// Post to perform cleanup on the thread which handles mSurface.
mService.mAnimationHandler.post(this);
}
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index 9b1f9c8..d9fa01e 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -141,6 +141,7 @@
import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_STARTING_WINDOW;
import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_STATES;
import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_SWITCH;
+import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS_MIN;
import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANGED__STATE__LETTERBOXED_FOR_ASPECT_RATIO;
import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANGED__STATE__LETTERBOXED_FOR_FIXED_ORIENTATION;
import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANGED__STATE__LETTERBOXED_FOR_SIZE_COMPAT_MODE;
@@ -991,6 +992,9 @@
private CustomAppTransition mCustomOpenTransition;
private CustomAppTransition mCustomCloseTransition;
+ /** Non-zero to pause dispatching configuration changes to the client. */
+ int mPauseConfigurationDispatchCount = 0;
+
private final Runnable mPauseTimeoutRunnable = new Runnable() {
@Override
public void run() {
@@ -2631,10 +2635,20 @@
if (snapshot == null) {
return false;
}
- if (!snapshot.getTopActivityComponent().equals(mActivityComponent)) {
- // Obsoleted snapshot.
- return false;
- }
+ return isSnapshotComponentCompatible(snapshot) && isSnapshotOrientationCompatible(snapshot);
+ }
+
+ /**
+ * Returns {@code true} if the top activity component of task snapshot equals to this activity.
+ */
+ boolean isSnapshotComponentCompatible(@NonNull TaskSnapshot snapshot) {
+ return snapshot.getTopActivityComponent().equals(mActivityComponent);
+ }
+
+ /**
+ * Returns {@code true} if the orientation of task snapshot is compatible with this activity.
+ */
+ boolean isSnapshotOrientationCompatible(@NonNull TaskSnapshot snapshot) {
final int rotation = mDisplayContent.rotationForActivityInDifferentOrientation(this);
final int currentRotation = task.getWindowConfiguration().getRotation();
final int targetRotation = rotation != ROTATION_UNDEFINED
@@ -3965,20 +3979,6 @@
return removedFromHistory;
}
- boolean safelyDestroy(String reason) {
- if (isDestroyable()) {
- if (DEBUG_SWITCH) {
- final Task task = getTask();
- Slog.v(TAG_SWITCH, "Safely destroying " + this + " in state " + getState()
- + " resumed=" + task.getTopResumedActivity()
- + " pausing=" + task.getTopPausingActivity()
- + " for reason " + reason);
- }
- return destroyImmediately(reason);
- }
- return false;
- }
-
/** Note: call {@link #cleanUp(boolean, boolean)} before this method. */
void removeFromHistory(String reason) {
finishActivityResults(Activity.RESULT_CANCELED,
@@ -4047,10 +4047,6 @@
}
}
- boolean isFinishing() {
- return finishing;
- }
-
/**
* This method is to only be called from the client via binder when the activity is destroyed
* AND finished.
@@ -7978,6 +7974,7 @@
if (mLetterboxUiController.shouldIgnoreRequestedOrientation(requestedOrientation)) {
return;
}
+ final int originalRelaunchingCount = mPendingRelaunchCount;
// This is necessary in order to avoid going into size compat mode when the orientation
// change request comes from the app
if (getRequestedConfigurationOrientation(false, requestedOrientation)
@@ -7995,8 +7992,10 @@
// the request is handled at task level with letterbox.
if (!getMergedOverrideConfiguration().equals(
mLastReportedConfiguration.getMergedConfiguration())) {
- ensureActivityConfiguration(
- false /* ignoreVisibility */, true /* isRequestedOrientationChanged */);
+ ensureActivityConfiguration(false /* ignoreVisibility */);
+ if (mPendingRelaunchCount > originalRelaunchingCount) {
+ mLetterboxUiController.setRelaunchingAfterRequestedOrientationChanged(true);
+ }
if (mTransitionController.inPlayingTransition(this)) {
mTransitionController.mValidateActivityCompat.add(this);
}
@@ -9291,6 +9290,59 @@
}
}
+ @Override
+ void dispatchConfigurationToChild(WindowState child, Configuration config) {
+ if (isConfigurationDispatchPaused()) {
+ return;
+ }
+ super.dispatchConfigurationToChild(child, config);
+ }
+
+ /**
+ * Pauses dispatch of configuration changes to the client. This includes any
+ * configuration-triggered lifecycle changes, WindowState configs, and surface changes. If
+ * a lifecycle change comes from another source (eg. stop), it will still run but will use the
+ * paused configuration.
+ *
+ * The main way this works is by blocking calls to {@link #updateReportedConfigurationAndSend}.
+ * That method is responsible for evaluating whether the activity needs to be relaunched and
+ * sending configurations.
+ */
+ void pauseConfigurationDispatch() {
+ ++mPauseConfigurationDispatchCount;
+ if (mPauseConfigurationDispatchCount == 1) {
+ ProtoLog.v(WM_DEBUG_WINDOW_TRANSITIONS_MIN, "Pausing configuration dispatch for "
+ + " %s", this);
+ }
+ }
+
+ /** @return `true` if configuration actually changed. */
+ boolean resumeConfigurationDispatch() {
+ --mPauseConfigurationDispatchCount;
+ if (mPauseConfigurationDispatchCount > 0) {
+ return false;
+ }
+ ProtoLog.v(WM_DEBUG_WINDOW_TRANSITIONS_MIN, "Resuming configuration dispatch for %s", this);
+ if (mPauseConfigurationDispatchCount < 0) {
+ Slog.wtf(TAG, "Trying to resume non-paused configuration dispatch");
+ mPauseConfigurationDispatchCount = 0;
+ return false;
+ }
+ if (mLastReportedDisplayId == getDisplayId()
+ && getConfiguration().equals(mLastReportedConfiguration.getMergedConfiguration())) {
+ return false;
+ }
+ for (int i = getChildCount() - 1; i >= 0; --i) {
+ dispatchConfigurationToChild(getChildAt(i), getConfiguration());
+ }
+ updateReportedConfigurationAndSend();
+ return true;
+ }
+
+ boolean isConfigurationDispatchPaused() {
+ return mPauseConfigurationDispatchCount > 0;
+ }
+
private boolean applyAspectRatio(Rect outBounds, Rect containingAppBounds,
Rect containingBounds) {
return applyAspectRatio(outBounds, containingAppBounds, containingBounds,
@@ -9502,11 +9554,6 @@
return ensureActivityConfiguration(false /* ignoreVisibility */);
}
- boolean ensureActivityConfiguration(boolean ignoreVisibility) {
- return ensureActivityConfiguration(ignoreVisibility,
- false /* isRequestedOrientationChanged */);
- }
-
/**
* Make sure the given activity matches the current configuration. Ensures the HistoryRecord
* is updated with the correct configuration and all other bookkeeping is handled.
@@ -9515,13 +9562,10 @@
* (stopped state). This is useful for the case where we know the
* activity will be visible soon and we want to ensure its configuration
* before we make it visible.
- * @param isRequestedOrientationChanged whether this is triggered in response to an app calling
- * {@link android.app.Activity#setRequestedOrientation}.
* @return False if the activity was relaunched and true if it wasn't relaunched because we
* can't or the app handles the specific configuration that is changing.
*/
- boolean ensureActivityConfiguration(boolean ignoreVisibility,
- boolean isRequestedOrientationChanged) {
+ boolean ensureActivityConfiguration(boolean ignoreVisibility) {
final Task rootTask = getRootTask();
if (rootTask.mConfigWillChange) {
ProtoLog.v(WM_DEBUG_CONFIGURATION, "Skipping config check "
@@ -9548,6 +9592,17 @@
return true;
}
+ if (isConfigurationDispatchPaused()) {
+ return true;
+ }
+
+ return updateReportedConfigurationAndSend();
+ }
+
+ boolean updateReportedConfigurationAndSend() {
+ if (isConfigurationDispatchPaused()) {
+ Slog.wtf(TAG, "trying to update reported(client) config while dispatch is paused");
+ }
ProtoLog.v(WM_DEBUG_CONFIGURATION, "Ensuring correct "
+ "configuration: %s", this);
@@ -9658,9 +9713,6 @@
} else {
mRelaunchReason = RELAUNCH_REASON_NONE;
}
- if (isRequestedOrientationChanged) {
- mLetterboxUiController.setRelaunchingAfterRequestedOrientationChanged(true);
- }
if (mState == PAUSING) {
// A little annoying: we are waiting for this activity to finish pausing. Let's not
// do anything now, but just flag that it needs to be restarted when done pausing.
diff --git a/services/core/java/com/android/server/wm/ActivitySecurityModelFeatureFlags.java b/services/core/java/com/android/server/wm/ActivitySecurityModelFeatureFlags.java
index f1a2159..db27f60 100644
--- a/services/core/java/com/android/server/wm/ActivitySecurityModelFeatureFlags.java
+++ b/services/core/java/com/android/server/wm/ActivitySecurityModelFeatureFlags.java
@@ -43,7 +43,7 @@
static final String DOC_LINK = "go/android-asm";
/** Used to determine which version of the ASM logic was used in logs while we iterate */
- static final int ASM_VERSION = 8;
+ static final int ASM_VERSION = 9;
private static final String NAMESPACE = NAMESPACE_WINDOW_MANAGER;
private static final String KEY_ASM_PREFIX = "ActivitySecurity__";
diff --git a/services/core/java/com/android/server/wm/ActivityStarter.java b/services/core/java/com/android/server/wm/ActivityStarter.java
index f6d77ea..d6f52b8 100644
--- a/services/core/java/com/android/server/wm/ActivityStarter.java
+++ b/services/core/java/com/android/server/wm/ActivityStarter.java
@@ -2053,8 +2053,8 @@
}
if (!mSupervisor.getBackgroundActivityLaunchController().checkActivityAllowedToStart(
- mSourceRecord, r, newTask, targetTask, mLaunchFlags, mBalCode, mCallingUid,
- mRealCallingUid)) {
+ mSourceRecord, r, newTask, avoidMoveToFront(), targetTask, mLaunchFlags, mBalCode,
+ mCallingUid, mRealCallingUid)) {
return START_ABORTED;
}
diff --git a/services/core/java/com/android/server/wm/BackNavigationController.java b/services/core/java/com/android/server/wm/BackNavigationController.java
index 2bd49bf..83ccbdc 100644
--- a/services/core/java/com/android/server/wm/BackNavigationController.java
+++ b/services/core/java/com/android/server/wm/BackNavigationController.java
@@ -19,6 +19,7 @@
import static android.app.ActivityTaskManager.INVALID_TASK_ID;
import static android.view.RemoteAnimationTarget.MODE_CLOSING;
import static android.view.RemoteAnimationTarget.MODE_OPENING;
+import static android.view.View.FOCUS_FORWARD;
import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
import static android.view.WindowManager.TRANSIT_CLOSE;
import static android.view.WindowManager.TRANSIT_OLD_NONE;
@@ -60,6 +61,7 @@
import com.android.internal.policy.TransitionAnimation;
import com.android.internal.protolog.common.ProtoLog;
import com.android.server.wm.utils.InsetUtils;
+import com.android.window.flags.Flags;
import java.io.PrintWriter;
import java.util.ArrayList;
@@ -101,10 +103,6 @@
static final boolean sPredictBackEnable =
SystemProperties.getBoolean("persist.wm.debug.predictive_back", true);
- static boolean isScreenshotEnabled() {
- return SystemProperties.getInt("persist.wm.debug.predictive_back_screenshot", 0) != 0;
- }
-
// Notify focus window changed
void onFocusChanged(WindowState newFocus) {
mNavigationMonitor.onFocusWindowChanged(newFocus);
@@ -167,6 +165,24 @@
return null;
}
+ // Move focus to the adjacent embedded window if it is higher than this window
+ final TaskFragment taskFragment = window.getTaskFragment();
+ final TaskFragment adjacentTaskFragment =
+ taskFragment != null ? taskFragment.getAdjacentTaskFragment() : null;
+ if (adjacentTaskFragment != null && taskFragment.isEmbedded()
+ && Flags.embeddedActivityBackNavFlag()) {
+ final WindowContainer parent = taskFragment.getParent();
+ if (parent.mChildren.indexOf(taskFragment) < parent.mChildren.indexOf(
+ adjacentTaskFragment)) {
+ mWindowManagerService.moveFocusToAdjacentWindow(window, FOCUS_FORWARD);
+ window = wmService.getFocusedWindowLocked();
+ if (window == null) {
+ Slog.e(TAG, "Adjacent window is null, returning null.");
+ return null;
+ }
+ }
+ }
+
// This is needed to bridge the old and new back behavior with recents. While in
// Overview with live tile enabled, the previous app is technically focused but we
// add an input consumer to capture all input that would otherwise go to the apps
@@ -290,9 +306,11 @@
// keyguard locked and activities are unable to show when locked.
backType = BackNavigationInfo.TYPE_CALLBACK;
}
+ } else if (currentTask.mAtmService.getLockTaskController().isTaskLocked(currentTask)) {
+ // Do not predict if current task is in task locked.
+ backType = BackNavigationInfo.TYPE_CALLBACK;
} else {
- // TODO(208789724): Create single source of truth for this, maybe in
- // RootWindowContainer
+ // Check back-to-home or cross-task
prevTask = currentTask.mRootWindowContainer.getTask(t -> {
if (t.showToCurrentUser() && !t.mChildren.isEmpty()) {
final ActivityRecord ar = t.getTopNonFinishingActivity();
@@ -938,6 +956,18 @@
return;
}
+ // Start fixed rotation for previous activity before create animation.
+ if (openingActivities.length == 1) {
+ final ActivityRecord next = openingActivities[0];
+ final DisplayContent dc = next.mDisplayContent;
+ dc.rotateInDifferentOrientationIfNeeded(next);
+ if (next.hasFixedRotationTransform()) {
+ // Set the record so we can recognize it to continue to update display
+ // orientation if the previous activity becomes the top later.
+ dc.setFixedRotationLaunchingApp(next,
+ next.getWindowConfiguration().getRotation());
+ }
+ }
mOpenAnimAdaptor = new BackWindowAnimationAdaptorWrapper(true, mSwitchType, open);
if (!mOpenAnimAdaptor.isValid()) {
Slog.w(TAG, "compose animations fail, skip");
@@ -1603,16 +1633,6 @@
}
activity.mLaunchTaskBehind = true;
- // Handle fixed rotation launching app.
- final DisplayContent dc = activity.mDisplayContent;
- dc.rotateInDifferentOrientationIfNeeded(activity);
- if (activity.hasFixedRotationTransform()) {
- // Set the record so we can recognize it to continue to update display
- // orientation if the previous activity becomes the top later.
- dc.setFixedRotationLaunchingApp(activity,
- activity.getWindowConfiguration().getRotation());
- }
-
ProtoLog.d(WM_DEBUG_BACK_PREVIEW,
"Setting Activity.mLauncherTaskBehind to true. Activity=%s", activity);
activity.mTaskSupervisor.mStoppingActivities.remove(activity);
@@ -1680,21 +1700,38 @@
static TaskSnapshot getSnapshot(@NonNull WindowContainer w,
ActivityRecord[] visibleOpenActivities) {
+ TaskSnapshot snapshot = null;
if (w.asTask() != null) {
final Task task = w.asTask();
- return task.mRootWindowContainer.mWindowManager.mTaskSnapshotController.getSnapshot(
+ snapshot = task.mRootWindowContainer.mWindowManager.mTaskSnapshotController.getSnapshot(
task.mTaskId, task.mUserId, false /* restoreFromDisk */,
false /* isLowResolution */);
- }
-
- if (w.asActivityRecord() != null) {
+ } else if (w.asActivityRecord() != null) {
final ActivityRecord ar = w.asActivityRecord();
- return ar.mWmService.mSnapshotController.mActivitySnapshotController
+ snapshot = ar.mWmService.mSnapshotController.mActivitySnapshotController
.getSnapshot(visibleOpenActivities);
}
- return null;
+
+ return isSnapshotCompatible(snapshot, visibleOpenActivities) ? snapshot : null;
}
+ static boolean isSnapshotCompatible(@NonNull TaskSnapshot snapshot,
+ @NonNull ActivityRecord[] visibleOpenActivities) {
+ if (snapshot == null) {
+ return false;
+ }
+ boolean oneComponentMatch = false;
+ for (int i = visibleOpenActivities.length - 1; i >= 0; --i) {
+ final ActivityRecord ar = visibleOpenActivities[i];
+ if (!ar.isSnapshotOrientationCompatible(snapshot)) {
+ return false;
+ }
+ oneComponentMatch |= ar.isSnapshotComponentCompatible(snapshot);
+ }
+ return oneComponentMatch;
+ }
+
+
void setWindowManager(WindowManagerService wm) {
mWindowManagerService = wm;
mAnimationHandler = new AnimationHandler(wm);
diff --git a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java
index 0f36d8e..4681396 100644
--- a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java
+++ b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java
@@ -34,6 +34,7 @@
import static com.android.server.wm.ActivityTaskManagerService.APP_SWITCH_ALLOW;
import static com.android.server.wm.ActivityTaskManagerService.APP_SWITCH_FG_ONLY;
import static com.android.server.wm.ActivityTaskSupervisor.getApplicationLabel;
+import static com.android.window.flags.Flags.balImproveRealCallerVisibilityCheck;
import static com.android.window.flags.Flags.balRequireOptInByPendingIntentCreator;
import static com.android.window.flags.Flags.balRequireOptInSameUid;
import static com.android.window.flags.Flags.balShowToasts;
@@ -57,6 +58,7 @@
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.Process;
+import android.os.SystemClock;
import android.os.UserHandle;
import android.provider.DeviceConfig;
import android.util.ArraySet;
@@ -69,7 +71,6 @@
import com.android.internal.util.Preconditions;
import com.android.server.UiThread;
import com.android.server.am.PendingIntentRecord;
-import com.android.window.flags.Flags;
import java.lang.annotation.Retention;
import java.util.HashMap;
@@ -274,10 +275,13 @@
@BackgroundActivityStartMode int realCallerBackgroundActivityStartMode =
checkedOptions.getPendingIntentBackgroundActivityStartMode();
- if (balRequireOptInByPendingIntentCreator() && originatingPendingIntent == null) {
- mAutoOptInReason = "notPendingIntent";
- } else if (balRequireOptInByPendingIntentCreator() && mIsCallForResult) {
+ if (!balImproveRealCallerVisibilityCheck()) {
+ // without this fix the auto-opt ins below would violate CTS tests
+ mAutoOptInReason = null;
+ } else if (mIsCallForResult) {
mAutoOptInReason = "callForResult";
+ } else if (originatingPendingIntent == null) {
+ mAutoOptInReason = "notPendingIntent";
} else if (callingUid == realCallingUid && !balRequireOptInSameUid()) {
mAutoOptInReason = "sameUid";
} else {
@@ -713,31 +717,6 @@
boolean callerCanAllow = resultForCaller.allows() && !state.callerExplicitOptOut();
boolean realCallerCanAllow = resultForRealCaller.allows()
&& !state.realCallerExplicitOptOut();
- if (callerCanAllow && realCallerCanAllow) {
- // Both caller and real caller allow with system defined behavior
- if (state.mBalAllowedByPiCreatorWithHardening.allowsBackgroundActivityStarts()) {
- // Will be allowed even with BAL hardening.
- if (DEBUG_ACTIVITY_STARTS) {
- Slog.d(TAG, "Activity start allowed by caller. "
- + state.dump());
- }
- return allowBasedOnCaller(state);
- }
- if (state.mBalAllowedByPiCreator.allowsBackgroundActivityStarts()) {
- Slog.wtf(TAG,
- "With Android 15 BAL hardening this activity start may be blocked"
- + " if the PI creator upgrades target_sdk to 35+"
- + " AND the PI sender upgrades target_sdk to 34+! "
- + state.dump());
- showBalRiskToast();
- return allowBasedOnCaller(state);
- }
- Slog.wtf(TAG,
- "Without Android 15 BAL hardening this activity start would be allowed"
- + " (missing opt in by PI creator or sender)! "
- + state.dump());
- return abortLaunch(state);
- }
if (callerCanAllow) {
// Allowed before V by creator
if (state.mBalAllowedByPiCreatorWithHardening.allowsBackgroundActivityStarts()) {
@@ -749,35 +728,29 @@
return allowBasedOnCaller(state);
}
if (state.mBalAllowedByPiCreator.allowsBackgroundActivityStarts()) {
- Slog.wtf(TAG,
- "With Android 15 BAL hardening this activity start may be blocked"
+ Slog.wtf(TAG, "With Android 15 BAL hardening this activity start may be blocked"
+ " if the PI creator upgrades target_sdk to 35+! "
+ " (missing opt in by PI creator)! "
+ state.dump());
showBalRiskToast();
return allowBasedOnCaller(state);
}
- Slog.wtf(TAG,
- "Without Android 15 BAL hardening this activity start would be allowed"
- + " (missing opt in by PI creator)! "
- + state.dump());
- return abortLaunch(state);
}
if (realCallerCanAllow) {
// Allowed before U by sender
if (state.mBalAllowedByPiSender.allowsBackgroundActivityStarts()) {
- Slog.wtf(TAG,
- "With Android 14 BAL hardening this activity start will be blocked"
+ Slog.wtf(TAG, "With Android 14 BAL hardening this activity start will be blocked"
+ " if the PI sender upgrades target_sdk to 34+! "
+ " (missing opt in by PI sender)! "
+ state.dump());
showBalRiskToast();
return allowBasedOnRealCaller(state);
}
- Slog.wtf(TAG, "Without Android 14 BAL hardening this activity start would be allowed"
- + " (missing opt in by PI sender)! "
- + state.dump());
- return abortLaunch(state);
+ }
+ // caller or real caller could start the activity, but would need to explicitly opt in
+ if (callerCanAllow || realCallerCanAllow) {
+ Slog.wtf(TAG, "Without BAL hardening this activity start would be allowed "
+ + state.dump());
}
// neither the caller not the realCaller can allow or have explicitly opted out
return abortLaunch(state);
@@ -949,7 +922,7 @@
// is allowed, or apps like live wallpaper with non app visible window will be allowed.
final boolean appSwitchAllowedOrFg = state.mAppSwitchState == APP_SWITCH_ALLOW
|| state.mAppSwitchState == APP_SWITCH_FG_ONLY;
- if (Flags.balImproveRealCallerVisibilityCheck()) {
+ if (balImproveRealCallerVisibilityCheck()) {
if (appSwitchAllowedOrFg && state.mRealCallingUidHasAnyVisibleWindow) {
return new BalVerdict(BAL_ALLOW_VISIBLE_WINDOW,
/*background*/ false, "realCallingUid has visible window");
@@ -1042,8 +1015,9 @@
* create a new task or bring an existing one into the foreground
*/
boolean checkActivityAllowedToStart(@Nullable ActivityRecord sourceRecord,
- @NonNull ActivityRecord targetRecord, boolean newTask, @NonNull Task targetTask,
- int launchFlags, int balCode, int callingUid, int realCallingUid) {
+ @NonNull ActivityRecord targetRecord, boolean newTask, boolean avoidMoveTaskToFront,
+ @Nullable Task targetTask, int launchFlags, int balCode, int callingUid,
+ int realCallingUid) {
// BAL Exception allowed in all cases
if (balCode == BAL_ALLOW_ALLOWLISTED_UID) {
return true;
@@ -1067,14 +1041,36 @@
}
if (balCode == BAL_ALLOW_GRACE_PERIOD) {
+ // Allow if launching into new task, and caller matches most recently finished activity
if (taskToFront && mTopFinishedActivity != null
&& mTopFinishedActivity.mUid == callingUid) {
return true;
- } else if (!taskToFront) {
- FinishedActivityEntry finishedEntry =
- mTaskIdToFinishedActivity.get(targetTask.mTaskId);
- if (finishedEntry != null && finishedEntry.mUid == callingUid) {
- return true;
+ }
+
+ // Launching into existing task - allow if matches most recently finished activity
+ // within the task.
+ // We can reach here multiple ways:
+ // 1. activity in fg fires intent (taskToFront = false, sourceRecord is available)
+ // 2. activity in bg fires intent (taskToFront = false, sourceRecord is available)
+ // 3. activity in bg fires intent with NEW_FLAG (taskToFront = true,
+ // avoidMoveTaskToFront = true, sourceRecord is available)
+ // 4. activity in bg fires PI (taskToFront = true, avoidMoveTaskToFront = true,
+ // sourceRecord is not available, targetTask may be available)
+ if (!taskToFront || avoidMoveTaskToFront) {
+ if (targetTask != null) {
+ FinishedActivityEntry finishedEntry =
+ mTaskIdToFinishedActivity.get(targetTask.mTaskId);
+ if (finishedEntry != null && finishedEntry.mUid == callingUid) {
+ return true;
+ }
+ }
+
+ if (sourceRecord != null) {
+ FinishedActivityEntry finishedEntry =
+ mTaskIdToFinishedActivity.get(sourceRecord.getTask().mTaskId);
+ if (finishedEntry != null && finishedEntry.mUid == callingUid) {
+ return true;
+ }
}
}
}
@@ -1098,7 +1094,7 @@
bas = isTopActivityMatchingUidAbsentForAsm(taskToCheck, sourceRecord.getUid(),
sourceRecord);
}
- } else if (!taskToFront) {
+ } else if (targetTask != null && (!taskToFront || avoidMoveTaskToFront)) {
// We don't have a sourceRecord, and we're launching into an existing task.
// Allow if callingUid is top of stack.
bas = isTopActivityMatchingUidAbsentForAsm(targetTask, callingUid,
@@ -1111,12 +1107,14 @@
// ASM rules have failed. Log why
return logAsmFailureAndCheckFeatureEnabled(sourceRecord, callingUid, realCallingUid,
- newTask, targetTask, targetRecord, balCode, launchFlags, bas, taskToFront);
+ newTask, avoidMoveTaskToFront, targetTask, targetRecord, balCode, launchFlags,
+ bas, taskToFront);
}
private boolean logAsmFailureAndCheckFeatureEnabled(ActivityRecord sourceRecord, int callingUid,
- int realCallingUid, boolean newTask, Task targetTask, ActivityRecord targetRecord,
- @BalCode int balCode, int launchFlags, BlockActivityStart bas, boolean taskToFront) {
+ int realCallingUid, boolean newTask, boolean avoidMoveTaskToFront, Task targetTask,
+ ActivityRecord targetRecord, @BalCode int balCode, int launchFlags,
+ BlockActivityStart bas, boolean taskToFront) {
ActivityRecord targetTopActivity = targetTask == null ? null
: targetTask.getActivity(ar -> !ar.finishing && !ar.isAlwaysOnTop());
@@ -1133,7 +1131,7 @@
String asmDebugInfo = getDebugInfoForActivitySecurity("Launch", sourceRecord,
targetRecord, targetTask, targetTopActivity, realCallingUid, balCode,
- blockActivityStartAndFeatureEnabled, taskToFront);
+ blockActivityStartAndFeatureEnabled, taskToFront, avoidMoveTaskToFront);
FrameworkStatsLog.write(FrameworkStatsLog.ACTIVITY_ACTION_BLOCKED,
/* caller_uid */
@@ -1265,7 +1263,7 @@
Slog.i(TAG, getDebugInfoForActivitySecurity("Clear Top", sourceRecord, targetRecord,
targetTask, targetTaskTop, realCallingUid, balCode, shouldBlockActivityStart,
- /* taskToFront */ true));
+ /* taskToFront */ true, /* avoidMoveTaskToFront */ false));
}
}
@@ -1379,7 +1377,7 @@
private BlockActivityStart isTopActivityMatchingUidAbsentForAsm(@NonNull Task task,
int uid, @Nullable ActivityRecord sourceRecord) {
// If the source is visible, consider it 'top'.
- if (sourceRecord != null && sourceRecord.isVisible()) {
+ if (sourceRecord != null && sourceRecord.isVisibleRequested()) {
return new BlockActivityStart(false, false);
}
@@ -1389,6 +1387,12 @@
return new BlockActivityStart(false, false);
}
+ // If UID is visible in target task, allow launch
+ if (task.forAllActivities((Predicate<ActivityRecord>)
+ ar -> ar.isUid(uid) && ar.isVisibleRequested())) {
+ return new BlockActivityStart(false, false);
+ }
+
// Consider the source activity, whether or not it is finishing. Do not consider any other
// finishing activity.
Predicate<ActivityRecord> topOfStackPredicate = (ar) -> ar.equals(sourceRecord)
@@ -1480,27 +1484,26 @@
@Nullable ActivityRecord sourceRecord, @NonNull ActivityRecord targetRecord,
@Nullable Task targetTask, @Nullable ActivityRecord targetTopActivity,
int realCallingUid, @BalCode int balCode,
- boolean blockActivityStartAndFeatureEnabled, boolean taskToFront) {
+ boolean blockActivityStartAndFeatureEnabled, boolean taskToFront,
+ boolean avoidMoveTaskToFront) {
final String prefix = "[ASM] ";
Function<ActivityRecord, String> recordToString = (ar) -> {
if (ar == null) {
return null;
}
- return (ar == sourceRecord ? " [source]=> "
+
+ return (ar == sourceRecord ? " [source]=> "
: ar == targetTopActivity ? " [ top ]=> "
- : ar == targetRecord ? " [target]=> "
- : " => ")
- + ar
- + " :: visible=" + ar.isVisible()
- + ", finishing=" + ar.isFinishing()
- + ", alwaysOnTop=" + ar.isAlwaysOnTop()
- + ", taskFragment=" + ar.getTaskFragment();
+ : ar == targetRecord ? " [target]=> "
+ : " => ")
+ + getDebugStringForActivityRecord(ar);
};
StringJoiner joiner = new StringJoiner("\n");
joiner.add(prefix + "------ Activity Security " + action + " Debug Logging Start ------");
joiner.add(prefix + "Block Enabled: " + blockActivityStartAndFeatureEnabled);
joiner.add(prefix + "ASM Version: " + ActivitySecurityModelFeatureFlags.ASM_VERSION);
+ joiner.add(prefix + "System Time: " + SystemClock.uptimeMillis());
boolean targetTaskMatchesSourceTask = targetTask != null
&& sourceRecord != null && sourceRecord.getTask() == targetTask;
@@ -1512,6 +1515,8 @@
joiner.add(prefix + "Real Calling Uid Package: " + realCallingPackage);
} else {
joiner.add(prefix + "Source Record: " + recordToString.apply(sourceRecord));
+ joiner.add(prefix + "Source Launch Package: " + sourceRecord.launchedFromPackage);
+ joiner.add(prefix + "Source Launch Intent: " + sourceRecord.intent);
if (targetTaskMatchesSourceTask) {
joiner.add(prefix + "Source/Target Task: " + sourceRecord.getTask());
joiner.add(prefix + "Source/Target Task Stack: ");
@@ -1536,7 +1541,30 @@
joiner.add(prefix + "Target Record: " + recordToString.apply(targetRecord));
joiner.add(prefix + "Intent: " + targetRecord.intent);
joiner.add(prefix + "TaskToFront: " + taskToFront);
+ joiner.add(prefix + "AvoidMoveToFront: " + avoidMoveTaskToFront);
joiner.add(prefix + "BalCode: " + balCodeToString(balCode));
+ joiner.add(prefix + "LastResumedActivity: "
+ + recordToString.apply(mService.mLastResumedActivity));
+
+ if (mTopFinishedActivity != null) {
+ joiner.add(prefix + "TopFinishedActivity: " + mTopFinishedActivity.mDebugInfo);
+ }
+
+ if (!mTaskIdToFinishedActivity.isEmpty()) {
+ joiner.add(prefix + "TaskIdToFinishedActivity: ");
+ mTaskIdToFinishedActivity.values().forEach(
+ (fae) -> joiner.add(prefix + " " + fae.mDebugInfo));
+ }
+
+ if (balCode == BAL_ALLOW_VISIBLE_WINDOW || balCode == BAL_ALLOW_NON_APP_VISIBLE_WINDOW
+ || balCode == BAL_ALLOW_FOREGROUND) {
+ Task task = sourceRecord != null ? sourceRecord.getTask() : targetTask;
+ if (task != null && task.getDisplayArea() != null) {
+ joiner.add(prefix + "Tasks: ");
+ task.getDisplayArea().forAllTasks((Consumer<Task>)
+ t -> joiner.add(prefix + " T: " + t.toFullString()));
+ }
+ }
joiner.add(prefix + "------ Activity Security " + action + " Debug Logging End ------");
return joiner.toString();
@@ -1620,7 +1648,7 @@
return;
}
- if (!finishActivity.mVisibleRequested
+ if (!finishActivity.isVisibleRequested()
&& finishActivity != finishActivity.getTask().getTopMostActivity()) {
return;
}
@@ -1666,10 +1694,22 @@
}
}
+ private static String getDebugStringForActivityRecord(ActivityRecord ar) {
+ return ar
+ + " :: visible=" + ar.isVisible()
+ + ", visibleRequested=" + ar.isVisibleRequested()
+ + ", finishing=" + ar.finishing
+ + ", alwaysOnTop=" + ar.isAlwaysOnTop()
+ + ", lastLaunchTime=" + ar.lastLaunchTime
+ + ", lastVisibleTime=" + ar.lastVisibleTime
+ + ", taskFragment=" + ar.getTaskFragment();
+ }
+
private class FinishedActivityEntry {
int mUid;
int mTaskId;
int mLaunchCount;
+ String mDebugInfo;
FinishedActivityEntry(ActivityRecord ar) {
FinishedActivityEntry entry = mTaskIdToFinishedActivity.get(ar.getTask().mTaskId);
@@ -1677,6 +1717,7 @@
this.mUid = ar.getUid();
this.mTaskId = taskId;
this.mLaunchCount = entry == null || !ar.isUid(entry.mUid) ? 1 : entry.mLaunchCount + 1;
+ this.mDebugInfo = getDebugStringForActivityRecord(ar);
mService.mH.postDelayed(() -> {
synchronized (mService.mGlobalLock) {
diff --git a/services/core/java/com/android/server/wm/DisplayFrames.java b/services/core/java/com/android/server/wm/DisplayFrames.java
index 7f785af..a1799b4 100644
--- a/services/core/java/com/android/server/wm/DisplayFrames.java
+++ b/services/core/java/com/android/server/wm/DisplayFrames.java
@@ -92,35 +92,39 @@
mRotation = rotation;
mWidth = w;
mHeight = h;
- final Rect unrestricted = mUnrestricted;
- unrestricted.set(0, 0, w, h);
- state.setDisplayFrame(unrestricted);
+ final Rect u = mUnrestricted;
+ u.set(0, 0, w, h);
+ state.setDisplayFrame(u);
state.setDisplayCutout(displayCutout);
state.setRoundedCorners(roundedCorners);
state.setPrivacyIndicatorBounds(indicatorBounds);
state.setDisplayShape(displayShape);
state.getDisplayCutoutSafe(safe);
- if (safe.left > unrestricted.left) {
- state.getOrCreateSource(ID_DISPLAY_CUTOUT_LEFT, displayCutout()).setFrame(
- unrestricted.left, unrestricted.top, safe.left, unrestricted.bottom);
+ if (safe.left > u.left) {
+ state.getOrCreateSource(ID_DISPLAY_CUTOUT_LEFT, displayCutout())
+ .setFrame(u.left, u.top, safe.left, u.bottom)
+ .updateSideHint(u);
} else {
state.removeSource(ID_DISPLAY_CUTOUT_LEFT);
}
- if (safe.top > unrestricted.top) {
- state.getOrCreateSource(ID_DISPLAY_CUTOUT_TOP, displayCutout()).setFrame(
- unrestricted.left, unrestricted.top, unrestricted.right, safe.top);
+ if (safe.top > u.top) {
+ state.getOrCreateSource(ID_DISPLAY_CUTOUT_TOP, displayCutout())
+ .setFrame(u.left, u.top, u.right, safe.top)
+ .updateSideHint(u);
} else {
state.removeSource(ID_DISPLAY_CUTOUT_TOP);
}
- if (safe.right < unrestricted.right) {
- state.getOrCreateSource(ID_DISPLAY_CUTOUT_RIGHT, displayCutout()).setFrame(
- safe.right, unrestricted.top, unrestricted.right, unrestricted.bottom);
+ if (safe.right < u.right) {
+ state.getOrCreateSource(ID_DISPLAY_CUTOUT_RIGHT, displayCutout())
+ .setFrame(safe.right, u.top, u.right, u.bottom)
+ .updateSideHint(u);
} else {
state.removeSource(ID_DISPLAY_CUTOUT_RIGHT);
}
- if (safe.bottom < unrestricted.bottom) {
- state.getOrCreateSource(ID_DISPLAY_CUTOUT_BOTTOM, displayCutout()).setFrame(
- unrestricted.left, safe.bottom, unrestricted.right, unrestricted.bottom);
+ if (safe.bottom < u.bottom) {
+ state.getOrCreateSource(ID_DISPLAY_CUTOUT_BOTTOM, displayCutout())
+ .setFrame(u.left, safe.bottom, u.right, u.bottom)
+ .updateSideHint(u);
} else {
state.removeSource(ID_DISPLAY_CUTOUT_BOTTOM);
}
diff --git a/services/core/java/com/android/server/wm/DisplayPolicy.java b/services/core/java/com/android/server/wm/DisplayPolicy.java
index 63ca592..e2bc59b 100644
--- a/services/core/java/com/android/server/wm/DisplayPolicy.java
+++ b/services/core/java/com/android/server/wm/DisplayPolicy.java
@@ -794,6 +794,9 @@
}
mService.mAtmService.mKeyguardController.updateDeferTransitionForAod(
mAwake /* waiting */);
+ if (!awake) {
+ mDisplayContent.mWallpaperController.onDisplaySwitchFinished();
+ }
}
}
@@ -836,7 +839,8 @@
mRemoteInsetsControllerControlsSystemBars = remoteInsetsControllerControlsSystemBars;
}
- public void screenTurnedOn(ScreenOnListener screenOnListener) {
+ /** Prepares to turn on screen. The given listener is used to notify that it is ready. */
+ public void screenTurningOn(ScreenOnListener screenOnListener) {
WindowProcessController visibleDozeUiProcess = null;
synchronized (mLock) {
mScreenOnEarly = true;
@@ -858,6 +862,11 @@
}
}
+ /** It is called after {@link #finishScreenTurningOn}. This runs on PowerManager's thread. */
+ public void screenTurnedOn() {
+ mDisplayContent.mWallpaperController.onDisplaySwitchFinished();
+ }
+
public void screenTurnedOff() {
synchronized (mLock) {
mScreenOnEarly = false;
diff --git a/services/core/java/com/android/server/wm/DragDropController.java b/services/core/java/com/android/server/wm/DragDropController.java
index 32d60c5..6a3cf43 100644
--- a/services/core/java/com/android/server/wm/DragDropController.java
+++ b/services/core/java/com/android/server/wm/DragDropController.java
@@ -469,8 +469,7 @@
case MSG_REMOVE_DRAG_SURFACE_TIMEOUT: {
synchronized (mService.mGlobalLock) {
- mService.mTransactionFactory.get()
- .reparent((SurfaceControl) msg.obj, null).apply();
+ mService.mTransactionFactory.get().remove((SurfaceControl) msg.obj).apply();
}
break;
}
diff --git a/services/core/java/com/android/server/wm/DragState.java b/services/core/java/com/android/server/wm/DragState.java
index adbe3bc..d302f06 100644
--- a/services/core/java/com/android/server/wm/DragState.java
+++ b/services/core/java/com/android/server/wm/DragState.java
@@ -270,7 +270,7 @@
}
if (mSurfaceControl != null) {
if (!mRelinquishDragSurfaceToDropTarget && !relinquishDragSurfaceToDragSource()) {
- mTransaction.reparent(mSurfaceControl, null).apply();
+ mTransaction.remove(mSurfaceControl).apply();
} else {
mDragDropController.sendTimeoutMessage(MSG_REMOVE_DRAG_SURFACE_TIMEOUT,
mSurfaceControl, DragDropController.DRAG_TIMEOUT_MS);
diff --git a/services/core/java/com/android/server/wm/InsetsSourceProvider.java b/services/core/java/com/android/server/wm/InsetsSourceProvider.java
index 9d5ddf3..d9dda4a 100644
--- a/services/core/java/com/android/server/wm/InsetsSourceProvider.java
+++ b/services/core/java/com/android/server/wm/InsetsSourceProvider.java
@@ -62,6 +62,8 @@
*/
class InsetsSourceProvider {
+ private static final Rect EMPTY_RECT = new Rect();
+
protected final DisplayContent mDisplayContent;
protected final @NonNull InsetsSource mSource;
protected WindowContainer mWindowContainer;
@@ -286,12 +288,15 @@
private void updateSourceFrameForServerVisibility() {
// Make sure we set the valid source frame only when server visible is true, because the
- // frame may not yet determined that server side doesn't think the window is ready to
+ // frame may not yet be determined that server side doesn't think the window is ready to
// visible. (i.e. No surface, pending insets that were given during layout, etc..)
- if (mServerVisible) {
- mSource.setFrame(mSourceFrame);
- } else {
- mSource.setFrame(0, 0, 0, 0);
+ final Rect frame = mServerVisible ? mSourceFrame : EMPTY_RECT;
+ if (mSource.getFrame().equals(frame)) {
+ return;
+ }
+ mSource.setFrame(frame);
+ if (mWindowContainer != null) {
+ mSource.updateSideHint(mWindowContainer.getBounds());
}
}
@@ -631,7 +636,7 @@
}
pw.print(prefix);
pw.print("mIsLeashReadyForDispatching="); pw.print(mIsLeashReadyForDispatching);
- pw.print("mHasPendingPosition="); pw.print(mHasPendingPosition);
+ pw.print(" mHasPendingPosition="); pw.print(mHasPendingPosition);
pw.println();
if (mWindowContainer != null) {
pw.print(prefix + "mWindowContainer=");
diff --git a/services/core/java/com/android/server/wm/LetterboxUiController.java b/services/core/java/com/android/server/wm/LetterboxUiController.java
index fcc1e5b..0e2d3d1 100644
--- a/services/core/java/com/android/server/wm/LetterboxUiController.java
+++ b/services/core/java/com/android/server/wm/LetterboxUiController.java
@@ -148,7 +148,7 @@
final class LetterboxUiController {
private static final Predicate<ActivityRecord> FIRST_OPAQUE_NOT_FINISHING_ACTIVITY_PREDICATE =
- activityRecord -> activityRecord.fillsParent() && !activityRecord.isFinishing();
+ ActivityRecord::occludesParent;
private static final String TAG = TAG_WITH_CLASS_NAME ? "LetterboxUiController" : TAG_ATM;
@@ -1424,7 +1424,7 @@
@VisibleForTesting
boolean shouldShowLetterboxUi(WindowState mainWindow) {
- if (mIsRelaunchingAfterRequestedOrientationChanged || !isSurfaceReadyToShow(mainWindow)) {
+ if (mIsRelaunchingAfterRequestedOrientationChanged) {
return mLastShouldShowLetterboxUi;
}
@@ -1442,13 +1442,6 @@
}
@VisibleForTesting
- boolean isSurfaceReadyToShow(WindowState mainWindow) {
- return mainWindow.isDrawn() // Regular case
- // Waiting for relayoutWindow to call preserveSurface
- || mainWindow.isDragResizeChanged();
- }
-
- @VisibleForTesting
boolean isSurfaceVisible(WindowState mainWindow) {
return mainWindow.isOnScreen() && (mActivityRecord.isVisible()
|| mActivityRecord.isVisibleRequested());
diff --git a/services/core/java/com/android/server/wm/RootWindowContainer.java b/services/core/java/com/android/server/wm/RootWindowContainer.java
index 02b3f15..587cc74 100644
--- a/services/core/java/com/android/server/wm/RootWindowContainer.java
+++ b/services/core/java/com/android/server/wm/RootWindowContainer.java
@@ -2783,6 +2783,9 @@
} else {
throw new RuntimeException("Create the same sleep token twice: " + token);
}
+ if (isSwappingDisplay) {
+ display.mWallpaperController.onDisplaySwitchStarted();
+ }
return token;
}
diff --git a/services/core/java/com/android/server/wm/Session.java b/services/core/java/com/android/server/wm/Session.java
index f10a733..083872a 100644
--- a/services/core/java/com/android/server/wm/Session.java
+++ b/services/core/java/com/android/server/wm/Session.java
@@ -669,7 +669,7 @@
}
@Override
- public Bundle sendWallpaperCommand(IBinder window, String action, int x, int y,
+ public void sendWallpaperCommand(IBinder window, String action, int x, int y,
int z, Bundle extras, boolean sync) {
synchronized (mService.mGlobalLock) {
final long ident = Binder.clearCallingIdentity();
@@ -680,10 +680,9 @@
if (mCanAlwaysUpdateWallpaper
|| windowState == wallpaperController.getWallpaperTarget()
|| windowState == wallpaperController.getPrevWallpaperTarget()) {
- return wallpaperController.sendWindowWallpaperCommandUnchecked(
+ wallpaperController.sendWindowWallpaperCommandUnchecked(
windowState, action, x, y, z, extras, sync);
}
- return null;
} finally {
Binder.restoreCallingIdentity(ident);
}
diff --git a/services/core/java/com/android/server/wm/TaskFragment.java b/services/core/java/com/android/server/wm/TaskFragment.java
index 6371bb4..b2b547e 100644
--- a/services/core/java/com/android/server/wm/TaskFragment.java
+++ b/services/core/java/com/android/server/wm/TaskFragment.java
@@ -45,7 +45,6 @@
import static android.view.WindowManager.TRANSIT_NONE;
import static android.view.WindowManager.TRANSIT_OPEN;
-import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_BACK_PREVIEW;
import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_STATES;
import static com.android.server.wm.ActivityRecord.State.PAUSED;
import static com.android.server.wm.ActivityRecord.State.PAUSING;
@@ -89,7 +88,6 @@
import android.content.res.Configuration;
import android.graphics.Point;
import android.graphics.Rect;
-import android.hardware.HardwareBuffer;
import android.os.IBinder;
import android.os.UserHandle;
import android.util.DisplayMetrics;
@@ -99,7 +97,6 @@
import android.view.RemoteAnimationTarget;
import android.view.SurfaceControl;
import android.window.ITaskFragmentOrganizer;
-import android.window.ScreenCapture;
import android.window.TaskFragmentAnimationParams;
import android.window.TaskFragmentInfo;
import android.window.TaskFragmentOrganizerToken;
@@ -113,7 +110,6 @@
import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.util.ArrayList;
-import java.util.HashMap;
import java.util.List;
import java.util.Set;
import java.util.function.Consumer;
@@ -369,6 +365,15 @@
*/
private boolean mMoveToBottomIfClearWhenLaunch;
+ /**
+ * If {@code true}, transitions are allowed even if this TaskFragment is empty. If
+ * {@code false}, transitions will wait until this TaskFragment becomes non-empty or other
+ * conditions are met. Default to {@code false}.
+ *
+ * @see #isReadyToTransit
+ */
+ private boolean mAllowTransitionWhenEmpty;
+
/** When set, will force the task to report as invisible. */
static final int FLAG_FORCE_HIDDEN_FOR_PINNED_TASK = 1;
static final int FLAG_FORCE_HIDDEN_FOR_TASK_ORG = 1 << 1;
@@ -394,10 +399,6 @@
/** For calculating app bounds, i.e. the area without the nav bar and display cutout. */
private final Rect mTmpNonDecorBounds = new Rect();
- //TODO(b/207481538) Remove once the infrastructure to support per-activity screenshot is
- // implemented
- HashMap<String, ScreenCapture.ScreenshotHardwareBuffer> mBackScreenshots = new HashMap<>();
-
private final EnsureActivitiesVisibleHelper mEnsureActivitiesVisibleHelper =
new EnsureActivitiesVisibleHelper(this);
@@ -509,6 +510,19 @@
mIsolatedNav = isolatedNav;
}
+ /**
+ * Sets whether transitions are allowed when the TaskFragment is empty. If {@code true},
+ * transitions are allowed when the TaskFragment is empty. If {@code false}, transitions
+ * will wait until the TaskFragment becomes non-empty or other conditions are met. Default
+ * to {@code false}.
+ */
+ void setAllowTransitionWhenEmpty(boolean allowTransitionWhenEmpty) {
+ if (!isEmbedded()) {
+ return;
+ }
+ mAllowTransitionWhenEmpty = allowTransitionWhenEmpty;
+ }
+
/** @see #mIsolatedNav */
boolean isIsolatedNav() {
return isEmbedded() && mIsolatedNav;
@@ -2070,17 +2084,6 @@
super.addChild(child, index);
if (isAddingActivity && task != null) {
- // TODO(b/207481538): temporary per-activity screenshoting
- if (r != null && BackNavigationController.isScreenshotEnabled()) {
- ProtoLog.v(WM_DEBUG_BACK_PREVIEW, "Screenshotting Activity %s",
- r.mActivityComponent.flattenToString());
- Rect outBounds = r.getBounds();
- ScreenCapture.ScreenshotHardwareBuffer backBuffer = ScreenCapture.captureLayers(
- r.mSurfaceControl,
- new Rect(0, 0, outBounds.width(), outBounds.height()),
- 1f);
- mBackScreenshots.put(r.mActivityComponent.flattenToString(), backBuffer);
- }
addingActivity.inHistory = true;
task.onDescendantActivityAdded(taskHadActivity, activityType, addingActivity);
}
@@ -2827,8 +2830,9 @@
return true;
}
// We don't want to start the transition if the organized TaskFragment is empty, unless
- // it is requested to be removed.
- if (getTopNonFinishingActivity() != null || mIsRemovalRequested) {
+ // it is requested to be removed or the mAllowTransitionWhenEmpty flag is true.
+ if (getTopNonFinishingActivity() != null || mIsRemovalRequested
+ || mAllowTransitionWhenEmpty) {
return true;
}
// Organizer shouldn't change embedded TaskFragment in PiP.
@@ -2882,19 +2886,6 @@
return !mCreatedByOrganizer || mIsRemovalRequested;
}
- @Nullable
- HardwareBuffer getSnapshotForActivityRecord(@Nullable ActivityRecord r) {
- if (!BackNavigationController.isScreenshotEnabled()) {
- return null;
- }
- if (r != null && r.mActivityComponent != null) {
- ScreenCapture.ScreenshotHardwareBuffer backBuffer =
- mBackScreenshots.get(r.mActivityComponent.flattenToString());
- return backBuffer != null ? backBuffer.getHardwareBuffer() : null;
- }
- return null;
- }
-
@Override
void removeChild(WindowContainer child) {
removeChild(child, true /* removeSelfIfPossible */);
@@ -2903,13 +2894,6 @@
void removeChild(WindowContainer child, boolean removeSelfIfPossible) {
super.removeChild(child);
final ActivityRecord r = child.asActivityRecord();
- if (BackNavigationController.isScreenshotEnabled()) {
- //TODO(b/207481538) Remove once the infrastructure to support per-activity screenshot is
- // implemented
- if (r != null) {
- mBackScreenshots.remove(r.mActivityComponent.flattenToString());
- }
- }
final WindowProcessController hostProcess = getOrganizerProcessIfDifferent(r);
if (hostProcess != null) {
hostProcess.removeEmbeddedActivity(r);
diff --git a/services/core/java/com/android/server/wm/TransitionController.java b/services/core/java/com/android/server/wm/TransitionController.java
index 59e3350..d7b4a39 100644
--- a/services/core/java/com/android/server/wm/TransitionController.java
+++ b/services/core/java/com/android/server/wm/TransitionController.java
@@ -496,6 +496,9 @@
if (mCollectingTransition != null && mCollectingTransition.isInTransientHide(task)) {
return true;
}
+ for (int i = mWaitingTransitions.size() - 1; i >= 0; --i) {
+ if (mWaitingTransitions.get(i).isInTransientHide(task)) return true;
+ }
for (int i = mPlayingTransitions.size() - 1; i >= 0; --i) {
if (mPlayingTransitions.get(i).isInTransientHide(task)) return true;
}
@@ -506,6 +509,9 @@
if (mCollectingTransition != null && mCollectingTransition.isTransientVisible(task)) {
return true;
}
+ for (int i = mWaitingTransitions.size() - 1; i >= 0; --i) {
+ if (mWaitingTransitions.get(i).isTransientVisible(task)) return true;
+ }
for (int i = mPlayingTransitions.size() - 1; i >= 0; --i) {
if (mPlayingTransitions.get(i).isTransientVisible(task)) return true;
}
diff --git a/services/core/java/com/android/server/wm/WallpaperController.java b/services/core/java/com/android/server/wm/WallpaperController.java
index d68f932..399815b 100644
--- a/services/core/java/com/android/server/wm/WallpaperController.java
+++ b/services/core/java/com/android/server/wm/WallpaperController.java
@@ -16,6 +16,7 @@
package com.android.server.wm;
+import static android.app.WallpaperManager.COMMAND_DISPLAY_SWITCH;
import static android.app.WallpaperManager.COMMAND_FREEZE;
import static android.app.WallpaperManager.COMMAND_UNFREEZE;
import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
@@ -120,6 +121,11 @@
private boolean mShouldOffsetWallpaperCenter;
+ /**
+ * Whether the wallpaper has been notified about a physical display switch event is started.
+ */
+ private volatile boolean mIsWallpaperNotifiedOnDisplaySwitch;
+
private final Consumer<WindowState> mFindWallpapers = w -> {
if (w.mAttrs.type == TYPE_WALLPAPER) {
WallpaperWindowToken token = w.mToken.asWallpaperToken();
@@ -635,11 +641,10 @@
}
}
- Bundle sendWindowWallpaperCommandUnchecked(
+ void sendWindowWallpaperCommandUnchecked(
WindowState window, String action, int x, int y, int z,
Bundle extras, boolean sync) {
sendWindowWallpaperCommand(action, x, y, z, extras, sync);
- return null;
}
private void sendWindowWallpaperCommand(
@@ -1083,6 +1088,52 @@
}
/**
+ * Notifies the wallpaper that the display turns off when switching physical device. If the
+ * wallpaper is currently visible, its client visibility will be preserved until the display is
+ * confirmed to be off or on.
+ */
+ void onDisplaySwitchStarted() {
+ mIsWallpaperNotifiedOnDisplaySwitch = notifyDisplaySwitch(true /* start */);
+ }
+
+ /**
+ * Called when the screen has finished turning on or the device goes to sleep. This is no-op if
+ * the operation is not part of a display switch.
+ */
+ void onDisplaySwitchFinished() {
+ // The method can be called outside WM lock (turned on), so only acquire lock if needed.
+ // This is to optimize the common cases that regular devices don't have display switch.
+ if (mIsWallpaperNotifiedOnDisplaySwitch) {
+ synchronized (mService.mGlobalLock) {
+ mIsWallpaperNotifiedOnDisplaySwitch = false;
+ notifyDisplaySwitch(false /* start */);
+ }
+ }
+ }
+
+ private boolean notifyDisplaySwitch(boolean start) {
+ boolean notified = false;
+ for (int curTokenNdx = mWallpaperTokens.size() - 1; curTokenNdx >= 0; curTokenNdx--) {
+ final WallpaperWindowToken token = mWallpaperTokens.get(curTokenNdx);
+ for (int i = token.getChildCount() - 1; i >= 0; i--) {
+ final WindowState w = token.getChildAt(i);
+ if (start && !w.mWinAnimator.getShown()) {
+ continue;
+ }
+ try {
+ w.mClient.dispatchWallpaperCommand(COMMAND_DISPLAY_SWITCH, 0 /* x */, 0 /* y */,
+ start ? 1 : 0 /* use z as start or finish */,
+ null /* bundle */, false /* sync */);
+ } catch (RemoteException e) {
+ Slog.w(TAG, "Failed to dispatch COMMAND_DISPLAY_SWITCH " + e);
+ }
+ notified = true;
+ }
+ }
+ return notified;
+ }
+
+ /**
* Each window can request a zoom, example:
* - User is in overview, zoomed out.
* - User also pulls down the shade.
diff --git a/services/core/java/com/android/server/wm/WindowAnimator.java b/services/core/java/com/android/server/wm/WindowAnimator.java
index 750fd50..b43a454 100644
--- a/services/core/java/com/android/server/wm/WindowAnimator.java
+++ b/services/core/java/com/android/server/wm/WindowAnimator.java
@@ -146,10 +146,11 @@
for (int i = 0; i < numDisplays; i++) {
final DisplayContent dc = root.getChildAt(i);
- dc.checkAppWindowsReadyToShow();
+ if (!useShellTransition) {
+ dc.checkAppWindowsReadyToShow();
+ }
if (accessibilityController.hasCallbacks()) {
- accessibilityController.drawMagnifiedRegionBorderIfNeeded(dc.mDisplayId,
- mTransaction);
+ accessibilityController.drawMagnifiedRegionBorderIfNeeded(dc.mDisplayId);
}
if (dc.isAnimating(animationFlags, ANIMATION_TYPE_ALL)) {
diff --git a/services/core/java/com/android/server/wm/WindowContainer.java b/services/core/java/com/android/server/wm/WindowContainer.java
index bdea1bc..286182e 100644
--- a/services/core/java/com/android/server/wm/WindowContainer.java
+++ b/services/core/java/com/android/server/wm/WindowContainer.java
@@ -465,7 +465,7 @@
}
}
final InsetsSource source = new InsetsSource(id, provider.getType());
- source.setFrame(provider.getArbitraryRectangle());
+ source.setFrame(provider.getArbitraryRectangle()).updateSideHint(getBounds());
mLocalInsetsSources.put(id, source);
mDisplayContent.getInsetsStateController().updateAboveInsetsState(true);
}
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index f8ac8da..9650b8bc 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -9156,55 +9156,63 @@
if (fromWin == null || !fromWin.isFocused()) {
return false;
}
- final TaskFragment fromFragment = fromWin.getTaskFragment();
- if (fromFragment == null) {
- return false;
- }
- final TaskFragment adjacentFragment = fromFragment.getAdjacentTaskFragment();
- if (adjacentFragment == null || adjacentFragment.asTask() != null) {
- // Don't move the focus to another task.
- return false;
- }
- final Rect fromBounds = fromFragment.getBounds();
- final Rect adjacentBounds = adjacentFragment.getBounds();
- switch (direction) {
- case View.FOCUS_LEFT:
- if (adjacentBounds.left >= fromBounds.left) {
- return false;
- }
- break;
- case View.FOCUS_UP:
- if (adjacentBounds.top >= fromBounds.top) {
- return false;
- }
- break;
- case View.FOCUS_RIGHT:
- if (adjacentBounds.right <= fromBounds.right) {
- return false;
- }
- break;
- case View.FOCUS_DOWN:
- if (adjacentBounds.bottom <= fromBounds.bottom) {
- return false;
- }
- break;
- case View.FOCUS_BACKWARD:
- case View.FOCUS_FORWARD:
- // These are not absolute directions. Skip checking the bounds.
- break;
- default:
+ return moveFocusToAdjacentWindow(fromWin, direction);
+ }
+ }
+
+ boolean moveFocusToAdjacentWindow(WindowState fromWin, @FocusDirection int direction) {
+ final TaskFragment fromFragment = fromWin.getTaskFragment();
+ if (fromFragment == null) {
+ return false;
+ }
+ final TaskFragment adjacentFragment = fromFragment.getAdjacentTaskFragment();
+ if (adjacentFragment == null || adjacentFragment.asTask() != null) {
+ // Don't move the focus to another task.
+ return false;
+ }
+ if (adjacentFragment.isIsolatedNav()) {
+ // Don't move the focus if the adjacent TF is isolated navigation.
+ return false;
+ }
+ final Rect fromBounds = fromFragment.getBounds();
+ final Rect adjacentBounds = adjacentFragment.getBounds();
+ switch (direction) {
+ case View.FOCUS_LEFT:
+ if (adjacentBounds.left >= fromBounds.left) {
return false;
- }
- final ActivityRecord topRunningActivity = adjacentFragment.topRunningActivity(
- true /* focusableOnly */);
- if (topRunningActivity == null) {
+ }
+ break;
+ case View.FOCUS_UP:
+ if (adjacentBounds.top >= fromBounds.top) {
+ return false;
+ }
+ break;
+ case View.FOCUS_RIGHT:
+ if (adjacentBounds.right <= fromBounds.right) {
+ return false;
+ }
+ break;
+ case View.FOCUS_DOWN:
+ if (adjacentBounds.bottom <= fromBounds.bottom) {
+ return false;
+ }
+ break;
+ case View.FOCUS_BACKWARD:
+ case View.FOCUS_FORWARD:
+ // These are not absolute directions. Skip checking the bounds.
+ break;
+ default:
return false;
- }
- moveDisplayToTopInternal(topRunningActivity.getDisplayId());
- handleTaskFocusChange(topRunningActivity.getTask(), topRunningActivity);
- if (fromWin.isFocused()) {
- return false;
- }
+ }
+ final ActivityRecord topRunningActivity = adjacentFragment.topRunningActivity(
+ true /* focusableOnly */);
+ if (topRunningActivity == null) {
+ return false;
+ }
+ moveDisplayToTopInternal(topRunningActivity.getDisplayId());
+ handleTaskFocusChange(topRunningActivity.getTask(), topRunningActivity);
+ if (fromWin.isFocused()) {
+ return false;
}
return true;
}
diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java
index 205ed97..4ba52e4 100644
--- a/services/core/java/com/android/server/wm/WindowOrganizerController.java
+++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java
@@ -2202,6 +2202,7 @@
}
final TaskFragment taskFragment = new TaskFragment(mService,
creationParams.getFragmentToken(), true /* createdByOrganizer */);
+ taskFragment.setAllowTransitionWhenEmpty(creationParams.getAllowTransitionWhenEmpty());
// Set task fragment organizer immediately, since it might have to be notified about further
// actions.
TaskFragmentOrganizerToken organizerToken = creationParams.getOrganizer();
diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java
index 56f2bc3..24e50c5 100644
--- a/services/core/java/com/android/server/wm/WindowState.java
+++ b/services/core/java/com/android/server/wm/WindowState.java
@@ -169,6 +169,7 @@
import static com.android.server.wm.WindowStateProto.REMOVED;
import static com.android.server.wm.WindowStateProto.REMOVE_ON_EXIT;
import static com.android.server.wm.WindowStateProto.REQUESTED_HEIGHT;
+import static com.android.server.wm.WindowStateProto.REQUESTED_VISIBLE_TYPES;
import static com.android.server.wm.WindowStateProto.REQUESTED_WIDTH;
import static com.android.server.wm.WindowStateProto.STACK_ID;
import static com.android.server.wm.WindowStateProto.SURFACE_INSETS;
@@ -3988,6 +3989,7 @@
proto.write(FORCE_SEAMLESS_ROTATION, mForceSeamlesslyRotate);
proto.write(HAS_COMPAT_SCALE, hasCompatScale());
proto.write(GLOBAL_SCALE, mGlobalScale);
+ proto.write(REQUESTED_VISIBLE_TYPES, mRequestedVisibleTypes);
for (Rect r : mKeepClearAreas) {
r.dumpDebug(proto, KEEP_CLEAR_AREAS);
}
@@ -5187,6 +5189,11 @@
if (mSurfaceControl == null) {
return;
}
+ if (mActivityRecord != null && mActivityRecord.isConfigurationDispatchPaused()) {
+ // Don't update surface-position while dispatch paused. This is calculated from
+ // the server-side activity configuration so return early.
+ return;
+ }
if ((mWmService.mWindowPlacerLocked.isLayoutDeferred() || isGoneForLayout())
&& !mSurfacePlacementNeeded) {
diff --git a/services/core/java/com/android/server/wm/WindowToken.java b/services/core/java/com/android/server/wm/WindowToken.java
index 5048cef..13e1ba78 100644
--- a/services/core/java/com/android/server/wm/WindowToken.java
+++ b/services/core/java/com/android/server/wm/WindowToken.java
@@ -639,9 +639,12 @@
@Override
void updateSurfacePosition(SurfaceControl.Transaction t) {
+ final ActivityRecord r = asActivityRecord();
+ if (r != null && r.isConfigurationDispatchPaused()) {
+ return;
+ }
super.updateSurfacePosition(t);
if (!mTransitionController.isShellTransitionsEnabled() && isFixedRotationTransforming()) {
- final ActivityRecord r = asActivityRecord();
final Task rootTask = r != null ? r.getRootTask() : null;
// Don't transform the activity in PiP because the PiP task organizer will handle it.
if (rootTask == null || !rootTask.inPinnedWindowingMode()) {
diff --git a/services/core/jni/com_android_server_input_InputManagerService.cpp b/services/core/jni/com_android_server_input_InputManagerService.cpp
index ae5d2cc..cbc301b 100644
--- a/services/core/jni/com_android_server_input_InputManagerService.cpp
+++ b/services/core/jni/com_android_server_input_InputManagerService.cpp
@@ -46,6 +46,7 @@
#include <com_android_input_flags.h>
#include <input/Input.h>
#include <input/PointerController.h>
+#include <input/PrintTools.h>
#include <input/SpriteController.h>
#include <inputflinger/InputManager.h>
#include <limits.h>
@@ -230,10 +231,6 @@
return a > b ? a : b;
}
-static inline const char* toString(bool value) {
- return value ? "true" : "false";
-}
-
static SpriteIcon toSpriteIcon(PointerIcon pointerIcon) {
// As a minor optimization, do not make a copy of the PointerIcon bitmap here. The loaded
// PointerIcons are only cached by InputManagerService in java, so we can safely assume they
@@ -284,11 +281,12 @@
void displayRemoved(JNIEnv* env, int32_t displayId);
void setFocusedApplication(JNIEnv* env, int32_t displayId, jobject applicationHandleObj);
void setFocusedDisplay(int32_t displayId);
+ void setMinTimeBetweenUserActivityPokes(int64_t intervalMillis);
void setInputDispatchMode(bool enabled, bool frozen);
void setSystemUiLightsOut(bool lightsOut);
void setPointerDisplayId(int32_t displayId);
void setPointerSpeed(int32_t speed);
- void setMousePointerAccelerationEnabled(bool enabled);
+ void setMousePointerAccelerationEnabled(int32_t displayId, bool enabled);
void setTouchpadPointerSpeed(int32_t speed);
void setTouchpadNaturalScrollingEnabled(bool enabled);
void setTouchpadTapToClickEnabled(bool enabled);
@@ -304,6 +302,7 @@
bool setPointerIcon(std::variant<std::unique_ptr<SpriteIcon>, PointerIconStyle> icon,
int32_t displayId, DeviceId deviceId, int32_t pointerId,
const sp<IBinder>& inputToken);
+ void setPointerIconVisibility(int32_t displayId, bool visible);
void setMotionClassifierEnabled(bool enabled);
std::optional<std::string> getBluetoothAddress(int32_t deviceId);
void setStylusButtonMotionEventsEnabled(bool enabled);
@@ -400,8 +399,8 @@
// Pointer speed.
int32_t pointerSpeed{0};
- // True if pointer acceleration is enabled for mice.
- bool mousePointerAccelerationEnabled{true};
+ // Displays on which its associated mice will have pointer acceleration disabled.
+ std::set<int32_t> displaysWithMousePointerAccelerationDisabled{};
// True if pointer gestures are enabled.
bool pointerGesturesEnabled{true};
@@ -492,8 +491,8 @@
dump += StringPrintf(INDENT "System UI Lights Out: %s\n",
toString(mLocked.systemUiLightsOut));
dump += StringPrintf(INDENT "Pointer Speed: %" PRId32 "\n", mLocked.pointerSpeed);
- dump += StringPrintf(INDENT "Mouse Pointer Acceleration: %s\n",
- mLocked.mousePointerAccelerationEnabled ? "Enabled" : "Disabled");
+ dump += StringPrintf(INDENT "Display with Mouse Pointer Acceleration Disabled: %s\n",
+ dumpSet(mLocked.displaysWithMousePointerAccelerationDisabled).c_str());
dump += StringPrintf(INDENT "Pointer Gestures Enabled: %s\n",
toString(mLocked.pointerGesturesEnabled));
dump += StringPrintf(INDENT "Show Touches: %s\n", toString(mLocked.showTouches));
@@ -676,11 +675,13 @@
std::scoped_lock _l(mLock);
outConfig->mousePointerSpeed = mLocked.pointerSpeed;
- outConfig->mousePointerAccelerationEnabled = mLocked.mousePointerAccelerationEnabled;
- outConfig->pointerVelocityControlParameters.scale = exp2f(mLocked.pointerSpeed
- * POINTER_SPEED_EXPONENT);
+ outConfig->displaysWithMousePointerAccelerationDisabled =
+ mLocked.displaysWithMousePointerAccelerationDisabled;
+ outConfig->pointerVelocityControlParameters.scale =
+ exp2f(mLocked.pointerSpeed * POINTER_SPEED_EXPONENT);
outConfig->pointerVelocityControlParameters.acceleration =
- mLocked.mousePointerAccelerationEnabled
+ mLocked.displaysWithMousePointerAccelerationDisabled.count(
+ mLocked.pointerDisplayId) == 0
? android::os::IInputConstants::DEFAULT_POINTER_ACCELERATION
: 1;
outConfig->pointerGesturesEnabled = mLocked.pointerGesturesEnabled;
@@ -1169,6 +1170,11 @@
mInputManager->getDispatcher().setFocusedDisplay(displayId);
}
+void NativeInputManager::setMinTimeBetweenUserActivityPokes(int64_t intervalMillis) {
+ mInputManager->getDispatcher().setMinTimeBetweenUserActivityPokes(
+ std::chrono::milliseconds(intervalMillis));
+}
+
void NativeInputManager::setInputDispatchMode(bool enabled, bool frozen) {
mInputManager->getDispatcher().setInputDispatchMode(enabled, frozen);
}
@@ -1224,16 +1230,23 @@
InputReaderConfiguration::Change::POINTER_SPEED);
}
-void NativeInputManager::setMousePointerAccelerationEnabled(bool enabled) {
+void NativeInputManager::setMousePointerAccelerationEnabled(int32_t displayId, bool enabled) {
{ // acquire lock
std::scoped_lock _l(mLock);
- if (mLocked.mousePointerAccelerationEnabled == enabled) {
+ const bool oldEnabled =
+ mLocked.displaysWithMousePointerAccelerationDisabled.count(displayId) == 0;
+ if (oldEnabled == enabled) {
return;
}
- ALOGI("Setting mouse pointer acceleration to %s", toString(enabled));
- mLocked.mousePointerAccelerationEnabled = enabled;
+ ALOGI("Setting mouse pointer acceleration to %s on display %d", toString(enabled),
+ displayId);
+ if (enabled) {
+ mLocked.displaysWithMousePointerAccelerationDisabled.erase(displayId);
+ } else {
+ mLocked.displaysWithMousePointerAccelerationDisabled.emplace(displayId);
+ }
} // release lock
mInputManager->getReader().requestRefreshConfiguration(
@@ -1397,6 +1410,13 @@
return mInputManager->getChoreographer().setPointerIcon(std::move(icon), displayId, deviceId);
}
+void NativeInputManager::setPointerIconVisibility(int32_t displayId, bool visible) {
+ if (!ENABLE_POINTER_CHOREOGRAPHER) {
+ return;
+ }
+ mInputManager->getChoreographer().setPointerIconVisibility(displayId, visible);
+}
+
TouchAffineTransformation NativeInputManager::getTouchAffineTransformation(
JNIEnv *env, jfloatArray matrixArr) {
ATRACE_CALL();
@@ -2110,6 +2130,13 @@
im->setFocusedDisplay(displayId);
}
+static void nativeSetUserActivityPokeInterval(JNIEnv* env, jobject nativeImplObj,
+ jlong intervalMillis) {
+ NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
+
+ im->setMinTimeBetweenUserActivityPokes(intervalMillis);
+}
+
static void nativeRequestPointerCapture(JNIEnv* env, jobject nativeImplObj, jobject tokenObj,
jboolean enabled) {
NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
@@ -2170,10 +2197,10 @@
}
static void nativeSetMousePointerAccelerationEnabled(JNIEnv* env, jobject nativeImplObj,
- jboolean enabled) {
+ jint displayId, jboolean enabled) {
NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
- im->setMousePointerAccelerationEnabled(enabled);
+ im->setMousePointerAccelerationEnabled(displayId, enabled);
}
static void nativeSetTouchpadPointerSpeed(JNIEnv* env, jobject nativeImplObj, jint speed) {
@@ -2552,6 +2579,13 @@
ibinderForJavaObject(env, inputTokenObj));
}
+static void nativeSetPointerIconVisibility(JNIEnv* env, jobject nativeImplObj, jint displayId,
+ jboolean visible) {
+ NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
+
+ im->setPointerIconVisibility(displayId, visible);
+}
+
static jboolean nativeCanDispatchToDisplay(JNIEnv* env, jobject nativeImplObj, jint deviceId,
jint displayId) {
NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
@@ -2744,6 +2778,15 @@
}
}
+static void nativeSetAccessibilitySlowKeysThreshold(JNIEnv* env, jobject nativeImplObj,
+ jint thresholdTimeMs) {
+ NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
+ if (ENABLE_INPUT_FILTER_RUST) {
+ im->getInputManager()->getInputFilter().setAccessibilitySlowKeysThreshold(
+ static_cast<nsecs_t>(thresholdTimeMs) * 1000000);
+ }
+}
+
static void nativeSetAccessibilityStickyKeysEnabled(JNIEnv* env, jobject nativeImplObj,
jboolean enabled) {
NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
@@ -2787,6 +2830,7 @@
{"setFocusedApplication", "(ILandroid/view/InputApplicationHandle;)V",
(void*)nativeSetFocusedApplication},
{"setFocusedDisplay", "(I)V", (void*)nativeSetFocusedDisplay},
+ {"setMinTimeBetweenUserActivityPokes", "(J)V", (void*)nativeSetUserActivityPokeInterval},
{"requestPointerCapture", "(Landroid/os/IBinder;Z)V", (void*)nativeRequestPointerCapture},
{"setInputDispatchMode", "(ZZ)V", (void*)nativeSetInputDispatchMode},
{"setSystemUiLightsOut", "(Z)V", (void*)nativeSetSystemUiLightsOut},
@@ -2794,7 +2838,7 @@
(void*)nativeTransferTouchFocus},
{"transferTouch", "(Landroid/os/IBinder;I)Z", (void*)nativeTransferTouch},
{"setPointerSpeed", "(I)V", (void*)nativeSetPointerSpeed},
- {"setMousePointerAccelerationEnabled", "(Z)V",
+ {"setMousePointerAccelerationEnabled", "(IZ)V",
(void*)nativeSetMousePointerAccelerationEnabled},
{"setTouchpadPointerSpeed", "(I)V", (void*)nativeSetTouchpadPointerSpeed},
{"setTouchpadNaturalScrollingEnabled", "(Z)V",
@@ -2831,6 +2875,7 @@
(void*)nativeSetCustomPointerIcon},
{"setPointerIcon", "(Landroid/view/PointerIcon;IIILandroid/os/IBinder;)Z",
(void*)nativeSetPointerIcon},
+ {"setPointerIconVisibility", "(IZ)V", (void*)nativeSetPointerIconVisibility},
{"canDispatchToDisplay", "(II)Z", (void*)nativeCanDispatchToDisplay},
{"notifyPortAssociationsChanged", "()V", (void*)nativeNotifyPortAssociationsChanged},
{"changeUniqueIdAssociation", "()V", (void*)nativeChangeUniqueIdAssociation},
@@ -2854,6 +2899,8 @@
{"setStylusPointerIconEnabled", "(Z)V", (void*)nativeSetStylusPointerIconEnabled},
{"setAccessibilityBounceKeysThreshold", "(I)V",
(void*)nativeSetAccessibilityBounceKeysThreshold},
+ {"setAccessibilitySlowKeysThreshold", "(I)V",
+ (void*)nativeSetAccessibilitySlowKeysThreshold},
{"setAccessibilityStickyKeysEnabled", "(Z)V",
(void*)nativeSetAccessibilityStickyKeysEnabled},
};
diff --git a/services/core/jni/com_android_server_location_GnssLocationProvider.cpp b/services/core/jni/com_android_server_location_GnssLocationProvider.cpp
index 11c40d7..9c033e2 100644
--- a/services/core/jni/com_android_server_location_GnssLocationProvider.cpp
+++ b/services/core/jni/com_android_server_location_GnssLocationProvider.cpp
@@ -675,7 +675,8 @@
options.enableCorrVecOutputs = enableCorrVecOutputs;
options.intervalMs = intervalMs;
- return gnssMeasurementIface->setCallback(std::make_unique<gnss::GnssMeasurementCallback>(),
+ return gnssMeasurementIface->setCallback(std::make_unique<gnss::GnssMeasurementCallback>(
+ gnssMeasurementIface->getInterfaceVersion()),
options);
}
diff --git a/services/core/jni/com_android_server_power_PowerManagerService.cpp b/services/core/jni/com_android_server_power_PowerManagerService.cpp
index 6ab98fe..d0b290c 100644
--- a/services/core/jni/com_android_server_power_PowerManagerService.cpp
+++ b/services/core/jni/com_android_server_power_PowerManagerService.cpp
@@ -31,6 +31,7 @@
#include <android_runtime/AndroidRuntime.h>
#include <android_runtime/Log.h>
#include <binder/IServiceManager.h>
+#include <com_android_input_flags.h>
#include <gui/SurfaceComposerClient.h>
#include <hardware_legacy/power.h>
#include <hidl/ServiceManagement.h>
@@ -109,10 +110,12 @@
eventTime = now;
}
- if (gLastEventTime[eventType] + MIN_TIME_BETWEEN_USERACTIVITIES > eventTime) {
- return;
+ if (!com::android::input::flags::rate_limit_user_activity_poke_in_dispatcher()) {
+ if (gLastEventTime[eventType] + MIN_TIME_BETWEEN_USERACTIVITIES > eventTime) {
+ return;
+ }
+ gLastEventTime[eventType] = eventTime;
}
- gLastEventTime[eventType] = eventTime;
// Tell the power HAL when user activity occurs.
setPowerBoost(Boost::INTERACTION, 0);
@@ -285,9 +288,11 @@
GET_METHOD_ID(gPowerManagerServiceClassInfo.userActivityFromNative, clazz,
"userActivityFromNative", "(JIII)V");
- // Initialize
- for (int i = 0; i <= USER_ACTIVITY_EVENT_LAST; i++) {
- gLastEventTime[i] = LLONG_MIN;
+ if (!com::android::input::flags::rate_limit_user_activity_poke_in_dispatcher()) {
+ // Initialize
+ for (int i = 0; i <= USER_ACTIVITY_EVENT_LAST; i++) {
+ gLastEventTime[i] = LLONG_MIN;
+ }
}
gPowerManagerServiceObj = NULL;
return 0;
diff --git a/services/core/jni/gnss/Gnss.cpp b/services/core/jni/gnss/Gnss.cpp
index 8934c3a..da8928b5 100644
--- a/services/core/jni/gnss/Gnss.cpp
+++ b/services/core/jni/gnss/Gnss.cpp
@@ -196,7 +196,8 @@
jboolean GnssHal::setCallback() {
if (gnssHalAidl != nullptr) {
- sp<IGnssCallbackAidl> gnssCbIfaceAidl = new GnssCallbackAidl();
+ sp<IGnssCallbackAidl> gnssCbIfaceAidl =
+ new GnssCallbackAidl(gnssHalAidl->getInterfaceVersion());
auto status = gnssHalAidl->setCallback(gnssCbIfaceAidl);
if (!checkAidlStatus(status, "IGnssAidl setCallback() failed.")) {
return JNI_FALSE;
diff --git a/services/core/jni/gnss/GnssCallback.cpp b/services/core/jni/gnss/GnssCallback.cpp
index 60eed8e6..3d598f7 100644
--- a/services/core/jni/gnss/GnssCallback.cpp
+++ b/services/core/jni/gnss/GnssCallback.cpp
@@ -120,7 +120,7 @@
Status GnssCallbackAidl::gnssSetCapabilitiesCb(const int capabilities) {
ALOGD("%s: %du\n", __func__, capabilities);
- bool isAdrCapabilityKnown = (getInterfaceVersion() >= 3) ? true : false;
+ bool isAdrCapabilityKnown = (interfaceVersion >= 3) ? true : false;
JNIEnv* env = getJniEnv();
env->CallVoidMethod(mCallbacksObj, method_setTopHalCapabilities, capabilities,
isAdrCapabilityKnown);
@@ -178,7 +178,7 @@
Status GnssCallbackAidl::gnssNmeaCb(const int64_t timestamp, const std::string& nmea) {
// In AIDL v1, if no listener is registered, do not report nmea to the framework.
- if (getInterfaceVersion() <= 1) {
+ if (interfaceVersion <= 1) {
if (!isNmeaRegistered) {
return Status::ok();
}
diff --git a/services/core/jni/gnss/GnssCallback.h b/services/core/jni/gnss/GnssCallback.h
index 33acec8..0622e53 100644
--- a/services/core/jni/gnss/GnssCallback.h
+++ b/services/core/jni/gnss/GnssCallback.h
@@ -60,6 +60,7 @@
*/
class GnssCallbackAidl : public hardware::gnss::BnGnssCallback {
public:
+ GnssCallbackAidl(int version) : interfaceVersion(version){};
binder::Status gnssSetCapabilitiesCb(const int capabilities) override;
binder::Status gnssSetSignalTypeCapabilitiesCb(
const std::vector<android::hardware::gnss::GnssSignalType>& signalTypes) override;
@@ -73,6 +74,9 @@
binder::Status gnssRequestTimeCb() override;
binder::Status gnssRequestLocationCb(const bool independentFromGnss,
const bool isUserEmergency) override;
+
+private:
+ const int interfaceVersion;
};
/*
diff --git a/services/core/jni/gnss/GnssMeasurement.h b/services/core/jni/gnss/GnssMeasurement.h
index 7a95db8..20400fd 100644
--- a/services/core/jni/gnss/GnssMeasurement.h
+++ b/services/core/jni/gnss/GnssMeasurement.h
@@ -41,6 +41,7 @@
const std::unique_ptr<GnssMeasurementCallback>& callback,
const android::hardware::gnss::IGnssMeasurementInterface::Options& options) = 0;
virtual jboolean close() = 0;
+ virtual int getInterfaceVersion() = 0;
};
class GnssMeasurement : public GnssMeasurementInterface {
@@ -50,6 +51,9 @@
const std::unique_ptr<GnssMeasurementCallback>& callback,
const android::hardware::gnss::IGnssMeasurementInterface::Options& options) override;
jboolean close() override;
+ int getInterfaceVersion() override {
+ return mIGnssMeasurement->getInterfaceVersion();
+ }
private:
const sp<android::hardware::gnss::IGnssMeasurementInterface> mIGnssMeasurement;
@@ -63,6 +67,9 @@
const std::unique_ptr<GnssMeasurementCallback>& callback,
const android::hardware::gnss::IGnssMeasurementInterface::Options& options) override;
jboolean close() override;
+ int getInterfaceVersion() override {
+ return 0;
+ }
private:
const sp<android::hardware::gnss::V1_0::IGnssMeasurement> mIGnssMeasurement_V1_0;
diff --git a/services/core/jni/gnss/GnssMeasurementCallback.cpp b/services/core/jni/gnss/GnssMeasurementCallback.cpp
index 2982546..ebab4c3 100644
--- a/services/core/jni/gnss/GnssMeasurementCallback.cpp
+++ b/services/core/jni/gnss/GnssMeasurementCallback.cpp
@@ -392,7 +392,7 @@
jobjectArray gnssAgcArray = nullptr;
gnssAgcArray = translateAllGnssAgcs(env, data.gnssAgcs);
- if (this->getInterfaceVersion() >= 3) {
+ if (interfaceVersion >= 3) {
setMeasurementData(env, mCallbacksObj, clock, measurementArray, gnssAgcArray,
/*hasIsFullTracking=*/true, data.isFullTracking);
} else {
@@ -467,7 +467,7 @@
satellitePvt.tropoDelayMeters);
}
- if (this->getInterfaceVersion() >= 2) {
+ if (interfaceVersion >= 2) {
callObjectMethodIgnoringResult(env, satellitePvtBuilderObject,
method_satellitePvtBuilderSetTimeOfClock,
satellitePvt.timeOfClockSeconds);
diff --git a/services/core/jni/gnss/GnssMeasurementCallback.h b/services/core/jni/gnss/GnssMeasurementCallback.h
index b3de486..3cb47ce 100644
--- a/services/core/jni/gnss/GnssMeasurementCallback.h
+++ b/services/core/jni/gnss/GnssMeasurementCallback.h
@@ -53,7 +53,8 @@
class GnssMeasurementCallbackAidl : public hardware::gnss::BnGnssMeasurementCallback {
public:
- GnssMeasurementCallbackAidl() : mCallbacksObj(getCallbacksObj()) {}
+ GnssMeasurementCallbackAidl(int version)
+ : mCallbacksObj(getCallbacksObj()), interfaceVersion(version) {}
android::binder::Status gnssMeasurementCb(const hardware::gnss::GnssData& data) override;
private:
@@ -71,6 +72,7 @@
void translateGnssClock(JNIEnv* env, const hardware::gnss::GnssData& data, JavaObject& object);
jobject& mCallbacksObj;
+ const int interfaceVersion;
};
/*
@@ -110,10 +112,10 @@
class GnssMeasurementCallback {
public:
- GnssMeasurementCallback() {}
+ GnssMeasurementCallback(int version) : interfaceVersion(version) {}
sp<GnssMeasurementCallbackAidl> getAidl() {
if (callbackAidl == nullptr) {
- callbackAidl = sp<GnssMeasurementCallbackAidl>::make();
+ callbackAidl = sp<GnssMeasurementCallbackAidl>::make(interfaceVersion);
}
return callbackAidl;
}
@@ -128,6 +130,7 @@
private:
sp<GnssMeasurementCallbackAidl> callbackAidl;
sp<GnssMeasurementCallbackHidl> callbackHidl;
+ const int interfaceVersion;
};
template <class T>
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/PersonalAppsSuspensionHelper.java b/services/devicepolicy/java/com/android/server/devicepolicy/PersonalAppsSuspensionHelper.java
index 532823a..e8c5658 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/PersonalAppsSuspensionHelper.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/PersonalAppsSuspensionHelper.java
@@ -17,6 +17,7 @@
package com.android.server.devicepolicy;
import static android.accessibilityservice.AccessibilityServiceInfo.FEEDBACK_ALL_MASK;
+import static android.app.admin.flags.Flags.defaultSmsPersonalAppSuspensionFixEnabled;
import android.accessibilityservice.AccessibilityServiceInfo;
import android.annotation.Nullable;
@@ -42,6 +43,7 @@
import android.view.inputmethod.InputMethodInfo;
import com.android.internal.R;
+import com.android.internal.telephony.SmsApplication;
import com.android.server.inputmethod.InputMethodManagerInternal;
import com.android.server.utils.Slogf;
@@ -97,7 +99,7 @@
result.removeAll(getSystemLauncherPackages());
result.removeAll(getAccessibilityServices());
result.removeAll(getInputMethodPackages());
- result.remove(Telephony.Sms.getDefaultSmsPackage(mContext));
+ result.remove(getDefaultSmsPackage());
result.remove(getSettingsPackageName());
final String[] unsuspendablePackages =
@@ -202,6 +204,17 @@
return resolveInfos != null && !resolveInfos.isEmpty();
}
+ private String getDefaultSmsPackage() {
+ //TODO(b/319449037): Unflag the following change.
+ if (defaultSmsPersonalAppSuspensionFixEnabled()) {
+ return SmsApplication.getDefaultSmsApplicationAsUser(
+ mContext, /*updateIfNeeded=*/ false, mContext.getUser())
+ .getPackageName();
+ } else {
+ return Telephony.Sms.getDefaultSmsPackage(mContext);
+ }
+ }
+
void dump(IndentingPrintWriter pw) {
pw.println("PersonalAppsSuspensionHelper");
@@ -212,7 +225,7 @@
DevicePolicyManagerService.dumpApps(pw, "accessibility services",
getAccessibilityServices());
DevicePolicyManagerService.dumpApps(pw, "input method packages", getInputMethodPackages());
- pw.printf("SMS package: %s\n", Telephony.Sms.getDefaultSmsPackage(mContext));
+ pw.printf("SMS package: %s\n", getDefaultSmsPackage());
pw.printf("Settings package: %s\n", getSettingsPackageName());
DevicePolicyManagerService.dumpApps(pw, "Packages subject to suspension",
getPersonalAppsForSuspension());
diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index 86ad494..2b8bcc7 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -203,6 +203,7 @@
import com.android.server.security.KeyAttestationApplicationIdProviderService;
import com.android.server.security.KeyChainSystemService;
import com.android.server.security.rkp.RemoteProvisioningService;
+import com.android.server.selinux.SelinuxAuditLogsService;
import com.android.server.sensorprivacy.SensorPrivacyService;
import com.android.server.sensors.SensorService;
import com.android.server.signedconfig.SignedConfigService;
@@ -433,6 +434,9 @@
private static final String ROLE_SERVICE_CLASS = "com.android.role.RoleService";
private static final String GAME_MANAGER_SERVICE_CLASS =
"com.android.server.app.GameManagerService$Lifecycle";
+ private static final String ENHANCED_CONFIRMATION_SERVICE_CLASS =
+ "com.android.ecm.EnhancedConfirmationService";
+
private static final String UWB_APEX_SERVICE_JAR_PATH =
"/apex/com.android.uwb/javalib/service-uwb.jar";
private static final String UWB_SERVICE_CLASS = "com.android.server.uwb.UwbService";
@@ -1592,6 +1596,12 @@
mSystemServiceManager.startService(DropBoxManagerService.class);
t.traceEnd();
+ if (android.permission.flags.Flags.enhancedConfirmationModeApisEnabled()) {
+ t.traceBegin("StartEnhancedConfirmationService");
+ mSystemServiceManager.startService(ENHANCED_CONFIRMATION_SERVICE_CLASS);
+ t.traceEnd();
+ }
+
// Grants default permissions and defines roles
t.traceBegin("StartRoleManagerService");
LocalManagerRegistry.addManager(RoleServicePlatformHelper.class,
@@ -2609,6 +2619,14 @@
t.traceEnd();
}
+ t.traceBegin("StartSelinuxAuditLogsService");
+ try {
+ SelinuxAuditLogsService.schedule(context);
+ } catch (Throwable e) {
+ reportWtf("starting SelinuxAuditLogsService", e);
+ }
+ t.traceEnd();
+
// LauncherAppsService uses ShortcutService.
t.traceBegin("StartShortcutServiceLifecycle");
mSystemServiceManager.startService(ShortcutService.Lifecycle.class);
diff --git a/services/permission/java/com/android/server/permission/access/permission/AppIdPermissionPolicy.kt b/services/permission/java/com/android/server/permission/access/permission/AppIdPermissionPolicy.kt
index 62d2d7e..4c74878 100644
--- a/services/permission/java/com/android/server/permission/access/permission/AppIdPermissionPolicy.kt
+++ b/services/permission/java/com/android/server/permission/access/permission/AppIdPermissionPolicy.kt
@@ -22,6 +22,7 @@
import android.content.pm.PermissionInfo
import android.content.pm.SigningDetails
import android.os.Build
+import android.permission.flags.Flags
import android.util.Slog
import com.android.internal.os.RoSystemProperties
import com.android.internal.pm.permission.CompatibilityPermissionInfo
@@ -1197,15 +1198,80 @@
newState.externalState.packageStates[PLATFORM_PACKAGE_NAME]!!
.androidPackage!!
.signingDetails
- return sourceSigningDetails?.hasCommonSignerWithCapability(
- packageSigningDetails,
- SigningDetails.CertCapabilities.PERMISSION
- ) == true ||
- packageSigningDetails.hasAncestorOrSelf(platformSigningDetails) ||
- platformSigningDetails.checkCapability(
+ val hasCommonSigner =
+ sourceSigningDetails?.hasCommonSignerWithCapability(
packageSigningDetails,
SigningDetails.CertCapabilities.PERMISSION
- )
+ ) == true ||
+ packageSigningDetails.hasAncestorOrSelf(platformSigningDetails) ||
+ platformSigningDetails.checkCapability(
+ packageSigningDetails,
+ SigningDetails.CertCapabilities.PERMISSION
+ )
+ if (!Flags.signaturePermissionAllowlistEnabled()) {
+ return hasCommonSigner;
+ }
+ if (!hasCommonSigner) {
+ return false
+ }
+ // A platform signature permission also needs to be allowlisted on non-debuggable builds.
+ if (permission.packageName == PLATFORM_PACKAGE_NAME) {
+ val isRequestedByFactoryApp =
+ if (packageState.isSystem) {
+ // For updated system applications, a signature permission still needs to be
+ // allowlisted if it wasn't requested by the original application.
+ if (packageState.isUpdatedSystemApp) {
+ val disabledSystemPackage =
+ newState.externalState.disabledSystemPackageStates[
+ packageState.packageName]
+ ?.androidPackage
+ disabledSystemPackage != null &&
+ permission.name in disabledSystemPackage.requestedPermissions
+ } else {
+ true
+ }
+ } else {
+ false
+ }
+ if (
+ !(isRequestedByFactoryApp ||
+ getSignaturePermissionAllowlistState(packageState, permission.name) == true)
+ ) {
+ Slog.w(
+ LOG_TAG,
+ "Signature permission ${permission.name} for package" +
+ " ${packageState.packageName} (${packageState.path}) not in" +
+ " signature permission allowlist"
+ )
+ if (!Build.isDebuggable()) {
+ return false
+ }
+ }
+ }
+ return true
+ }
+
+ private fun MutateStateScope.getSignaturePermissionAllowlistState(
+ packageState: PackageState,
+ permissionName: String
+ ): Boolean? {
+ val permissionAllowlist = newState.externalState.permissionAllowlist
+ val packageName = packageState.packageName
+ return when {
+ packageState.isVendor || packageState.isOdm ->
+ permissionAllowlist.getVendorSignatureAppAllowlistState(packageName, permissionName)
+ packageState.isProduct ->
+ permissionAllowlist.getProductSignatureAppAllowlistState(
+ packageName,
+ permissionName
+ )
+ packageState.isSystemExt ->
+ permissionAllowlist.getSystemExtSignatureAppAllowlistState(
+ packageName,
+ permissionName
+ )
+ else -> permissionAllowlist.getSignatureAppAllowlistState(packageName, permissionName)
+ }
}
private fun MutateStateScope.checkPrivilegedPermissionAllowlist(
diff --git a/services/permission/java/com/android/server/permission/access/permission/DevicePermissionPolicy.kt b/services/permission/java/com/android/server/permission/access/permission/DevicePermissionPolicy.kt
index a0fb013..24d4952 100644
--- a/services/permission/java/com/android/server/permission/access/permission/DevicePermissionPolicy.kt
+++ b/services/permission/java/com/android/server/permission/access/permission/DevicePermissionPolicy.kt
@@ -94,7 +94,9 @@
isSystemUpdated: Boolean
) {
packageNames.forEachIndexed { _, packageName ->
- val packageState = newState.externalState.packageStates[packageName]!!
+ // The package may still be removed even if it was once notified as installed.
+ val packageState = newState.externalState.packageStates[packageName]
+ ?: return@forEachIndexed
trimPermissionStates(packageState.appId)
}
}
@@ -245,6 +247,13 @@
flagMask: Int,
flagValues: Int
): Boolean {
+ if (userId !in newState.userStates) {
+ // Despite that we check UserManagerInternal.exists() in PermissionService, we may still
+ // sometimes get race conditions between that check and the actual mutateState() call.
+ // This should rarely happen but at least we should not crash.
+ Slog.e(LOG_TAG, "Unable to update permission flags for missing user $userId")
+ return false
+ }
val oldFlags =
newState.userStates[userId]!!
.appIdDevicePermissionFlags[appId]
diff --git a/services/permission/java/com/android/server/permission/access/permission/PermissionService.kt b/services/permission/java/com/android/server/permission/access/permission/PermissionService.kt
index f469ab5..097d73a 100644
--- a/services/permission/java/com/android/server/permission/access/permission/PermissionService.kt
+++ b/services/permission/java/com/android/server/permission/access/permission/PermissionService.kt
@@ -960,8 +960,8 @@
if (permissionName !in androidPackage.requestedPermissions && oldFlags == 0) {
if (reportError) {
- throw SecurityException(
- "Permission $permissionName isn't requested by package $packageName"
+ Slog.e(
+ LOG_TAG, "Permission $permissionName isn't requested by package $packageName"
)
}
return
diff --git a/services/tests/servicestests/src/com/android/server/inputmethod/AdditionalSubtypeUtilsTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/AdditionalSubtypeUtilsTest.java
similarity index 90%
rename from services/tests/servicestests/src/com/android/server/inputmethod/AdditionalSubtypeUtilsTest.java
rename to services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/AdditionalSubtypeUtilsTest.java
index d82e6ab..0edb3df 100644
--- a/services/tests/servicestests/src/com/android/server/inputmethod/AdditionalSubtypeUtilsTest.java
+++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/AdditionalSubtypeUtilsTest.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2022 The Android Open Source Project
+ * Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -26,18 +26,13 @@
import android.view.inputmethod.InputMethodSubtype;
import androidx.test.InstrumentationRegistry;
-import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
import org.junit.Test;
-import org.junit.runner.RunWith;
import java.io.File;
import java.util.List;
-@SmallTest
-@RunWith(AndroidJUnit4.class)
-public class AdditionalSubtypeUtilsTest {
+public final class AdditionalSubtypeUtilsTest {
@Test
public void testSaveAndLoad() throws Exception {
@@ -60,7 +55,7 @@
// Save & load.
AtomicFile atomicFile = new AtomicFile(
new File(InstrumentationRegistry.getContext().getCacheDir(), "subtypes.xml"));
- AdditionalSubtypeUtils.saveToFile(allSubtypes, methodMap, atomicFile);
+ AdditionalSubtypeUtils.saveToFile(allSubtypes, InputMethodMap.of(methodMap), atomicFile);
ArrayMap<String, List<InputMethodSubtype>> loadedSubtypes = new ArrayMap<>();
AdditionalSubtypeUtils.loadFromFile(loadedSubtypes, atomicFile);
diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/ClientControllerTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/ClientControllerTest.java
index 30afa72..b9f1ea0 100644
--- a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/ClientControllerTest.java
+++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/ClientControllerTest.java
@@ -16,13 +16,14 @@
package com.android.server.inputmethod;
import static com.android.server.inputmethod.ClientController.ClientControllerCallback;
-import static com.android.server.inputmethod.ClientController.ClientState;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
import static org.junit.Assert.assertThrows;
import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyInt;
+import static org.mockito.Mockito.anyLong;
import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -53,6 +54,7 @@
private static final int ANY_DISPLAY_ID = Display.DEFAULT_DISPLAY;
private static final int ANY_CALLER_UID = 1;
private static final int ANY_CALLER_PID = 1;
+ private static final String SOME_PACKAGE_NAME = "some.package";
@Rule
public final RavenwoodRule mRavenwood = new RavenwoodRule.Builder()
@@ -81,7 +83,8 @@
}
@Test
- // TODO(b/314150112): Enable host side mode for this test once b/315544364 is fixed.
+ // TODO(b/314150112): Enable host side mode for this test once Ravenwood is enabled for
+ // inputmethod server classes.
@IgnoreUnderRavenwood(blockedBy = {InputBinding.class, IInputMethodClientInvoker.class})
public void testAddClient_cannotAddTheSameClientTwice() {
var invoker = IInputMethodClientInvoker.create(mClient, mHandler);
@@ -103,7 +106,8 @@
}
@Test
- // TODO(b/314150112): Enable host side mode for this test once b/315544364 is fixed.
+ // TODO(b/314150112): Enable host side mode for this test once Ravenwood is enabled for
+ // inputmethod server classes.
@IgnoreUnderRavenwood(blockedBy = {InputBinding.class, IInputMethodClientInvoker.class})
public void testAddClient() throws Exception {
synchronized (ImfLock.class) {
@@ -117,7 +121,8 @@
}
@Test
- // TODO(b/314150112): Enable host side mode for this test once b/315544364 is fixed.
+ // TODO(b/314150112): Enable host side mode for this test once Ravenwood is enabled for
+ // inputmethod server classes.
@IgnoreUnderRavenwood(blockedBy = {InputBinding.class, IInputMethodClientInvoker.class})
public void testRemoveClient() {
var callback = new TestClientControllerCallback();
@@ -137,6 +142,36 @@
assertThat(removed).isSameInstanceAs(added);
}
+ @Test
+ // TODO(b/314150112): Enable host side mode for this test once Ravenwood is enabled for
+ // inputmethod server classes and updated to newer Mockito with static mock support (mock
+ // InputMethodUtils#checkIfPackageBelongsToUid instead of PackageManagerInternal#isSameApp)
+ @IgnoreUnderRavenwood(blockedBy = {InputMethodUtils.class})
+ public void testVerifyClientAndPackageMatch() {
+ when(mMockPackageManagerInternal.isSameApp(eq(SOME_PACKAGE_NAME), /* flags= */
+ anyLong(), eq(ANY_CALLER_UID), /* userId= */ anyInt())).thenReturn(true);
+
+ synchronized (ImfLock.class) {
+ var invoker = IInputMethodClientInvoker.create(mClient, mHandler);
+ mController.addClient(invoker, mConnection, ANY_DISPLAY_ID, ANY_CALLER_UID,
+ ANY_CALLER_PID);
+ assertThat(
+ mController.verifyClientAndPackageMatch(mClient, SOME_PACKAGE_NAME)).isTrue();
+ }
+ }
+
+ @Test
+ public void testVerifyClientAndPackageMatch_unknownClient() {
+ synchronized (ImfLock.class) {
+ assertThrows(IllegalArgumentException.class,
+ () -> {
+ synchronized (ImfLock.class) {
+ mController.verifyClientAndPackageMatch(mClient, SOME_PACKAGE_NAME);
+ }
+ });
+ }
+ }
+
private static class TestClientControllerCallback implements ClientControllerCallback {
private final CountDownLatch mLatch = new CountDownLatch(1);
diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/DefaultImeVisibilityApplierTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/DefaultImeVisibilityApplierTest.java
index 438bea4..1c71a62 100644
--- a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/DefaultImeVisibilityApplierTest.java
+++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/DefaultImeVisibilityApplierTest.java
@@ -22,7 +22,6 @@
import static com.android.internal.inputmethod.SoftInputShowHideReason.HIDE_SOFT_INPUT;
import static com.android.internal.inputmethod.SoftInputShowHideReason.HIDE_SWITCH_USER;
import static com.android.internal.inputmethod.SoftInputShowHideReason.SHOW_SOFT_INPUT;
-import static com.android.server.inputmethod.ClientController.ClientState;
import static com.android.server.inputmethod.ImeVisibilityStateComputer.STATE_HIDE_IME;
import static com.android.server.inputmethod.ImeVisibilityStateComputer.STATE_HIDE_IME_EXPLICIT;
import static com.android.server.inputmethod.ImeVisibilityStateComputer.STATE_HIDE_IME_NOT_ALWAYS;
diff --git a/services/tests/servicestests/src/com/android/server/inputmethod/HardwareKeyboardShortcutControllerTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/HardwareKeyboardShortcutControllerTest.java
similarity index 90%
rename from services/tests/servicestests/src/com/android/server/inputmethod/HardwareKeyboardShortcutControllerTest.java
rename to services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/HardwareKeyboardShortcutControllerTest.java
index 6eedeea..b7223d6 100644
--- a/services/tests/servicestests/src/com/android/server/inputmethod/HardwareKeyboardShortcutControllerTest.java
+++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/HardwareKeyboardShortcutControllerTest.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2023 The Android Open Source Project
+ * Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -19,17 +19,11 @@
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
-import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
-
import org.junit.Test;
-import org.junit.runner.RunWith;
import java.util.Arrays;
import java.util.List;
-@SmallTest
-@RunWith(AndroidJUnit4.class)
public final class HardwareKeyboardShortcutControllerTest {
@Test
diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceRestrictImeAmountTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceRestrictImeAmountTest.java
index 570132f..71752ba 100644
--- a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceRestrictImeAmountTest.java
+++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceRestrictImeAmountTest.java
@@ -126,11 +126,9 @@
List<String> enabledComponents) {
final ArrayMap<String, List<InputMethodSubtype>> emptyAdditionalSubtypeMap =
new ArrayMap<>();
- final ArrayMap<String, InputMethodInfo> methodMap = new ArrayMap<>();
- final ArrayList<InputMethodInfo> methodList = new ArrayList<>();
- InputMethodManagerService.filterInputMethodServices(emptyAdditionalSubtypeMap, methodMap,
- methodList, enabledComponents, mContext, resolveInfoList);
- return methodList;
+ final InputMethodMap methodMap = InputMethodManagerService.filterInputMethodServices(
+ emptyAdditionalSubtypeMap, enabledComponents, mContext, resolveInfoList);
+ return methodMap.values();
}
private ResolveInfo createFakeSystemResolveInfo(String packageName, String componentName) {
diff --git a/services/tests/servicestests/src/com/android/server/inputmethod/InputMethodManagerServiceTests.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceTests.java
similarity index 93%
rename from services/tests/servicestests/src/com/android/server/inputmethod/InputMethodManagerServiceTests.java
rename to services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceTests.java
index fd65807..a33e52f 100644
--- a/services/tests/servicestests/src/com/android/server/inputmethod/InputMethodManagerServiceTests.java
+++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceTests.java
@@ -25,23 +25,14 @@
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
-import android.platform.test.annotations.Presubmit;
-
-import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
-
import com.android.internal.inputmethod.SoftInputShowHideReason;
import org.junit.Test;
-import org.junit.runner.RunWith;
import java.io.PrintWriter;
import java.io.StringWriter;
-@Presubmit
-@SmallTest
-@RunWith(AndroidJUnit4.class)
-public class InputMethodManagerServiceTests {
+public final class InputMethodManagerServiceTests {
static final int SYSTEM_DECORATION_SUPPORT_DISPLAY_ID = 2;
static final int NO_SYSTEM_DECORATION_SUPPORT_DISPLAY_ID = 3;
diff --git a/services/tests/servicestests/src/com/android/server/inputmethod/InputMethodSettingsTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodSettingsTest.java
similarity index 96%
rename from services/tests/servicestests/src/com/android/server/inputmethod/InputMethodSettingsTest.java
rename to services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodSettingsTest.java
index a55d1c4..75118ea 100644
--- a/services/tests/servicestests/src/com/android/server/inputmethod/InputMethodSettingsTest.java
+++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodSettingsTest.java
@@ -22,14 +22,9 @@
import android.util.IntArray;
import androidx.annotation.NonNull;
-import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
import org.junit.Test;
-import org.junit.runner.RunWith;
-@SmallTest
-@RunWith(AndroidJUnit4.class)
public final class InputMethodSettingsTest {
private static void verifyUpdateEnabledImeString(@NonNull String expectedEnabledImeStr,
@NonNull String initialEnabledImeStr, @NonNull String imeId,
diff --git a/services/tests/servicestests/src/com/android/server/inputmethod/InputMethodSubtypeSwitchingControllerTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodSubtypeSwitchingControllerTest.java
similarity index 98%
rename from services/tests/servicestests/src/com/android/server/inputmethod/InputMethodSubtypeSwitchingControllerTest.java
rename to services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodSubtypeSwitchingControllerTest.java
index 0884b78..fbe384a 100644
--- a/services/tests/servicestests/src/com/android/server/inputmethod/InputMethodSubtypeSwitchingControllerTest.java
+++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodSubtypeSwitchingControllerTest.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2014 The Android Open Source Project
+ * Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -28,22 +28,16 @@
import android.view.inputmethod.InputMethodSubtype;
import android.view.inputmethod.InputMethodSubtype.InputMethodSubtypeBuilder;
-import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
-
import com.android.server.inputmethod.InputMethodSubtypeSwitchingController.ControllerImpl;
import com.android.server.inputmethod.InputMethodSubtypeSwitchingController.ImeSubtypeListItem;
import org.junit.Test;
-import org.junit.runner.RunWith;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
-@SmallTest
-@RunWith(AndroidJUnit4.class)
-public class InputMethodSubtypeSwitchingControllerTest {
+public final class InputMethodSubtypeSwitchingControllerTest {
private static final String DUMMY_PACKAGE_NAME = "dummy package name";
private static final String DUMMY_IME_LABEL = "dummy ime label";
private static final String DUMMY_SETTING_ACTIVITY_NAME = "";
diff --git a/services/tests/servicestests/src/com/android/server/inputmethod/InputMethodUtilsTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodUtilsTest.java
similarity index 98%
rename from services/tests/servicestests/src/com/android/server/inputmethod/InputMethodUtilsTest.java
rename to services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodUtilsTest.java
index 9688ef6..2857619 100644
--- a/services/tests/servicestests/src/com/android/server/inputmethod/InputMethodUtilsTest.java
+++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodUtilsTest.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2013 The Android Open Source Project
+ * Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -41,15 +41,12 @@
import androidx.annotation.NonNull;
import androidx.test.InstrumentationRegistry;
-import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
import com.android.internal.inputmethod.StartInputFlags;
import com.google.common.truth.Truth;
import org.junit.Test;
-import org.junit.runner.RunWith;
import java.util.ArrayList;
import java.util.Collections;
@@ -57,9 +54,7 @@
import java.util.Locale;
import java.util.Objects;
-@SmallTest
-@RunWith(AndroidJUnit4.class)
-public class InputMethodUtilsTest {
+public final class InputMethodUtilsTest {
private static final boolean IS_AUX = true;
private static final boolean IS_DEFAULT = true;
private static final boolean IS_OVERRIDES_IMPLICITLY_ENABLED_SUBTYPE = true;
@@ -274,7 +269,7 @@
"com.android.apps.inputmethod.latin", "FakeLatinIme", !IS_AUX, IS_DEFAULT,
subtypes);
final ArrayList<InputMethodSubtype> result =
- SubtypeUtils.getImplicitlyApplicableSubtypesLocked(
+ SubtypeUtils.getImplicitlyApplicableSubtypes(
new LocaleList(LOCALE_EN_US), imi);
assertEquals(1, result.size());
verifyEquality(autoSubtype, result.get(0));
@@ -298,7 +293,7 @@
"com.android.apps.inputmethod.latin", "FakeLatinIme", !IS_AUX, IS_DEFAULT,
subtypes);
final ArrayList<InputMethodSubtype> result =
- SubtypeUtils.getImplicitlyApplicableSubtypesLocked(
+ SubtypeUtils.getImplicitlyApplicableSubtypes(
new LocaleList(LOCALE_EN_US), imi);
assertEquals(2, result.size());
verifyEquality(nonAutoEnUS, result.get(0));
@@ -322,7 +317,7 @@
"com.android.apps.inputmethod.latin", "FakeLatinIme", !IS_AUX, IS_DEFAULT,
subtypes);
final ArrayList<InputMethodSubtype> result =
- SubtypeUtils.getImplicitlyApplicableSubtypesLocked(
+ SubtypeUtils.getImplicitlyApplicableSubtypes(
new LocaleList(LOCALE_EN_GB), imi);
assertEquals(2, result.size());
verifyEquality(nonAutoEnGB, result.get(0));
@@ -347,7 +342,7 @@
"com.android.apps.inputmethod.latin", "FakeLatinIme", !IS_AUX, IS_DEFAULT,
subtypes);
final ArrayList<InputMethodSubtype> result =
- SubtypeUtils.getImplicitlyApplicableSubtypesLocked(
+ SubtypeUtils.getImplicitlyApplicableSubtypes(
new LocaleList(LOCALE_FR), imi);
assertEquals(2, result.size());
verifyEquality(nonAutoFrCA, result.get(0));
@@ -368,7 +363,7 @@
"com.android.apps.inputmethod.latin", "FakeLatinIme", !IS_AUX, IS_DEFAULT,
subtypes);
final ArrayList<InputMethodSubtype> result =
- SubtypeUtils.getImplicitlyApplicableSubtypesLocked(
+ SubtypeUtils.getImplicitlyApplicableSubtypes(
new LocaleList(LOCALE_FR_CA), imi);
assertEquals(2, result.size());
verifyEquality(nonAutoFrCA, result.get(0));
@@ -390,7 +385,7 @@
"com.android.apps.inputmethod.latin", "FakeLatinIme", !IS_AUX, IS_DEFAULT,
subtypes);
final ArrayList<InputMethodSubtype> result =
- SubtypeUtils.getImplicitlyApplicableSubtypesLocked(
+ SubtypeUtils.getImplicitlyApplicableSubtypes(
new LocaleList(LOCALE_JA_JP), imi);
assertEquals(3, result.size());
verifyEquality(nonAutoJa, result.get(0));
@@ -412,7 +407,7 @@
"com.android.apps.inputmethod.latin", "FakeLatinIme", !IS_AUX, IS_DEFAULT,
subtypes);
final ArrayList<InputMethodSubtype> result =
- SubtypeUtils.getImplicitlyApplicableSubtypesLocked(
+ SubtypeUtils.getImplicitlyApplicableSubtypes(
new LocaleList(LOCALE_JA_JP), imi);
assertEquals(1, result.size());
verifyEquality(nonAutoHi, result.get(0));
@@ -429,7 +424,7 @@
"com.android.apps.inputmethod.latin", "FakeLatinIme", !IS_AUX, IS_DEFAULT,
subtypes);
final ArrayList<InputMethodSubtype> result =
- SubtypeUtils.getImplicitlyApplicableSubtypesLocked(
+ SubtypeUtils.getImplicitlyApplicableSubtypes(
new LocaleList(LOCALE_JA_JP), imi);
assertEquals(1, result.size());
verifyEquality(nonAutoEnUS, result.get(0));
@@ -446,7 +441,7 @@
"com.android.apps.inputmethod.latin", "FakeLatinIme", !IS_AUX, IS_DEFAULT,
subtypes);
final ArrayList<InputMethodSubtype> result =
- SubtypeUtils.getImplicitlyApplicableSubtypesLocked(
+ SubtypeUtils.getImplicitlyApplicableSubtypes(
new LocaleList(LOCALE_JA_JP), imi);
assertEquals(1, result.size());
verifyEquality(nonAutoEnUS, result.get(0));
@@ -468,7 +463,7 @@
"com.android.apps.inputmethod.latin", "FakeLatinIme", !IS_AUX, IS_DEFAULT,
subtypes);
final ArrayList<InputMethodSubtype> result =
- SubtypeUtils.getImplicitlyApplicableSubtypesLocked(
+ SubtypeUtils.getImplicitlyApplicableSubtypes(
new LocaleList(Locale.forLanguageTag("sr-Latn-RS")), imi);
assertEquals(2, result.size());
assertThat(nonAutoSrLatn, is(in(result)));
@@ -488,7 +483,7 @@
"com.android.apps.inputmethod.latin", "FakeLatinIme", !IS_AUX, IS_DEFAULT,
subtypes);
final ArrayList<InputMethodSubtype> result =
- SubtypeUtils.getImplicitlyApplicableSubtypesLocked(
+ SubtypeUtils.getImplicitlyApplicableSubtypes(
new LocaleList(Locale.forLanguageTag("sr-Cyrl-RS")), imi);
assertEquals(2, result.size());
assertThat(nonAutoSrCyrl, is(in(result)));
@@ -514,7 +509,7 @@
"com.android.apps.inputmethod.latin", "FakeLatinIme", !IS_AUX, IS_DEFAULT,
subtypes);
final ArrayList<InputMethodSubtype> result =
- SubtypeUtils.getImplicitlyApplicableSubtypesLocked(
+ SubtypeUtils.getImplicitlyApplicableSubtypes(
new LocaleList(
Locale.forLanguageTag("sr-Latn-RS-x-android"),
Locale.forLanguageTag("ja-JP"),
@@ -541,7 +536,7 @@
"com.android.apps.inputmethod.latin", "FakeLatinIme", !IS_AUX, IS_DEFAULT,
subtypes);
final ArrayList<InputMethodSubtype> result =
- SubtypeUtils.getImplicitlyApplicableSubtypesLocked(
+ SubtypeUtils.getImplicitlyApplicableSubtypes(
new LocaleList(LOCALE_FIL_PH), imi);
assertEquals(1, result.size());
verifyEquality(nonAutoFil, result.get(0));
@@ -559,7 +554,7 @@
"com.android.apps.inputmethod.latin", "FakeLatinIme", !IS_AUX, IS_DEFAULT,
subtypes);
final ArrayList<InputMethodSubtype> result =
- SubtypeUtils.getImplicitlyApplicableSubtypesLocked(
+ SubtypeUtils.getImplicitlyApplicableSubtypes(
new LocaleList(LOCALE_FI), imi);
assertEquals(1, result.size());
verifyEquality(nonAutoJa, result.get(0));
@@ -575,7 +570,7 @@
"com.android.apps.inputmethod.latin", "FakeLatinIme", !IS_AUX, IS_DEFAULT,
subtypes);
final ArrayList<InputMethodSubtype> result =
- SubtypeUtils.getImplicitlyApplicableSubtypesLocked(
+ SubtypeUtils.getImplicitlyApplicableSubtypes(
new LocaleList(LOCALE_IN), imi);
assertEquals(1, result.size());
verifyEquality(nonAutoIn, result.get(0));
@@ -589,7 +584,7 @@
"com.android.apps.inputmethod.latin", "FakeLatinIme", !IS_AUX, IS_DEFAULT,
subtypes);
final ArrayList<InputMethodSubtype> result =
- SubtypeUtils.getImplicitlyApplicableSubtypesLocked(
+ SubtypeUtils.getImplicitlyApplicableSubtypes(
new LocaleList(LOCALE_ID), imi);
assertEquals(1, result.size());
verifyEquality(nonAutoIn, result.get(0));
@@ -603,7 +598,7 @@
"com.android.apps.inputmethod.latin", "FakeLatinIme", !IS_AUX, IS_DEFAULT,
subtypes);
final ArrayList<InputMethodSubtype> result =
- SubtypeUtils.getImplicitlyApplicableSubtypesLocked(
+ SubtypeUtils.getImplicitlyApplicableSubtypes(
new LocaleList(LOCALE_IN), imi);
assertEquals(1, result.size());
verifyEquality(nonAutoId, result.get(0));
@@ -617,7 +612,7 @@
"com.android.apps.inputmethod.latin", "FakeLatinIme", !IS_AUX, IS_DEFAULT,
subtypes);
final ArrayList<InputMethodSubtype> result =
- SubtypeUtils.getImplicitlyApplicableSubtypesLocked(
+ SubtypeUtils.getImplicitlyApplicableSubtypes(
new LocaleList(LOCALE_ID), imi);
assertEquals(1, result.size());
verifyEquality(nonAutoId, result.get(0));
@@ -639,7 +634,7 @@
"com.android.apps.inputmethod.latin", "FakeLatinIme", !IS_AUX, IS_DEFAULT,
subtypes);
final ArrayList<InputMethodSubtype> result =
- SubtypeUtils.getImplicitlyApplicableSubtypesLocked(
+ SubtypeUtils.getImplicitlyApplicableSubtypes(
new LocaleList(LOCALE_FR, LOCALE_EN_US, LOCALE_JA_JP), imi);
assertThat(nonAutoFrCA, is(in(result)));
assertThat(nonAutoEnUS, is(in(result)));
@@ -801,19 +796,22 @@
{
final ArrayMap<String, InputMethodInfo> methodMap = new ArrayMap<>();
methodMap.put(systemIme.getId(), systemIme);
- assertNull(InputMethodInfoUtils.chooseSystemVoiceIme(methodMap, null, ""));
+ assertNull(InputMethodInfoUtils.chooseSystemVoiceIme(InputMethodMap.of(methodMap),
+ null, ""));
}
// Returns null when the config value is empty.
{
final ArrayMap<String, InputMethodInfo> methodMap = new ArrayMap<>();
methodMap.put(systemIme.getId(), systemIme);
- assertNull(InputMethodInfoUtils.chooseSystemVoiceIme(methodMap, "", ""));
+ assertNull(InputMethodInfoUtils.chooseSystemVoiceIme(InputMethodMap.of(methodMap), "",
+ ""));
}
// Returns null when the configured package doesn't have an IME.
{
- assertNull(InputMethodInfoUtils.chooseSystemVoiceIme(new ArrayMap<>(),
+ assertNull(InputMethodInfoUtils.chooseSystemVoiceIme(
+ InputMethodMap.emptyMap(),
systemIme.getPackageName(), ""));
}
@@ -821,7 +819,8 @@
{
final ArrayMap<String, InputMethodInfo> methodMap = new ArrayMap<>();
methodMap.put(systemIme.getId(), systemIme);
- assertEquals(systemIme, InputMethodInfoUtils.chooseSystemVoiceIme(methodMap,
+ assertEquals(systemIme, InputMethodInfoUtils.chooseSystemVoiceIme(
+ InputMethodMap.of(methodMap),
systemIme.getPackageName(), null));
}
@@ -829,13 +828,15 @@
{
final ArrayMap<String, InputMethodInfo> methodMap = new ArrayMap<>();
methodMap.put(systemIme.getId(), systemIme);
- assertEquals(systemIme, InputMethodInfoUtils.chooseSystemVoiceIme(methodMap,
+ assertEquals(systemIme, InputMethodInfoUtils.chooseSystemVoiceIme(
+ InputMethodMap.of(methodMap),
systemIme.getPackageName(), ""));
}
// Returns null when the current default isn't found.
{
- assertNull(InputMethodInfoUtils.chooseSystemVoiceIme(new ArrayMap<>(),
+ assertNull(InputMethodInfoUtils.chooseSystemVoiceIme(
+ InputMethodMap.emptyMap(),
systemIme.getPackageName(), systemIme.getId()));
}
@@ -846,7 +847,7 @@
final ArrayMap<String, InputMethodInfo> methodMap = new ArrayMap<>();
methodMap.put(systemIme.getId(), systemIme);
methodMap.put(secondIme.getId(), secondIme);
- assertNull(InputMethodInfoUtils.chooseSystemVoiceIme(methodMap,
+ assertNull(InputMethodInfoUtils.chooseSystemVoiceIme(InputMethodMap.of(methodMap),
systemIme.getPackageName(), ""));
}
@@ -857,7 +858,8 @@
final ArrayMap<String, InputMethodInfo> methodMap = new ArrayMap<>();
methodMap.put(systemIme.getId(), systemIme);
methodMap.put(secondIme.getId(), secondIme);
- assertEquals(systemIme, InputMethodInfoUtils.chooseSystemVoiceIme(methodMap,
+ assertEquals(systemIme, InputMethodInfoUtils.chooseSystemVoiceIme(
+ InputMethodMap.of(methodMap),
systemIme.getPackageName(), systemIme.getId()));
}
@@ -867,7 +869,7 @@
final InputMethodInfo nonSystemIme = createFakeInputMethodInfo("NonSystemIme",
"fake.voice0", false /* isSystem */);
methodMap.put(nonSystemIme.getId(), nonSystemIme);
- assertNull(InputMethodInfoUtils.chooseSystemVoiceIme(methodMap,
+ assertNull(InputMethodInfoUtils.chooseSystemVoiceIme(InputMethodMap.of(methodMap),
nonSystemIme.getPackageName(), nonSystemIme.getId()));
}
@@ -878,7 +880,7 @@
"FakeDefaultAutoVoiceIme", "fake.voice0", false /* isSystem */);
methodMap.put(systemIme.getId(), systemIme);
methodMap.put(nonSystemIme.getId(), nonSystemIme);
- assertNull(InputMethodInfoUtils.chooseSystemVoiceIme(methodMap,
+ assertNull(InputMethodInfoUtils.chooseSystemVoiceIme(InputMethodMap.of(methodMap),
nonSystemIme.getPackageName(), ""));
}
}
diff --git a/services/tests/servicestests/src/com/android/server/inputmethod/LocaleUtilsTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/LocaleUtilsTest.java
similarity index 98%
rename from services/tests/servicestests/src/com/android/server/inputmethod/LocaleUtilsTest.java
rename to services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/LocaleUtilsTest.java
index 01f8129..d0b46f5 100644
--- a/services/tests/servicestests/src/com/android/server/inputmethod/LocaleUtilsTest.java
+++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/LocaleUtilsTest.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2016 The Android Open Source Project
+ * Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -22,18 +22,12 @@
import android.os.LocaleList;
-import androidx.test.filters.SmallTest;
-import androidx.test.runner.AndroidJUnit4;
-
import org.junit.Test;
-import org.junit.runner.RunWith;
import java.util.ArrayList;
import java.util.Locale;
-@SmallTest
-@RunWith(AndroidJUnit4.class)
-public class LocaleUtilsTest {
+public final class LocaleUtilsTest {
private static final LocaleUtils.LocaleExtractor<Locale> sIdentityMapper = source -> source;
diff --git a/services/tests/mockingservicestests/src/com/android/server/backup/SystemBackupAgentTest.java b/services/tests/mockingservicestests/src/com/android/server/backup/SystemBackupAgentTest.java
index 4095be7..18dc114 100644
--- a/services/tests/mockingservicestests/src/com/android/server/backup/SystemBackupAgentTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/backup/SystemBackupAgentTest.java
@@ -20,11 +20,13 @@
import android.annotation.NonNull;
import android.app.backup.BackupHelper;
+import android.app.backup.BackupHelperWithLogger;
import android.content.Context;
import android.content.pm.PackageManager;
import android.os.UserHandle;
import android.os.UserManager;
import android.platform.test.annotations.Presubmit;
+import android.platform.test.flag.junit.SetFlagsRule;
import android.util.ArraySet;
import static org.mockito.Mockito.when;
@@ -32,7 +34,10 @@
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
+import com.android.server.backup.Flags;
+
import org.junit.Before;
+import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
@@ -55,6 +60,9 @@
@Mock
private PackageManager mPackageManagerMock;
+ @Rule
+ public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
@@ -71,7 +79,7 @@
mSystemBackupAgent.onCreate(userHandle, /* backupDestination= */ 0);
- assertThat(mSystemBackupAgent.mAddedHelpers)
+ assertThat(mSystemBackupAgent.mAddedHelpersKey)
.containsExactly(
"account_sync_settings",
"preferred_activities",
@@ -96,7 +104,7 @@
mSystemBackupAgent.onCreate(userHandle, /* backupDestination= */ 0);
- assertThat(mSystemBackupAgent.mAddedHelpers)
+ assertThat(mSystemBackupAgent.mAddedHelpersKey)
.containsExactly(
"account_sync_settings",
"preferred_activities",
@@ -118,7 +126,7 @@
mSystemBackupAgent.onCreate(userHandle, /* backupDestination= */ 0);
- assertThat(mSystemBackupAgent.mAddedHelpers)
+ assertThat(mSystemBackupAgent.mAddedHelpersKey)
.containsExactly(
"account_sync_settings",
"notifications",
@@ -134,7 +142,7 @@
mSystemBackupAgent.onCreate(userHandle, /* backupDestination= */ 0);
- assertThat(mSystemBackupAgent.mAddedHelpers)
+ assertThat(mSystemBackupAgent.mAddedHelpersKey)
.containsExactly(
"account_sync_settings",
"preferred_activities",
@@ -147,12 +155,42 @@
"companion");
}
+ @Test
+ public void onAddHelperIfEligibleForUser_flagIsOff_helpersHaveNoLogger() {
+ UserHandle userHandle = new UserHandle(UserHandle.USER_SYSTEM);
+ when(mUserManagerMock.isProfile()).thenReturn(false);
+ mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_METRICS_SYSTEM_BACKUP_AGENTS);
+
+ mSystemBackupAgent.onCreate(userHandle, /* backupDestination= */ 0);
+
+ for (BackupHelperWithLogger helper:mSystemBackupAgent.mAddedHelpers){
+ assertThat(helper.isLoggerSet()).isFalse();
+ }
+ }
+
+ @Test
+ public void onAddHelperIfEligibleForUser_flagIsOn_helpersHaveLogger() {
+ UserHandle userHandle = new UserHandle(UserHandle.USER_SYSTEM);
+ when(mUserManagerMock.isProfile()).thenReturn(false);
+ mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_METRICS_SYSTEM_BACKUP_AGENTS);
+
+ mSystemBackupAgent.onCreate(userHandle, /* backupDestination= */ 0);
+
+ for (BackupHelperWithLogger helper:mSystemBackupAgent.mAddedHelpers){
+ assertThat(helper.isLoggerSet()).isTrue();
+ }
+ }
+
private class TestableSystemBackupAgent extends SystemBackupAgent {
- final Set<String> mAddedHelpers = new ArraySet<>();
+ final Set<String> mAddedHelpersKey = new ArraySet<>();
+ final Set<BackupHelperWithLogger> mAddedHelpers = new ArraySet<>();
@Override
public void addHelper(String keyPrefix, BackupHelper helper) {
- mAddedHelpers.add(keyPrefix);
+ mAddedHelpersKey.add(keyPrefix);
+ if (helper instanceof BackupHelperWithLogger) {
+ mAddedHelpers.add((BackupHelperWithLogger) helper);
+ }
}
@Override
diff --git a/services/tests/mockingservicestests/src/com/android/server/job/controllers/FlexibilityControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/job/controllers/FlexibilityControllerTest.java
index 93a2eef..28471b3 100644
--- a/services/tests/mockingservicestests/src/com/android/server/job/controllers/FlexibilityControllerTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/job/controllers/FlexibilityControllerTest.java
@@ -180,6 +180,7 @@
JobSchedulerService.sElapsedRealtimeClock =
Clock.fixed(Instant.ofEpochMilli(FROZEN_TIME), ZoneOffset.UTC);
// Initialize real objects.
+ doReturn(Long.MAX_VALUE).when(mPrefetchController).getNextEstimatedLaunchTimeLocked(any());
ArgumentCaptor<BroadcastReceiver> receiverCaptor =
ArgumentCaptor.forClass(BroadcastReceiver.class);
mFlexibilityController = new FlexibilityController(mJobSchedulerService,
diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/PackageArchiverTest.java b/services/tests/mockingservicestests/src/com/android/server/pm/PackageArchiverTest.java
index e989d7b..a65ef00 100644
--- a/services/tests/mockingservicestests/src/com/android/server/pm/PackageArchiverTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/pm/PackageArchiverTest.java
@@ -367,7 +367,7 @@
verify(mInstallerService).uninstall(
eq(new VersionedPackage(PACKAGE, PackageManager.VERSION_CODE_HIGHEST)),
eq(CALLER_PACKAGE), eq(DELETE_ARCHIVE | DELETE_KEEP_DATA), eq(mIntentSender),
- eq(UserHandle.CURRENT.getIdentifier()), anyInt());
+ eq(UserHandle.CURRENT.getIdentifier()), anyInt(), anyInt());
ArchiveState expectedArchiveState = createArchiveState();
ArchiveState actualArchiveState = mPackageSetting.readUserState(
@@ -391,7 +391,7 @@
eq(CALLER_PACKAGE),
eq(DELETE_ARCHIVE | DELETE_KEEP_DATA),
eq(mIntentSender),
- eq(UserHandle.CURRENT.getIdentifier()), anyInt());
+ eq(UserHandle.CURRENT.getIdentifier()), anyInt(), anyInt());
ArchiveState expectedArchiveState = createArchiveState();
ArchiveState actualArchiveState = mPackageSetting.readUserState(
@@ -552,22 +552,20 @@
when(mComputer.getPackageStateFiltered(eq(PACKAGE), anyInt(), anyInt())).thenReturn(
null);
- assertThat(mArchiveManager.getArchivedAppIcon(PACKAGE, UserHandle.CURRENT,
- CALLER_PACKAGE)).isNull();
+ assertThat(mArchiveManager.getArchivedAppIcon(PACKAGE, UserHandle.CURRENT)).isNull();
}
@Test
public void getArchivedAppIcon_notArchived() {
- assertThat(mArchiveManager.getArchivedAppIcon(PACKAGE, UserHandle.CURRENT,
- CALLER_PACKAGE)).isNull();
+ assertThat(mArchiveManager.getArchivedAppIcon(PACKAGE, UserHandle.CURRENT)).isNull();
}
@Test
public void getArchivedAppIcon_success() {
mUserState.setArchiveState(createArchiveState()).setInstalled(false);
- assertThat(mArchiveManager.getArchivedAppIcon(PACKAGE, UserHandle.CURRENT,
- CALLER_PACKAGE)).isEqualTo(mIcon);
+ assertThat(mArchiveManager.getArchivedAppIcon(PACKAGE, UserHandle.CURRENT)).isEqualTo(
+ mIcon);
}
diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerServiceTest.java
index 5bec903..656bc71 100644
--- a/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerServiceTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerServiceTest.java
@@ -556,7 +556,7 @@
@Test
public void testCreateUserWithLongName_TruncatesName() {
UserInfo user = mUms.createUserWithThrow(generateLongString(), USER_TYPE_FULL_SECONDARY, 0);
- assertThat(user.name.length()).isEqualTo(500);
+ assertThat(user.name.length()).isEqualTo(UserManager.MAX_USER_NAME_LENGTH);
UserInfo user1 = mUms.createUserWithThrow("Test", USER_TYPE_FULL_SECONDARY, 0);
assertThat(user1.name.length()).isEqualTo(4);
}
diff --git a/services/tests/mockingservicestests/src/com/android/server/selinux/RateLimiterTest.java b/services/tests/mockingservicestests/src/com/android/server/selinux/RateLimiterTest.java
new file mode 100644
index 0000000..01c7fbe
--- /dev/null
+++ b/services/tests/mockingservicestests/src/com/android/server/selinux/RateLimiterTest.java
@@ -0,0 +1,113 @@
+/*
+ * 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.server.selinux;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import com.android.internal.os.Clock;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.time.Duration;
+import java.time.temporal.ChronoUnit;
+
+@RunWith(AndroidJUnit4.class)
+public class RateLimiterTest {
+
+ private final MockClock mMockClock = new MockClock();
+
+ @Test
+ public void testRateLimiter_1QPS() {
+ RateLimiter rateLimiter = new RateLimiter(mMockClock, Duration.ofSeconds(1));
+
+ // First acquire is granted.
+ assertThat(rateLimiter.tryAcquire()).isTrue();
+ // Next acquire is negated because it's too soon.
+ assertThat(rateLimiter.tryAcquire()).isFalse();
+ // Wait >=1 seconds.
+ mMockClock.currentTimeMillis += Duration.ofSeconds(1).toMillis();
+ assertThat(rateLimiter.tryAcquire()).isTrue();
+ }
+
+ @Test
+ public void testRateLimiter_3QPS() {
+ RateLimiter rateLimiter =
+ new RateLimiter(
+ mMockClock,
+ Duration.ofSeconds(1).dividedBy(3).truncatedTo(ChronoUnit.MILLIS));
+
+ assertThat(rateLimiter.tryAcquire()).isTrue();
+ mMockClock.currentTimeMillis += Duration.ofSeconds(1).dividedBy(2).toMillis();
+ assertThat(rateLimiter.tryAcquire()).isTrue();
+ mMockClock.currentTimeMillis += Duration.ofSeconds(1).dividedBy(3).toMillis();
+ assertThat(rateLimiter.tryAcquire()).isTrue();
+ mMockClock.currentTimeMillis += Duration.ofSeconds(1).dividedBy(4).toMillis();
+ assertThat(rateLimiter.tryAcquire()).isFalse();
+ }
+
+ @Test
+ public void testRateLimiter_infiniteQPS() {
+ RateLimiter rateLimiter = new RateLimiter(mMockClock, Duration.ofMillis(0));
+
+ // so many permits.
+ assertThat(rateLimiter.tryAcquire()).isTrue();
+ assertThat(rateLimiter.tryAcquire()).isTrue();
+ assertThat(rateLimiter.tryAcquire()).isTrue();
+ assertThat(rateLimiter.tryAcquire()).isTrue();
+ assertThat(rateLimiter.tryAcquire()).isTrue();
+
+ mMockClock.currentTimeMillis += Duration.ofSeconds(10).toMillis();
+ // still so many permits.
+ assertThat(rateLimiter.tryAcquire()).isTrue();
+ assertThat(rateLimiter.tryAcquire()).isTrue();
+ assertThat(rateLimiter.tryAcquire()).isTrue();
+ assertThat(rateLimiter.tryAcquire()).isTrue();
+ assertThat(rateLimiter.tryAcquire()).isTrue();
+
+ mMockClock.currentTimeMillis += Duration.ofDays(-10).toMillis();
+ // only going backwards in time you will stop the permits.
+ assertThat(rateLimiter.tryAcquire()).isFalse();
+ assertThat(rateLimiter.tryAcquire()).isFalse();
+ assertThat(rateLimiter.tryAcquire()).isFalse();
+ }
+
+ @Test
+ public void testRateLimiter_negativeQPS() {
+ RateLimiter rateLimiter = new RateLimiter(mMockClock, Duration.ofMillis(-10));
+
+ // Negative QPS is effectively turning of the rate limiter.
+ assertThat(rateLimiter.tryAcquire()).isTrue();
+ assertThat(rateLimiter.tryAcquire()).isTrue();
+ assertThat(rateLimiter.tryAcquire()).isTrue();
+ mMockClock.currentTimeMillis += Duration.ofSeconds(1000).toMillis();
+ assertThat(rateLimiter.tryAcquire()).isTrue();
+ assertThat(rateLimiter.tryAcquire()).isTrue();
+ assertThat(rateLimiter.tryAcquire()).isTrue();
+ }
+
+ private static final class MockClock extends Clock {
+
+ public long currentTimeMillis = 0;
+
+ @Override
+ public long currentTimeMillis() {
+ return currentTimeMillis;
+ }
+ }
+}
diff --git a/services/tests/mockingservicestests/src/com/android/server/selinux/SelinuxAuditLogsBuilderTest.java b/services/tests/mockingservicestests/src/com/android/server/selinux/SelinuxAuditLogsBuilderTest.java
new file mode 100644
index 0000000..b36c9bd
--- /dev/null
+++ b/services/tests/mockingservicestests/src/com/android/server/selinux/SelinuxAuditLogsBuilderTest.java
@@ -0,0 +1,202 @@
+/*
+ * 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.server.selinux;
+
+import static com.android.server.selinux.SelinuxAuditLogBuilder.PATH_MATCHER;
+import static com.android.server.selinux.SelinuxAuditLogBuilder.SCONTEXT_MATCHER;
+import static com.android.server.selinux.SelinuxAuditLogBuilder.TCONTEXT_MATCHER;
+import static com.android.server.selinux.SelinuxAuditLogBuilder.toCategories;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import com.android.server.selinux.SelinuxAuditLogBuilder.SelinuxAuditLog;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class SelinuxAuditLogsBuilderTest {
+
+ private final SelinuxAuditLogBuilder mAuditLogBuilder = new SelinuxAuditLogBuilder();
+
+ @Test
+ public void testMatcher_scontext() {
+ assertThat(SCONTEXT_MATCHER.reset("u:r:sdk_sandbox_audit:s0").matches()).isTrue();
+ assertThat(SCONTEXT_MATCHER.group("stype")).isEqualTo("sdk_sandbox_audit");
+ assertThat(SCONTEXT_MATCHER.group("scategories")).isNull();
+
+ assertThat(SCONTEXT_MATCHER.reset("u:r:sdk_sandbox_audit:s0:c123,c456").matches()).isTrue();
+ assertThat(SCONTEXT_MATCHER.group("stype")).isEqualTo("sdk_sandbox_audit");
+ assertThat(toCategories(SCONTEXT_MATCHER.group("scategories")))
+ .isEqualTo(new int[] {123, 456});
+
+ assertThat(SCONTEXT_MATCHER.reset("u:r:not_sdk_sandbox:s0").matches()).isFalse();
+ assertThat(SCONTEXT_MATCHER.reset("u:object_r:sdk_sandbox_audit:s0").matches()).isFalse();
+ assertThat(SCONTEXT_MATCHER.reset("u:r:sdk_sandbox_audit:s0:p123").matches()).isFalse();
+ }
+
+ @Test
+ public void testMatcher_tcontext() {
+ assertThat(TCONTEXT_MATCHER.reset("u:object_r:target_type:s0").matches()).isTrue();
+ assertThat(TCONTEXT_MATCHER.group("ttype")).isEqualTo("target_type");
+ assertThat(TCONTEXT_MATCHER.group("tcategories")).isNull();
+
+ assertThat(TCONTEXT_MATCHER.reset("u:object_r:target_type2:s0:c666").matches()).isTrue();
+ assertThat(TCONTEXT_MATCHER.group("ttype")).isEqualTo("target_type2");
+ assertThat(toCategories(TCONTEXT_MATCHER.group("tcategories"))).isEqualTo(new int[] {666});
+
+ assertThat(TCONTEXT_MATCHER.reset("u:r:target_type:s0").matches()).isFalse();
+ assertThat(TCONTEXT_MATCHER.reset("u:r:sdk_sandbox_audit:s0:x456").matches()).isFalse();
+ }
+
+ @Test
+ public void testMatcher_path() {
+ assertThat(PATH_MATCHER.reset("\"/data\"").matches()).isTrue();
+ assertThat(PATH_MATCHER.group("path")).isEqualTo("/data");
+ assertThat(PATH_MATCHER.reset("\"/data/local\"").matches()).isTrue();
+ assertThat(PATH_MATCHER.group("path")).isEqualTo("/data/local");
+ assertThat(PATH_MATCHER.reset("\"/data/local/tmp\"").matches()).isTrue();
+ assertThat(PATH_MATCHER.group("path")).isEqualTo("/data/local");
+
+ assertThat(PATH_MATCHER.reset("\"/data/local").matches()).isFalse();
+ assertThat(PATH_MATCHER.reset("\"_data_local\"").matches()).isFalse();
+ }
+
+ @Test
+ public void testSelinuxAuditLogsBuilder_noOptionals() {
+ mAuditLogBuilder.reset(
+ "granted { p } scontext=u:r:sdk_sandbox_audit:s0 tcontext=u:object_r:t:s0"
+ + " tclass=c");
+ assertAuditLog(
+ mAuditLogBuilder.build(), true, new String[] {"p"}, "sdk_sandbox_audit", "t", "c");
+
+ mAuditLogBuilder.reset(
+ "tclass=c2 granted { p2 } tcontext=u:object_r:t2:s0"
+ + " scontext=u:r:sdk_sandbox_audit:s0");
+ assertAuditLog(
+ mAuditLogBuilder.build(),
+ true,
+ new String[] {"p2"},
+ "sdk_sandbox_audit",
+ "t2",
+ "c2");
+ }
+
+ @Test
+ public void testSelinuxAuditLogsBuilder_withCategories() {
+ mAuditLogBuilder.reset(
+ "granted { p } scontext=u:r:sdk_sandbox_audit:s0:c123"
+ + " tcontext=u:object_r:t:s0:c456,c666 tclass=c");
+ assertAuditLog(
+ mAuditLogBuilder.build(),
+ true,
+ new String[] {"p"},
+ "sdk_sandbox_audit",
+ new int[] {123},
+ "t",
+ new int[] {456, 666},
+ "c",
+ null,
+ false);
+ }
+
+ @Test
+ public void testSelinuxAuditLogsBuilder_withPath() {
+ mAuditLogBuilder.reset(
+ "granted { p } scontext=u:r:sdk_sandbox_audit:s0 path=\"/very/long/path\""
+ + " tcontext=u:object_r:t:s0 tclass=c");
+ assertAuditLog(
+ mAuditLogBuilder.build(),
+ true,
+ new String[] {"p"},
+ "sdk_sandbox_audit",
+ null,
+ "t",
+ null,
+ "c",
+ "/very/long",
+ false);
+ }
+
+ @Test
+ public void testSelinuxAuditLogsBuilder_withPermissive() {
+ mAuditLogBuilder.reset(
+ "granted { p } scontext=u:r:sdk_sandbox_audit:s0 permissive=0"
+ + " tcontext=u:object_r:t:s0 tclass=c");
+ assertAuditLog(
+ mAuditLogBuilder.build(),
+ true,
+ new String[] {"p"},
+ "sdk_sandbox_audit",
+ null,
+ "t",
+ null,
+ "c",
+ null,
+ false);
+
+ mAuditLogBuilder.reset(
+ "granted { p } scontext=u:r:sdk_sandbox_audit:s0 tcontext=u:object_r:t:s0 tclass=c"
+ + " permissive=1");
+ assertAuditLog(
+ mAuditLogBuilder.build(),
+ true,
+ new String[] {"p"},
+ "sdk_sandbox_audit",
+ null,
+ "t",
+ null,
+ "c",
+ null,
+ true);
+ }
+
+ private void assertAuditLog(
+ SelinuxAuditLog auditLog,
+ boolean granted,
+ String[] permissions,
+ String sType,
+ String tType,
+ String tClass) {
+ assertAuditLog(
+ auditLog, granted, permissions, sType, null, tType, null, tClass, null, false);
+ }
+
+ private void assertAuditLog(
+ SelinuxAuditLog auditLog,
+ boolean granted,
+ String[] permissions,
+ String sType,
+ int[] sCategories,
+ String tType,
+ int[] tCategories,
+ String tClass,
+ String path,
+ boolean permissive) {
+ assertThat(auditLog).isNotNull();
+ assertThat(auditLog.mGranted).isEqualTo(granted);
+ assertThat(auditLog.mPermissions).isEqualTo(permissions);
+ assertThat(auditLog.mSType).isEqualTo(sType);
+ assertThat(auditLog.mSCategories).isEqualTo(sCategories);
+ assertThat(auditLog.mTType).isEqualTo(tType);
+ assertThat(auditLog.mTCategories).isEqualTo(tCategories);
+ assertThat(auditLog.mTClass).isEqualTo(tClass);
+ assertThat(auditLog.mPath).isEqualTo(path);
+ assertThat(auditLog.mPermissive).isEqualTo(permissive);
+ }
+}
diff --git a/services/tests/mockingservicestests/src/com/android/server/selinux/SelinuxAuditLogsCollectorTest.java b/services/tests/mockingservicestests/src/com/android/server/selinux/SelinuxAuditLogsCollectorTest.java
new file mode 100644
index 0000000..9758ea5
--- /dev/null
+++ b/services/tests/mockingservicestests/src/com/android/server/selinux/SelinuxAuditLogsCollectorTest.java
@@ -0,0 +1,644 @@
+/*
+ * 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.server.selinux;
+
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+
+import android.util.EventLog;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import com.android.internal.os.Clock;
+import com.android.internal.util.FrameworkStatsLog;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.MockitoSession;
+
+import java.time.Duration;
+import java.time.Instant;
+import java.util.Arrays;
+import java.util.stream.Collectors;
+
+@RunWith(AndroidJUnit4.class)
+public class SelinuxAuditLogsCollectorTest {
+
+ // Fake tag to use for testing
+ private static final int ANSWER_TAG = 42;
+
+ private final MockClock mClock = new MockClock();
+
+ private final SelinuxAuditLogsCollector mSelinuxAutidLogsCollector =
+ // Ignore rate limiting for tests
+ new SelinuxAuditLogsCollector(
+ new RateLimiter(mClock, /* window= */ Duration.ofMillis(0)),
+ new QuotaLimiter(
+ mClock, /* windowSize= */ Duration.ofHours(1), /* maxPermits= */ 5));
+
+ private MockitoSession mMockitoSession;
+
+ @Before
+ public void setUp() {
+ // move the clock forward for the limiters.
+ mClock.currentTimeMillis += Duration.ofHours(1).toMillis();
+ // Ignore what was written in the event logs by previous tests.
+ mSelinuxAutidLogsCollector.mLastWrite = Instant.now();
+
+ mMockitoSession =
+ mockitoSession().initMocks(this).mockStatic(FrameworkStatsLog.class).startMocking();
+ }
+
+ @After
+ public void tearDown() {
+ mMockitoSession.finishMocking();
+ }
+
+ @Test
+ public void testWriteSdkSandboxAuditLogs() {
+ writeTestLog("granted", "perm", "sdk_sandbox_audit", "ttype", "tclass");
+ writeTestLog("denied", "perm1", "sdk_sandbox_audit", "ttype1", "tclass1");
+
+ boolean done = mSelinuxAutidLogsCollector.collect(ANSWER_TAG);
+
+ assertThat(done).isTrue();
+ verify(
+ () ->
+ FrameworkStatsLog.write(
+ FrameworkStatsLog.SELINUX_AUDIT_LOG,
+ true,
+ new String[] {"perm"},
+ "sdk_sandbox_audit",
+ null,
+ "ttype",
+ null,
+ "tclass",
+ null,
+ false));
+ verify(
+ () ->
+ FrameworkStatsLog.write(
+ FrameworkStatsLog.SELINUX_AUDIT_LOG,
+ false,
+ new String[] {"perm1"},
+ "sdk_sandbox_audit",
+ null,
+ "ttype1",
+ null,
+ "tclass1",
+ null,
+ false));
+ }
+
+ @Test
+ public void testWriteSdkSandboxAuditLogs_multiplePerms() {
+ writeTestLog("denied", "perm1 perm2", "sdk_sandbox_audit", "ttype", "tclass");
+ writeTestLog("denied", "perm3 perm4", "sdk_sandbox_audit", "ttype", "tclass");
+
+ boolean done = mSelinuxAutidLogsCollector.collect(ANSWER_TAG);
+
+ assertThat(done).isTrue();
+ verify(
+ () ->
+ FrameworkStatsLog.write(
+ FrameworkStatsLog.SELINUX_AUDIT_LOG,
+ false,
+ new String[] {"perm1", "perm2"},
+ "sdk_sandbox_audit",
+ null,
+ "ttype",
+ null,
+ "tclass",
+ null,
+ false));
+ verify(
+ () ->
+ FrameworkStatsLog.write(
+ FrameworkStatsLog.SELINUX_AUDIT_LOG,
+ false,
+ new String[] {"perm3", "perm4"},
+ "sdk_sandbox_audit",
+ null,
+ "ttype",
+ null,
+ "tclass",
+ null,
+ false));
+ }
+
+ @Test
+ public void testWriteSdkSandboxAuditLogs_withPaths() {
+ writeTestLog("denied", "perm", "sdk_sandbox_audit", "ttype", "tclass", "/good/path");
+ writeTestLog("denied", "perm", "sdk_sandbox_audit", "ttype", "tclass", "/very/long/path");
+ writeTestLog("denied", "perm", "sdk_sandbox_audit", "ttype", "tclass", "/short_path");
+ writeTestLog("denied", "perm", "sdk_sandbox_audit", "ttype", "tclass", "not_a_path");
+
+ boolean done = mSelinuxAutidLogsCollector.collect(ANSWER_TAG);
+
+ assertThat(done).isTrue();
+ verify(
+ () ->
+ FrameworkStatsLog.write(
+ FrameworkStatsLog.SELINUX_AUDIT_LOG,
+ false,
+ new String[] {"perm"},
+ "sdk_sandbox_audit",
+ null,
+ "ttype",
+ null,
+ "tclass",
+ "/good/path",
+ false));
+ verify(
+ () ->
+ FrameworkStatsLog.write(
+ FrameworkStatsLog.SELINUX_AUDIT_LOG,
+ false,
+ new String[] {"perm"},
+ "sdk_sandbox_audit",
+ null,
+ "ttype",
+ null,
+ "tclass",
+ "/very/long",
+ false));
+ verify(
+ () ->
+ FrameworkStatsLog.write(
+ FrameworkStatsLog.SELINUX_AUDIT_LOG,
+ false,
+ new String[] {"perm"},
+ "sdk_sandbox_audit",
+ null,
+ "ttype",
+ null,
+ "tclass",
+ "/short_path",
+ false));
+ verify(
+ () ->
+ FrameworkStatsLog.write(
+ FrameworkStatsLog.SELINUX_AUDIT_LOG,
+ false,
+ new String[] {"perm"},
+ "sdk_sandbox_audit",
+ null,
+ "ttype",
+ null,
+ "tclass",
+ null,
+ false));
+ }
+
+ @Test
+ public void testWriteSdkSandboxAuditLogs_withCategories() {
+ writeTestLog(
+ "denied", "perm", "sdk_sandbox_audit", new int[] {123}, "ttype", null, "tclass");
+ writeTestLog(
+ "denied",
+ "perm",
+ "sdk_sandbox_audit",
+ new int[] {123, 456},
+ "ttype",
+ null,
+ "tclass");
+ writeTestLog(
+ "denied", "perm", "sdk_sandbox_audit", null, "ttype", new int[] {666}, "tclass");
+ writeTestLog(
+ "denied",
+ "perm",
+ "sdk_sandbox_audit",
+ new int[] {123, 456},
+ "ttype",
+ new int[] {666, 777},
+ "tclass");
+
+ boolean done = mSelinuxAutidLogsCollector.collect(ANSWER_TAG);
+
+ assertThat(done).isTrue();
+ verify(
+ () ->
+ FrameworkStatsLog.write(
+ FrameworkStatsLog.SELINUX_AUDIT_LOG,
+ false,
+ new String[] {"perm"},
+ "sdk_sandbox_audit",
+ new int[] {123},
+ "ttype",
+ null,
+ "tclass",
+ null,
+ false));
+ verify(
+ () ->
+ FrameworkStatsLog.write(
+ FrameworkStatsLog.SELINUX_AUDIT_LOG,
+ false,
+ new String[] {"perm"},
+ "sdk_sandbox_audit",
+ new int[] {123, 456},
+ "ttype",
+ null,
+ "tclass",
+ null,
+ false));
+ verify(
+ () ->
+ FrameworkStatsLog.write(
+ FrameworkStatsLog.SELINUX_AUDIT_LOG,
+ false,
+ new String[] {"perm"},
+ "sdk_sandbox_audit",
+ null,
+ "ttype",
+ new int[] {666},
+ "tclass",
+ null,
+ false));
+ verify(
+ () ->
+ FrameworkStatsLog.write(
+ FrameworkStatsLog.SELINUX_AUDIT_LOG,
+ false,
+ new String[] {"perm"},
+ "sdk_sandbox_audit",
+ new int[] {123, 456},
+ "ttype",
+ new int[] {666, 777},
+ "tclass",
+ null,
+ false));
+ }
+
+ @Test
+ public void testWriteSdkSandboxAuditLogs_withPathAndCategories() {
+ writeTestLog(
+ "denied",
+ "perm",
+ "sdk_sandbox_audit",
+ new int[] {123},
+ "ttype",
+ new int[] {666},
+ "tclass",
+ "/a/path");
+
+ boolean done = mSelinuxAutidLogsCollector.collect(ANSWER_TAG);
+
+ assertThat(done).isTrue();
+ verify(
+ () ->
+ FrameworkStatsLog.write(
+ FrameworkStatsLog.SELINUX_AUDIT_LOG,
+ false,
+ new String[] {"perm"},
+ "sdk_sandbox_audit",
+ new int[] {123},
+ "ttype",
+ new int[] {666},
+ "tclass",
+ "/a/path",
+ false));
+ }
+
+ @Test
+ public void testWriteSdkSandboxAuditLogs_permissive() {
+ writeTestLog("denied", "perm", "sdk_sandbox_audit", "ttype", "tclass");
+ writeTestLog("denied", "perm", "sdk_sandbox_audit", "ttype", "tclass", true);
+ writeTestLog("denied", "perm", "sdk_sandbox_audit", "ttype", "tclass", false);
+
+ boolean done = mSelinuxAutidLogsCollector.collect(ANSWER_TAG);
+
+ assertThat(done).isTrue();
+ verify(
+ () ->
+ FrameworkStatsLog.write(
+ FrameworkStatsLog.SELINUX_AUDIT_LOG,
+ false,
+ new String[] {"perm"},
+ "sdk_sandbox_audit",
+ null,
+ "ttype",
+ null,
+ "tclass",
+ null,
+ false),
+ times(2));
+ verify(
+ () ->
+ FrameworkStatsLog.write(
+ FrameworkStatsLog.SELINUX_AUDIT_LOG,
+ false,
+ new String[] {"perm"},
+ "sdk_sandbox_audit",
+ null,
+ "ttype",
+ null,
+ "tclass",
+ null,
+ true));
+ }
+
+ @Test
+ public void testNotWriteAuditLogs_notSdkSandbox() {
+ writeTestLog("denied", "perm", "stype", "ttype", "tclass");
+
+ boolean done = mSelinuxAutidLogsCollector.collect(ANSWER_TAG);
+
+ assertThat(done).isTrue();
+ verify(
+ () ->
+ FrameworkStatsLog.write(
+ anyInt(),
+ anyBoolean(),
+ any(),
+ anyString(),
+ any(),
+ anyString(),
+ any(),
+ anyString(),
+ any(),
+ anyBoolean()),
+ never());
+ }
+
+ @Test
+ public void testWriteSdkSandboxAuditLogs_upToQuota() {
+ writeTestLog("denied", "perm", "sdk_sandbox_audit", "ttype", "tclass");
+ writeTestLog("denied", "perm", "sdk_sandbox_audit", "ttype", "tclass");
+ writeTestLog("denied", "perm", "sdk_sandbox_audit", "ttype", "tclass");
+ writeTestLog("denied", "perm", "sdk_sandbox_audit", "ttype", "tclass");
+ writeTestLog("denied", "perm", "sdk_sandbox_audit", "ttype", "tclass");
+ // These are not pushed.
+ writeTestLog("denied", "perm", "sdk_sandbox_audit", "ttype", "tclass");
+ writeTestLog("denied", "perm", "sdk_sandbox_audit", "ttype", "tclass");
+
+ boolean done = mSelinuxAutidLogsCollector.collect(ANSWER_TAG);
+
+ assertThat(done).isTrue();
+ verify(
+ () ->
+ FrameworkStatsLog.write(
+ anyInt(),
+ anyBoolean(),
+ any(),
+ anyString(),
+ any(),
+ anyString(),
+ any(),
+ anyString(),
+ any(),
+ anyBoolean()),
+ times(5));
+ }
+
+ @Test
+ public void testWriteSdkSandboxAuditLogs_resetQuota() {
+ writeTestLog("denied", "perm", "sdk_sandbox_audit", "ttype", "tclass");
+ writeTestLog("denied", "perm", "sdk_sandbox_audit", "ttype", "tclass");
+ writeTestLog("denied", "perm", "sdk_sandbox_audit", "ttype", "tclass");
+ writeTestLog("denied", "perm", "sdk_sandbox_audit", "ttype", "tclass");
+ writeTestLog("denied", "perm", "sdk_sandbox_audit", "ttype", "tclass");
+ writeTestLog("denied", "perm", "sdk_sandbox_audit", "ttype", "tclass");
+ writeTestLog("denied", "perm", "sdk_sandbox_audit", "ttype", "tclass");
+
+ boolean done = mSelinuxAutidLogsCollector.collect(ANSWER_TAG);
+ assertThat(done).isTrue();
+ verify(
+ () ->
+ FrameworkStatsLog.write(
+ anyInt(),
+ anyBoolean(),
+ any(),
+ anyString(),
+ any(),
+ anyString(),
+ any(),
+ anyString(),
+ any(),
+ anyBoolean()),
+ times(5));
+
+ writeTestLog("denied", "perm", "sdk_sandbox_audit", "ttype", "tclass");
+ writeTestLog("denied", "perm", "sdk_sandbox_audit", "ttype", "tclass");
+ writeTestLog("denied", "perm", "sdk_sandbox_audit", "ttype", "tclass");
+ writeTestLog("denied", "perm", "sdk_sandbox_audit", "ttype", "tclass");
+ writeTestLog("denied", "perm", "sdk_sandbox_audit", "ttype", "tclass");
+ // move the clock forward to reset the quota limiter.
+ mClock.currentTimeMillis += Duration.ofHours(1).toMillis();
+ done = mSelinuxAutidLogsCollector.collect(ANSWER_TAG);
+ assertThat(done).isTrue();
+ verify(
+ () ->
+ FrameworkStatsLog.write(
+ anyInt(),
+ anyBoolean(),
+ any(),
+ anyString(),
+ any(),
+ anyString(),
+ any(),
+ anyString(),
+ any(),
+ anyBoolean()),
+ times(10));
+ }
+
+ @Test
+ public void testNotWriteAuditLogs_stopRequested() {
+ writeTestLog("denied", "perm", "sdk_sandbox_audit", "ttype", "tclass");
+ writeTestLog("denied", "perm", "sdk_sandbox_audit", "ttype", "tclass");
+ writeTestLog("denied", "perm", "sdk_sandbox_audit", "ttype", "tclass");
+ writeTestLog("denied", "perm", "sdk_sandbox_audit", "ttype", "tclass");
+ writeTestLog("denied", "perm", "sdk_sandbox_audit", "ttype", "tclass");
+ // These are not pushed.
+ writeTestLog("denied", "perm", "sdk_sandbox_audit", "ttype", "tclass");
+ writeTestLog("denied", "perm", "sdk_sandbox_audit", "ttype", "tclass");
+
+ mSelinuxAutidLogsCollector.mStopRequested.set(true);
+ boolean done = mSelinuxAutidLogsCollector.collect(ANSWER_TAG);
+ assertThat(done).isFalse();
+ verify(
+ () ->
+ FrameworkStatsLog.write(
+ anyInt(),
+ anyBoolean(),
+ any(),
+ anyString(),
+ any(),
+ anyString(),
+ any(),
+ anyString(),
+ any(),
+ anyBoolean()),
+ never());
+
+ mSelinuxAutidLogsCollector.mStopRequested.set(false);
+ done = mSelinuxAutidLogsCollector.collect(ANSWER_TAG);
+ assertThat(done).isTrue();
+ verify(
+ () ->
+ FrameworkStatsLog.write(
+ anyInt(),
+ anyBoolean(),
+ any(),
+ anyString(),
+ any(),
+ anyString(),
+ any(),
+ anyString(),
+ any(),
+ anyBoolean()),
+ times(5));
+ }
+
+ @Test
+ public void testAuditLogs_resumeJobDoesNotExceedLimit() {
+ writeTestLog("denied", "perm", "sdk_sandbox_audit", "ttype", "tclass");
+ mSelinuxAutidLogsCollector.mStopRequested.set(true);
+
+ boolean done = mSelinuxAutidLogsCollector.collect(ANSWER_TAG);
+
+ assertThat(done).isFalse();
+ verify(
+ () ->
+ FrameworkStatsLog.write(
+ anyInt(),
+ anyBoolean(),
+ any(),
+ anyString(),
+ any(),
+ anyString(),
+ any(),
+ anyString(),
+ any(),
+ anyBoolean()),
+ never());
+ }
+
+ private static void writeTestLog(
+ String granted, String permissions, String sType, String tType, String tClass) {
+ EventLog.writeEvent(
+ ANSWER_TAG,
+ String.format(
+ "avc: %s { %s } scontext=u:r:%s:s0 tcontext=u:object_r:%s:s0 tclass=%s",
+ granted, permissions, sType, tType, tClass));
+ }
+
+ private static void writeTestLog(
+ String granted,
+ String permissions,
+ String sType,
+ String tType,
+ String tClass,
+ String path) {
+ EventLog.writeEvent(
+ ANSWER_TAG,
+ String.format(
+ "avc: %s { %s } path=\"%s\" scontext=u:r:%s:s0 tcontext=u:object_r:%s:s0"
+ + " tclass=%s",
+ granted, permissions, path, sType, tType, tClass));
+ }
+
+ private static void writeTestLog(
+ String granted,
+ String permissions,
+ String sType,
+ int[] sCategories,
+ String tType,
+ int[] tCategories,
+ String tClass) {
+ EventLog.writeEvent(
+ ANSWER_TAG,
+ String.format(
+ "avc: %s { %s } scontext=u:r:%s:s0%s tcontext=u:object_r:%s:s0%s tclass=%s",
+ granted,
+ permissions,
+ sType,
+ toCategoriesString(sCategories),
+ tType,
+ toCategoriesString(tCategories),
+ tClass));
+ }
+
+ private static void writeTestLog(
+ String granted,
+ String permissions,
+ String sType,
+ int[] sCategories,
+ String tType,
+ int[] tCategories,
+ String tClass,
+ String path) {
+ EventLog.writeEvent(
+ ANSWER_TAG,
+ String.format(
+ "avc: %s { %s } path=\"%s\" scontext=u:r:%s:s0%s"
+ + " tcontext=u:object_r:%s:s0%s tclass=%s",
+ granted,
+ permissions,
+ path,
+ sType,
+ toCategoriesString(sCategories),
+ tType,
+ toCategoriesString(tCategories),
+ tClass));
+ }
+
+ private static void writeTestLog(
+ String granted,
+ String permissions,
+ String sType,
+ String tType,
+ String tClass,
+ boolean permissive) {
+ EventLog.writeEvent(
+ ANSWER_TAG,
+ String.format(
+ "avc: %s { %s } scontext=u:r:%s:s0 tcontext=u:object_r:%s:s0 tclass=%s"
+ + " permissive=%s",
+ granted, permissions, sType, tType, tClass, permissive ? "1" : "0"));
+ }
+
+ private static String toCategoriesString(int[] categories) {
+ return (categories == null || categories.length == 0)
+ ? ""
+ : ":c"
+ + Arrays.stream(categories)
+ .mapToObj(String::valueOf)
+ .collect(Collectors.joining(",c"));
+ }
+
+ private static final class MockClock extends Clock {
+
+ public long currentTimeMillis = 0;
+
+ @Override
+ public long currentTimeMillis() {
+ return currentTimeMillis;
+ }
+ }
+}
diff --git a/services/tests/powerstatstests/Android.bp b/services/tests/powerstatstests/Android.bp
index 654d7a8d..f49f638 100644
--- a/services/tests/powerstatstests/Android.bp
+++ b/services/tests/powerstatstests/Android.bp
@@ -44,6 +44,7 @@
"servicestests-utils",
"platform-test-annotations",
"flag-junit",
+ "ravenwood-junit",
],
libs: [
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsRule.java b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsRule.java
index ca162e0..ba2b538 100644
--- a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsRule.java
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsRule.java
@@ -32,6 +32,7 @@
import android.os.HandlerThread;
import android.os.UidBatteryConsumer;
import android.os.UserBatteryConsumer;
+import android.platform.test.ravenwood.RavenwoodRule;
import android.util.SparseArray;
import androidx.test.InstrumentationRegistry;
@@ -57,7 +58,8 @@
private final PowerProfile mPowerProfile;
private final MockClock mMockClock = new MockClock();
- private final MockBatteryStatsImpl mBatteryStats;
+ private final File mHistoryDir;
+ private MockBatteryStatsImpl mBatteryStats;
private Handler mHandler;
private BatteryUsageStats mBatteryUsageStats;
@@ -66,6 +68,10 @@
private SparseArray<int[]> mCpusByPolicy = new SparseArray<>();
private SparseArray<int[]> mFreqsByPolicy = new SparseArray<>();
+ private int mDisplayCount = -1;
+ private int mPerUidModemModel = -1;
+ private NetworkStats mNetworkStats;
+
public BatteryUsageStatsRule() {
this(0, null);
}
@@ -78,16 +84,38 @@
mHandler = mock(Handler.class);
mPowerProfile = spy(new PowerProfile());
mMockClock.currentTime = currentTime;
- mBatteryStats = new MockBatteryStatsImpl(mMockClock, historyDir, mHandler);
- mBatteryStats.setPowerProfile(mPowerProfile);
+ mHistoryDir = historyDir;
+
+ if (!RavenwoodRule.isUnderRavenwood()) {
+ lateInitBatteryStats();
+ }
mCpusByPolicy.put(0, new int[]{0, 1, 2, 3});
mCpusByPolicy.put(4, new int[]{4, 5, 6, 7});
mFreqsByPolicy.put(0, new int[]{300000, 1000000, 2000000});
mFreqsByPolicy.put(4, new int[]{300000, 1000000, 2500000, 3000000});
+ }
+
+ private void lateInitBatteryStats() {
+ if (mBatteryStats != null) return;
+
+ mBatteryStats = new MockBatteryStatsImpl(mMockClock, mHistoryDir, mHandler);
+ mBatteryStats.setPowerProfile(mPowerProfile);
mBatteryStats.setCpuScalingPolicies(new CpuScalingPolicies(mCpusByPolicy, mFreqsByPolicy));
mBatteryStats.onSystemReady();
+
+ if (mDisplayCount != -1) {
+ mBatteryStats.setDisplayCountLocked(mDisplayCount);
+ }
+ if (mPerUidModemModel != -1) {
+ synchronized (mBatteryStats) {
+ mBatteryStats.setPerUidModemModel(mPerUidModemModel);
+ }
+ }
+ if (mNetworkStats != null) {
+ mBatteryStats.setNetworkStats(mNetworkStats);
+ }
}
public MockClock getMockClock() {
@@ -112,7 +140,10 @@
}
mCpusByPolicy.put(policy, relatedCpus);
mFreqsByPolicy.put(policy, frequencies);
- mBatteryStats.setCpuScalingPolicies(new CpuScalingPolicies(mCpusByPolicy, mFreqsByPolicy));
+ if (mBatteryStats != null) {
+ mBatteryStats.setCpuScalingPolicies(
+ new CpuScalingPolicies(mCpusByPolicy, mFreqsByPolicy));
+ }
return this;
}
@@ -174,13 +205,19 @@
public BatteryUsageStatsRule setNumDisplays(int value) {
when(mPowerProfile.getNumDisplays()).thenReturn(value);
- mBatteryStats.setDisplayCountLocked(value);
+ mDisplayCount = value;
+ if (mBatteryStats != null) {
+ mBatteryStats.setDisplayCountLocked(mDisplayCount);
+ }
return this;
}
public BatteryUsageStatsRule setPerUidModemModel(int perUidModemModel) {
- synchronized (mBatteryStats) {
- mBatteryStats.setPerUidModemModel(perUidModemModel);
+ mPerUidModemModel = perUidModemModel;
+ if (mBatteryStats != null) {
+ synchronized (mBatteryStats) {
+ mBatteryStats.setPerUidModemModel(mPerUidModemModel);
+ }
}
return this;
}
@@ -210,7 +247,10 @@
}
public void setNetworkStats(NetworkStats networkStats) {
- mBatteryStats.setNetworkStats(networkStats);
+ mNetworkStats = networkStats;
+ if (mBatteryStats != null) {
+ mBatteryStats.setNetworkStats(mNetworkStats);
+ }
}
@Override
@@ -225,6 +265,7 @@
}
private void before() {
+ lateInitBatteryStats();
HandlerThread bgThread = new HandlerThread("bg thread");
bgThread.start();
mHandler = new Handler(bgThread.getLooper());
diff --git a/services/tests/servicestests/Android.bp b/services/tests/servicestests/Android.bp
index 0831086..8958fac 100644
--- a/services/tests/servicestests/Android.bp
+++ b/services/tests/servicestests/Android.bp
@@ -31,6 +31,10 @@
"test-apps/SuspendTestApp/src/**/*.java",
],
+
+ kotlincflags: [
+ "-Werror",
+ ],
static_libs: [
"frameworks-base-testutils",
"services.accessibility",
@@ -78,6 +82,7 @@
"securebox",
"flag-junit",
"ravenwood-junit",
+ "net_flags_lib",
],
libs: [
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java
index 57c3a1d..95cfc2a 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java
@@ -22,6 +22,7 @@
import static android.provider.Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_NONE;
import static android.provider.Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW;
import static android.view.accessibility.Flags.FLAG_CLEANUP_ACCESSIBILITY_WARNING_DIALOG;
+import static android.view.accessibility.Flags.FLAG_SKIP_ACCESSIBILITY_WARNING_DIALOG_FOR_TRUSTED_SERVICES;
import static com.android.internal.accessibility.AccessibilityShortcutController.ACCESSIBILITY_HEARING_AIDS_COMPONENT_NAME;
import static com.android.internal.accessibility.AccessibilityShortcutController.MAGNIFICATION_CONTROLLER_NAME;
@@ -82,6 +83,7 @@
import androidx.test.filters.SmallTest;
import com.android.compatibility.common.util.TestUtils;
+import com.android.internal.R;
import com.android.internal.compat.IPlatformCompat;
import com.android.server.LocalServices;
import com.android.server.accessibility.AccessibilityManagerService.AccessibilityDisplayListener;
@@ -857,8 +859,7 @@
@RequiresFlagsEnabled(FLAG_CLEANUP_ACCESSIBILITY_WARNING_DIALOG)
public void testIsAccessibilityServiceWarningRequired_requiredByDefault() {
mockManageAccessibilityGranted(mTestableContext);
- final AccessibilityServiceInfo info = new AccessibilityServiceInfo();
- info.setComponentName(COMPONENT_NAME);
+ final AccessibilityServiceInfo info = mockAccessibilityServiceInfo(COMPONENT_NAME);
assertThat(mA11yms.isAccessibilityServiceWarningRequired(info)).isTrue();
}
@@ -867,10 +868,9 @@
@RequiresFlagsEnabled(FLAG_CLEANUP_ACCESSIBILITY_WARNING_DIALOG)
public void testIsAccessibilityServiceWarningRequired_notRequiredIfAlreadyEnabled() {
mockManageAccessibilityGranted(mTestableContext);
- final AccessibilityServiceInfo info_a = new AccessibilityServiceInfo();
- info_a.setComponentName(COMPONENT_NAME);
- final AccessibilityServiceInfo info_b = new AccessibilityServiceInfo();
- info_b.setComponentName(new ComponentName("package_b", "class_b"));
+ final AccessibilityServiceInfo info_a = mockAccessibilityServiceInfo(COMPONENT_NAME);
+ final AccessibilityServiceInfo info_b = mockAccessibilityServiceInfo(
+ new ComponentName("package_b", "class_b"));
final AccessibilityUserState userState = mA11yms.getCurrentUserState();
userState.mEnabledServices.clear();
userState.mEnabledServices.add(info_b.getComponentName());
@@ -883,12 +883,12 @@
@RequiresFlagsEnabled(FLAG_CLEANUP_ACCESSIBILITY_WARNING_DIALOG)
public void testIsAccessibilityServiceWarningRequired_notRequiredIfExistingShortcut() {
mockManageAccessibilityGranted(mTestableContext);
- final AccessibilityServiceInfo info_a = new AccessibilityServiceInfo();
- info_a.setComponentName(new ComponentName("package_a", "class_a"));
- final AccessibilityServiceInfo info_b = new AccessibilityServiceInfo();
- info_b.setComponentName(new ComponentName("package_b", "class_b"));
- final AccessibilityServiceInfo info_c = new AccessibilityServiceInfo();
- info_c.setComponentName(new ComponentName("package_c", "class_c"));
+ final AccessibilityServiceInfo info_a = mockAccessibilityServiceInfo(
+ new ComponentName("package_a", "class_a"));
+ final AccessibilityServiceInfo info_b = mockAccessibilityServiceInfo(
+ new ComponentName("package_b", "class_b"));
+ final AccessibilityServiceInfo info_c = mockAccessibilityServiceInfo(
+ new ComponentName("package_c", "class_c"));
final AccessibilityUserState userState = mA11yms.getCurrentUserState();
userState.mAccessibilityButtonTargets.clear();
userState.mAccessibilityButtonTargets.add(info_b.getComponentName().flattenToString());
@@ -900,6 +900,51 @@
assertThat(mA11yms.isAccessibilityServiceWarningRequired(info_c)).isFalse();
}
+ @Test
+ @RequiresFlagsEnabled({
+ FLAG_CLEANUP_ACCESSIBILITY_WARNING_DIALOG,
+ FLAG_SKIP_ACCESSIBILITY_WARNING_DIALOG_FOR_TRUSTED_SERVICES})
+ public void testIsAccessibilityServiceWarningRequired_notRequiredIfAllowlisted() {
+ mockManageAccessibilityGranted(mTestableContext);
+ final AccessibilityServiceInfo info_a = mockAccessibilityServiceInfo(
+ new ComponentName("package_a", "class_a"), true);
+ final AccessibilityServiceInfo info_b = mockAccessibilityServiceInfo(
+ new ComponentName("package_b", "class_b"), false);
+ final AccessibilityServiceInfo info_c = mockAccessibilityServiceInfo(
+ new ComponentName("package_c", "class_c"), true);
+ mTestableContext.getOrCreateTestableResources().addOverride(
+ R.array.config_trustedAccessibilityServices,
+ new String[]{
+ info_b.getComponentName().flattenToString(),
+ info_c.getComponentName().flattenToString()});
+
+ // info_a is not in the allowlist => require the warning
+ assertThat(mA11yms.isAccessibilityServiceWarningRequired(info_a)).isTrue();
+ // info_b is not preinstalled => require the warning
+ assertThat(mA11yms.isAccessibilityServiceWarningRequired(info_b)).isTrue();
+ // info_c is both in the allowlist and preinstalled => do not require the warning
+ assertThat(mA11yms.isAccessibilityServiceWarningRequired(info_c)).isFalse();
+ }
+
+ private static AccessibilityServiceInfo mockAccessibilityServiceInfo(
+ ComponentName componentName) {
+ return mockAccessibilityServiceInfo(componentName, false);
+ }
+
+ private static AccessibilityServiceInfo mockAccessibilityServiceInfo(
+ ComponentName componentName,
+ boolean isSystemApp) {
+ AccessibilityServiceInfo accessibilityServiceInfo =
+ Mockito.spy(new AccessibilityServiceInfo());
+ accessibilityServiceInfo.setComponentName(componentName);
+ ResolveInfo mockResolveInfo = Mockito.mock(ResolveInfo.class);
+ when(accessibilityServiceInfo.getResolveInfo()).thenReturn(mockResolveInfo);
+ mockResolveInfo.serviceInfo = Mockito.mock(ServiceInfo.class);
+ mockResolveInfo.serviceInfo.applicationInfo = Mockito.mock(ApplicationInfo.class);
+ when(mockResolveInfo.serviceInfo.applicationInfo.isSystemApp()).thenReturn(isSystemApp);
+ return accessibilityServiceInfo;
+ }
+
// Single package intents can trigger multiple PackageMonitor callbacks.
// Collect the state of the lock in a set, since tests only care if calls
// were all locked or all unlocked.
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/AuthServiceTest.java b/services/tests/servicestests/src/com/android/server/biometrics/AuthServiceTest.java
index 88b2ed4..071db68 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/AuthServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/AuthServiceTest.java
@@ -16,6 +16,7 @@
package com.android.server.biometrics;
+import static android.adaptiveauth.Flags.FLAG_REPORT_BIOMETRIC_AUTH_ATTEMPTS;
import static android.Manifest.permission.MANAGE_BIOMETRIC;
import static android.Manifest.permission.TEST_BIOMETRIC;
import static android.Manifest.permission.USE_BIOMETRIC_INTERNAL;
@@ -491,6 +492,22 @@
}
@Test
+ public void testRegisterAuthenticationStateListener_callsFaceService() throws Exception {
+ mSetFlagsRule.enableFlags(FLAG_REPORT_BIOMETRIC_AUTH_ATTEMPTS);
+ setInternalAndTestBiometricPermissions(mContext, true /* hasPermission */);
+
+ mAuthService = new AuthService(mContext, mInjector);
+ mAuthService.onStart();
+
+ final AuthenticationStateListener listener = mock(AuthenticationStateListener.class);
+
+ mAuthService.mImpl.registerAuthenticationStateListener(listener);
+
+ waitForIdle();
+ verify(mFaceService).registerAuthenticationStateListener(eq(listener));
+ }
+
+ @Test
public void testRegisterKeyguardCallback_callsBiometricServiceRegisterKeyguardCallback()
throws Exception {
setInternalAndTestBiometricPermissions(mContext, true /* hasPermission */);
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceAuthenticationClientTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceAuthenticationClientTest.java
index 3a3dd6e..f8b5b04 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceAuthenticationClientTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceAuthenticationClientTest.java
@@ -16,6 +16,7 @@
package com.android.server.biometrics.sensors.face.aidl;
+import static android.adaptiveauth.Flags.FLAG_REPORT_BIOMETRIC_AUTH_ATTEMPTS;
import static android.hardware.biometrics.BiometricConstants.BIOMETRIC_ERROR_CANCELED;
import static android.hardware.biometrics.BiometricFaceConstants.FACE_ERROR_LOCKOUT;
import static android.hardware.biometrics.BiometricFaceConstants.FACE_ERROR_LOCKOUT_PERMANENT;
@@ -49,6 +50,7 @@
import android.os.PowerManager;
import android.os.RemoteException;
import android.platform.test.annotations.Presubmit;
+import android.platform.test.flag.junit.SetFlagsRule;
import android.testing.TestableContext;
import androidx.test.filters.SmallTest;
@@ -58,6 +60,7 @@
import com.android.server.biometrics.log.BiometricLogger;
import com.android.server.biometrics.log.OperationContextExt;
import com.android.server.biometrics.sensors.AuthSessionCoordinator;
+import com.android.server.biometrics.sensors.AuthenticationStateListeners;
import com.android.server.biometrics.sensors.ClientMonitorCallback;
import com.android.server.biometrics.sensors.ClientMonitorCallbackConverter;
import com.android.server.biometrics.sensors.LockoutTracker;
@@ -81,6 +84,8 @@
@SmallTest
public class FaceAuthenticationClientTest {
+ @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
private static final int USER_ID = 12;
private static final long OP_ID = 32;
private static final int WAKE_REASON = WakeReason.LIFT;
@@ -105,6 +110,8 @@
@Mock
private ClientMonitorCallback mCallback;
@Mock
+ private AuthenticationStateListeners mAuthenticationStateListeners;
+ @Mock
private AidlResponseHandler mAidlResponseHandler;
@Mock
private ActivityTaskManager mActivityTaskManager;
@@ -264,6 +271,29 @@
verify(mHal, never()).authenticate(anyInt());
}
+ @Test
+ public void testAuthenticationStateListeners_onAuthenticationSucceeded()
+ throws RemoteException {
+ mSetFlagsRule.enableFlags(FLAG_REPORT_BIOMETRIC_AUTH_ATTEMPTS);
+ final FaceAuthenticationClient client = createClient();
+ client.start(mCallback);
+ client.onAuthenticated(new Face("friendly", 1 /* faceId */, 2 /* deviceId */),
+ true /* authenticated */, new ArrayList<>());
+
+ verify(mAuthenticationStateListeners).onAuthenticationSucceeded(anyInt(), anyInt());
+ }
+
+ @Test
+ public void testAuthenticationStateListeners_onAuthenticationFailed() throws RemoteException {
+ mSetFlagsRule.enableFlags(FLAG_REPORT_BIOMETRIC_AUTH_ATTEMPTS);
+ final FaceAuthenticationClient client = createClient();
+ client.start(mCallback);
+ client.onAuthenticated(new Face("friendly", 1 /* faceId */, 2 /* deviceId */),
+ false /* authenticated */, new ArrayList<>());
+
+ verify(mAuthenticationStateListeners).onAuthenticationFailed(anyInt(), anyInt());
+ }
+
private FaceAuthenticationClient createClient() throws RemoteException {
return createClient(2 /* version */, mClientMonitorCallbackConverter,
false /* allowBackgroundAuthentication */,
@@ -311,7 +341,8 @@
false /* requireConfirmation */,
mBiometricLogger, mBiometricContext, true /* isStrongBiometric */,
mUsageStats, lockoutTracker, allowBackgroundAuthentication,
- null /* sensorPrivacyManager */, 0 /* biometricStrength */) {
+ null /* sensorPrivacyManager */, 0 /* biometricStrength */,
+ mAuthenticationStateListeners) {
@Override
protected ActivityTaskManager getActivityTaskManager() {
return mActivityTaskManager;
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceProviderTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceProviderTest.java
index 772ec8b..7648bd17 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceProviderTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceProviderTest.java
@@ -51,6 +51,7 @@
import com.android.internal.R;
import com.android.server.biometrics.Flags;
import com.android.server.biometrics.log.BiometricContext;
+import com.android.server.biometrics.sensors.AuthenticationStateListeners;
import com.android.server.biometrics.sensors.BaseClientMonitor;
import com.android.server.biometrics.sensors.BiometricScheduler;
import com.android.server.biometrics.sensors.BiometricStateCallback;
@@ -89,6 +90,8 @@
private BiometricContext mBiometricContext;
@Mock
private BiometricStateCallback mBiometricStateCallback;
+ @Mock
+ private AuthenticationStateListeners mAuthenticationStateListeners;
private final TestLooper mLooper = new TestLooper();
private SensorProps[] mSensorProps;
@@ -119,8 +122,8 @@
mLockoutResetDispatcher = new LockoutResetDispatcher(mContext);
mFaceProvider = new FaceProvider(mContext, mBiometricStateCallback,
- mSensorProps, TAG, mLockoutResetDispatcher, mBiometricContext,
- mDaemon, new Handler(mLooper.getLooper()),
+ mAuthenticationStateListeners, mSensorProps, TAG, mLockoutResetDispatcher,
+ mBiometricContext, mDaemon, new Handler(mLooper.getLooper()),
false /* resetLockoutRequiresChallenge */, false /* testHalEnabled */);
}
@@ -154,7 +157,7 @@
final HidlFaceSensorConfig[] hidlFaceSensorConfig =
new HidlFaceSensorConfig[]{faceSensorConfig};
mFaceProvider = new FaceProvider(mContext,
- mBiometricStateCallback, hidlFaceSensorConfig, TAG,
+ mBiometricStateCallback, mAuthenticationStateListeners, hidlFaceSensorConfig, TAG,
mLockoutResetDispatcher, mBiometricContext, mDaemon,
new Handler(mLooper.getLooper()),
true /* resetLockoutRequiresChallenge */,
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/hidl/Face10Test.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/hidl/Face10Test.java
index e558c4d..78c1e08 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/hidl/Face10Test.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/hidl/Face10Test.java
@@ -44,6 +44,7 @@
import com.android.internal.R;
import com.android.server.biometrics.log.BiometricContext;
+import com.android.server.biometrics.sensors.AuthenticationStateListeners;
import com.android.server.biometrics.sensors.BiometricScheduler;
import com.android.server.biometrics.sensors.BiometricStateCallback;
import com.android.server.biometrics.sensors.LockoutResetDispatcher;
@@ -81,6 +82,8 @@
private BiometricContext mBiometricContext;
@Mock
private BiometricStateCallback mBiometricStateCallback;
+ @Mock
+ private AuthenticationStateListeners mAuthenticationStateListeners;
private final Handler mHandler = new Handler(Looper.getMainLooper());
private LockoutResetDispatcher mLockoutResetDispatcher;
@@ -116,8 +119,8 @@
Face10.sSystemClock = Clock.fixed(
Instant.ofEpochMilli(100), ZoneId.of("America/Los_Angeles"));
- mFace10 = new Face10(mContext, mBiometricStateCallback, sensorProps,
- mLockoutResetDispatcher, mHandler, mScheduler, mBiometricContext);
+ mFace10 = new Face10(mContext, mBiometricStateCallback, mAuthenticationStateListeners,
+ sensorProps, mLockoutResetDispatcher, mHandler, mScheduler, mBiometricContext);
mBinder = new Binder();
}
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClientTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClientTest.java
index 774ea5b..4ed6f74 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClientTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClientTest.java
@@ -16,6 +16,7 @@
package com.android.server.biometrics.sensors.fingerprint.aidl;
+import static android.adaptiveauth.Flags.FLAG_REPORT_BIOMETRIC_AUTH_ATTEMPTS;
import static android.hardware.biometrics.BiometricConstants.BIOMETRIC_ERROR_CANCELED;
import static com.android.systemui.shared.Flags.FLAG_SIDEFPS_CONTROLLER_REFACTOR;
@@ -451,6 +452,29 @@
}
@Test
+ public void testAuthenticationStateListeners_onAuthenticationSucceeded()
+ throws RemoteException {
+ mSetFlagsRule.enableFlags(FLAG_REPORT_BIOMETRIC_AUTH_ATTEMPTS);
+ final FingerprintAuthenticationClient client = createClient();
+ client.start(mCallback);
+ client.onAuthenticated(new Fingerprint("friendly", 1 /* fingerId */,
+ 2 /* deviceId */), true /* authenticated */, new ArrayList<>());
+
+ verify(mAuthenticationStateListeners).onAuthenticationSucceeded(anyInt(), anyInt());
+ }
+
+ @Test
+ public void testAuthenticationStateListeners_onAuthenticationFailed() throws RemoteException {
+ mSetFlagsRule.enableFlags(FLAG_REPORT_BIOMETRIC_AUTH_ATTEMPTS);
+ final FingerprintAuthenticationClient client = createClient();
+ client.start(mCallback);
+ client.onAuthenticated(new Fingerprint("friendly", 1 /* fingerId */,
+ 2 /* deviceId */), false /* authenticated */, new ArrayList<>());
+
+ verify(mAuthenticationStateListeners).onAuthenticationFailed(anyInt(), anyInt());
+ }
+
+ @Test
public void cancelsAuthWhenNotInForeground() throws Exception {
final ActivityManager.RunningTaskInfo topTask = new ActivityManager.RunningTaskInfo();
topTask.topActivity = new ComponentName("other", "thing");
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java
index 0973d46..5e38010 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java
@@ -25,6 +25,7 @@
import static com.android.server.hdmi.Constants.ADDR_TV;
import static com.android.server.hdmi.HdmiCecLocalDevice.ActiveSource;
import static com.android.server.hdmi.HdmiControlService.INITIATED_BY_ENABLE_CEC;
+import static com.android.server.hdmi.HdmiControlService.INITIATED_BY_WAKE_UP_MESSAGE;
import static com.google.common.truth.Truth.assertThat;
@@ -1807,4 +1808,35 @@
// TV should only send <Give Osd Name> once
assertEquals(1, Collections.frequency(mNativeWrapper.getResultMessages(), giveOsdName));
}
+
+ @Test
+ public void initiateCecByWakeupMessage_selectInternalSourceAfterDelay_broadcastsActiveSource() {
+ HdmiCecMessage activeSourceFromTv =
+ HdmiCecMessageBuilder.buildActiveSource(ADDR_TV, 0x0000);
+
+ mHdmiControlService.allocateLogicalAddress(mLocalDevices, INITIATED_BY_WAKE_UP_MESSAGE);
+ mTestLooper.dispatchAll();
+
+ mTestLooper.moveTimeForward(HdmiConfig.TIMEOUT_MS);
+ mTestLooper.dispatchAll();
+
+ mHdmiCecLocalDeviceTv.deviceSelect(ADDR_TV, new TestCallback());
+ mTestLooper.dispatchAll();
+
+ assertThat(mNativeWrapper.getResultMessages()).contains(activeSourceFromTv);
+ }
+
+ @Test
+ public void initiateCecByWakeupMessage_selectInternalSource_doesNotBroadcastActiveSource() {
+ HdmiCecMessage activeSourceFromTv =
+ HdmiCecMessageBuilder.buildActiveSource(ADDR_TV, 0x0000);
+
+ mHdmiControlService.allocateLogicalAddress(mLocalDevices, INITIATED_BY_WAKE_UP_MESSAGE);
+ mTestLooper.dispatchAll();
+
+ mHdmiCecLocalDeviceTv.deviceSelect(ADDR_TV, new TestCallback());
+ mTestLooper.dispatchAll();
+
+ assertThat(mNativeWrapper.getResultMessages()).doesNotContain(activeSourceFromTv);
+ }
}
diff --git a/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsServiceTests.java b/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsServiceTests.java
index 5a62d92..5081198 100644
--- a/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsServiceTests.java
+++ b/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsServiceTests.java
@@ -16,6 +16,8 @@
package com.android.server.locksettings;
+import static android.security.Flags.FLAG_REPORT_PRIMARY_AUTH_ATTEMPTS;
+
import static com.android.internal.widget.LockPatternUtils.CREDENTIAL_TYPE_NONE;
import static com.android.internal.widget.LockPatternUtils.CREDENTIAL_TYPE_PASSWORD;
import static com.android.internal.widget.LockPatternUtils.CREDENTIAL_TYPE_PATTERN;
@@ -30,25 +32,30 @@
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.app.PropertyInvalidatedCache;
+import android.os.IBinder;
import android.os.RemoteException;
import android.platform.test.annotations.Presubmit;
+import android.platform.test.flag.junit.SetFlagsRule;
import android.service.gatekeeper.GateKeeperResponse;
import android.text.TextUtils;
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
+import com.android.internal.widget.ILockSettingsStateListener;
import com.android.internal.widget.LockPatternUtils;
import com.android.internal.widget.LockscreenCredential;
import com.android.internal.widget.VerifyCredentialResponse;
import org.junit.Before;
+import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -59,6 +66,7 @@
@Presubmit
@RunWith(AndroidJUnit4.class)
public class LockSettingsServiceTests extends BaseLockSettingsServiceTests {
+ @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
@Before
public void setUp() {
@@ -399,6 +407,60 @@
}
@Test
+ public void testVerifyCredential_notifyLockSettingsStateListeners_whenGoodPassword()
+ throws Exception {
+ mSetFlagsRule.enableFlags(FLAG_REPORT_PRIMARY_AUTH_ATTEMPTS);
+ final LockscreenCredential password = newPassword("password");
+ setCredential(PRIMARY_USER_ID, password);
+ final ILockSettingsStateListener listener = mockLockSettingsStateListener();
+ mLocalService.registerLockSettingsStateListener(listener);
+
+ assertEquals(VerifyCredentialResponse.RESPONSE_OK,
+ mService.verifyCredential(password, PRIMARY_USER_ID, 0 /* flags */)
+ .getResponseCode());
+
+ verify(listener).onAuthenticationSucceeded(PRIMARY_USER_ID);
+ }
+
+ @Test
+ public void testVerifyCredential_notifyLockSettingsStateListeners_whenBadPassword()
+ throws Exception {
+ mSetFlagsRule.enableFlags(FLAG_REPORT_PRIMARY_AUTH_ATTEMPTS);
+ final LockscreenCredential password = newPassword("password");
+ setCredential(PRIMARY_USER_ID, password);
+ final LockscreenCredential badPassword = newPassword("badPassword");
+ final ILockSettingsStateListener listener = mockLockSettingsStateListener();
+ mLocalService.registerLockSettingsStateListener(listener);
+
+ assertEquals(VerifyCredentialResponse.RESPONSE_ERROR,
+ mService.verifyCredential(badPassword, PRIMARY_USER_ID, 0 /* flags */)
+ .getResponseCode());
+
+ verify(listener).onAuthenticationFailed(PRIMARY_USER_ID);
+ }
+
+ @Test
+ public void testLockSettingsStateListener_registeredThenUnregistered() throws Exception {
+ mSetFlagsRule.enableFlags(FLAG_REPORT_PRIMARY_AUTH_ATTEMPTS);
+ final LockscreenCredential password = newPassword("password");
+ setCredential(PRIMARY_USER_ID, password);
+ final LockscreenCredential badPassword = newPassword("badPassword");
+ final ILockSettingsStateListener listener = mockLockSettingsStateListener();
+
+ mLocalService.registerLockSettingsStateListener(listener);
+ assertEquals(VerifyCredentialResponse.RESPONSE_OK,
+ mService.verifyCredential(password, PRIMARY_USER_ID, 0 /* flags */)
+ .getResponseCode());
+ verify(listener).onAuthenticationSucceeded(PRIMARY_USER_ID);
+
+ mLocalService.unregisterLockSettingsStateListener(listener);
+ assertEquals(VerifyCredentialResponse.RESPONSE_ERROR,
+ mService.verifyCredential(badPassword, PRIMARY_USER_ID, 0 /* flags */)
+ .getResponseCode());
+ verify(listener, never()).onAuthenticationFailed(PRIMARY_USER_ID);
+ }
+
+ @Test
public void testSetCredentialNotPossibleInSecureFrpModeDuringSuw() {
setUserSetupComplete(false);
setSecureFrpMode(true);
@@ -537,4 +599,12 @@
assertNotEquals(0, mGateKeeperService.getSecureUserId(userId));
}
}
+
+ private ILockSettingsStateListener mockLockSettingsStateListener() {
+ ILockSettingsStateListener listener = mock(ILockSettingsStateListener.Stub.class);
+ IBinder binder = mock(IBinder.class);
+ when(binder.isBinderAlive()).thenReturn(true);
+ when(listener.asBinder()).thenReturn(binder);
+ return listener;
+ }
}
diff --git a/services/tests/servicestests/src/com/android/server/net/NetworkManagementServiceTest.java b/services/tests/servicestests/src/com/android/server/net/NetworkManagementServiceTest.java
index 13dc120..d6d2b6d 100644
--- a/services/tests/servicestests/src/com/android/server/net/NetworkManagementServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/net/NetworkManagementServiceTest.java
@@ -16,6 +16,7 @@
package com.android.server.net;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_BACKGROUND;
import static android.net.ConnectivityManager.FIREWALL_CHAIN_DOZABLE;
import static android.net.ConnectivityManager.FIREWALL_CHAIN_LOW_POWER_STANDBY;
import static android.net.ConnectivityManager.FIREWALL_CHAIN_POWERSAVE;
@@ -327,12 +328,20 @@
isRestrictedForLowPowerStandby.put(INetd.FIREWALL_RULE_DENY, true);
expected.put(FIREWALL_CHAIN_LOW_POWER_STANDBY, isRestrictedForLowPowerStandby);
+ // Background chain
+ final ArrayMap<Integer, Boolean> isRestrictedInBackground = new ArrayMap<>();
+ isRestrictedInBackground.put(NetworkPolicyManager.FIREWALL_RULE_DEFAULT, true);
+ isRestrictedInBackground.put(INetd.FIREWALL_RULE_ALLOW, false);
+ isRestrictedInBackground.put(INetd.FIREWALL_RULE_DENY, true);
+ expected.put(FIREWALL_CHAIN_BACKGROUND, isRestrictedInBackground);
+
final int[] chains = {
FIREWALL_CHAIN_STANDBY,
FIREWALL_CHAIN_POWERSAVE,
FIREWALL_CHAIN_DOZABLE,
FIREWALL_CHAIN_RESTRICTED,
- FIREWALL_CHAIN_LOW_POWER_STANDBY
+ FIREWALL_CHAIN_LOW_POWER_STANDBY,
+ FIREWALL_CHAIN_BACKGROUND
};
final int[] states = {
INetd.FIREWALL_RULE_ALLOW,
diff --git a/services/tests/servicestests/src/com/android/server/net/NetworkPolicyManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/net/NetworkPolicyManagerServiceTest.java
index 2a76452..4451cae 100644
--- a/services/tests/servicestests/src/com/android/server/net/NetworkPolicyManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/net/NetworkPolicyManagerServiceTest.java
@@ -26,12 +26,14 @@
import static android.app.ActivityManager.PROCESS_STATE_TOP;
import static android.net.ConnectivityManager.BLOCKED_METERED_REASON_DATA_SAVER;
import static android.net.ConnectivityManager.BLOCKED_METERED_REASON_USER_RESTRICTED;
+import static android.net.ConnectivityManager.BLOCKED_REASON_APP_BACKGROUND;
import static android.net.ConnectivityManager.BLOCKED_REASON_APP_STANDBY;
import static android.net.ConnectivityManager.BLOCKED_REASON_BATTERY_SAVER;
import static android.net.ConnectivityManager.BLOCKED_REASON_DOZE;
import static android.net.ConnectivityManager.BLOCKED_REASON_LOW_POWER_STANDBY;
import static android.net.ConnectivityManager.BLOCKED_REASON_NONE;
import static android.net.ConnectivityManager.CONNECTIVITY_ACTION;
+import static android.net.ConnectivityManager.FIREWALL_CHAIN_BACKGROUND;
import static android.net.ConnectivityManager.FIREWALL_CHAIN_LOW_POWER_STANDBY;
import static android.net.ConnectivityManager.FIREWALL_CHAIN_RESTRICTED;
import static android.net.ConnectivityManager.TYPE_MOBILE;
@@ -48,8 +50,13 @@
import static android.net.NetworkPolicyManager.ALLOWED_REASON_FOREGROUND;
import static android.net.NetworkPolicyManager.ALLOWED_REASON_LOW_POWER_STANDBY_ALLOWLIST;
import static android.net.NetworkPolicyManager.ALLOWED_REASON_NONE;
+import static android.net.NetworkPolicyManager.ALLOWED_REASON_NOT_IN_BACKGROUND;
+import static android.net.NetworkPolicyManager.ALLOWED_REASON_POWER_SAVE_ALLOWLIST;
+import static android.net.NetworkPolicyManager.ALLOWED_REASON_POWER_SAVE_EXCEPT_IDLE_ALLOWLIST;
+import static android.net.NetworkPolicyManager.ALLOWED_REASON_RESTRICTED_MODE_PERMISSIONS;
import static android.net.NetworkPolicyManager.ALLOWED_REASON_SYSTEM;
import static android.net.NetworkPolicyManager.ALLOWED_REASON_TOP;
+import static android.net.NetworkPolicyManager.BACKGROUND_THRESHOLD_STATE;
import static android.net.NetworkPolicyManager.FIREWALL_RULE_DEFAULT;
import static android.net.NetworkPolicyManager.POLICY_ALLOW_METERED_BACKGROUND;
import static android.net.NetworkPolicyManager.POLICY_NONE;
@@ -64,6 +71,7 @@
import static android.net.NetworkTemplate.MATCH_CARRIER;
import static android.net.NetworkTemplate.MATCH_MOBILE;
import static android.net.NetworkTemplate.MATCH_WIFI;
+import static android.os.PowerExemptionManager.REASON_OTHER;
import static android.telephony.CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED;
import static android.telephony.CarrierConfigManager.DATA_CYCLE_THRESHOLD_DISABLED;
import static android.telephony.CarrierConfigManager.DATA_CYCLE_USE_PLATFORM_DEFAULT;
@@ -146,6 +154,8 @@
import android.os.Handler;
import android.os.INetworkManagementService;
import android.os.PersistableBundle;
+import android.os.PowerExemptionManager;
+import android.os.PowerManager;
import android.os.PowerManagerInternal;
import android.os.PowerSaveState;
import android.os.RemoteException;
@@ -153,6 +163,9 @@
import android.os.UserHandle;
import android.os.UserManager;
import android.platform.test.annotations.Presubmit;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
import android.telephony.CarrierConfigManager;
import android.telephony.SubscriptionInfo;
import android.telephony.SubscriptionManager;
@@ -169,6 +182,7 @@
import android.util.Range;
import android.util.RecurrenceRule;
import android.util.SparseArray;
+import android.util.SparseIntArray;
import androidx.test.InstrumentationRegistry;
import androidx.test.filters.FlakyTest;
@@ -243,6 +257,9 @@
public class NetworkPolicyManagerServiceTest {
private static final String TAG = "NetworkPolicyManagerServiceTest";
+ @Rule
+ public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+
private static final long TEST_START = 1194220800000L;
private static final String TEST_IFACE = "test0";
private static final String TEST_WIFI_NETWORK_KEY = "TestWifiNetworkKey";
@@ -285,6 +302,7 @@
private @Mock TelephonyManager mTelephonyManager;
private @Mock UserManager mUserManager;
private @Mock NetworkStatsManager mStatsManager;
+ private @Mock PowerExemptionManager mPowerExemptionManager;
private TestDependencies mDeps;
private ArgumentCaptor<ConnectivityManager.NetworkCallback> mNetworkCallbackCaptor =
@@ -302,6 +320,7 @@
private NetworkPolicyManagerService mService;
private final ArraySet<BroadcastReceiver> mRegisteredReceivers = new ArraySet<>();
+ private BroadcastReceiver mPowerAllowlistReceiver;
/**
* In some of the tests while initializing NetworkPolicyManagerService,
@@ -446,6 +465,7 @@
@Before
public void callSystemReady() throws Exception {
MockitoAnnotations.initMocks(this);
+ when(mPowerExemptionManager.getAllowListedAppIds(anyBoolean())).thenReturn(new int[0]);
final Context context = InstrumentationRegistry.getContext();
@@ -482,6 +502,8 @@
return mUserManager;
case Context.NETWORK_STATS_SERVICE:
return mStatsManager;
+ case Context.POWER_EXEMPTION_SERVICE:
+ return mPowerExemptionManager;
default:
return super.getSystemService(name);
}
@@ -495,6 +517,9 @@
@Override
public Intent registerReceiver(BroadcastReceiver receiver,
IntentFilter filter, String broadcastPermission, Handler scheduler) {
+ if (filter.hasAction(PowerManager.ACTION_POWER_SAVE_WHITELIST_CHANGED)) {
+ mPowerAllowlistReceiver = receiver;
+ }
mRegisteredReceivers.add(receiver);
return super.registerReceiver(receiver, filter, broadcastPermission, scheduler);
}
@@ -2066,6 +2091,12 @@
expectHasUseRestrictedNetworksPermission(UID_A, true);
expectHasUseRestrictedNetworksPermission(UID_B, false);
+ // Set low enough proc-states to ensure these uids are allowed in the background chain.
+ // To maintain clean separation between separate firewall chains, the tests could
+ // check for the specific blockedReasons in the uidBlockedState.
+ callAndWaitOnUidStateChanged(UID_A, BACKGROUND_THRESHOLD_STATE - 1, 21);
+ callAndWaitOnUidStateChanged(UID_B, BACKGROUND_THRESHOLD_STATE - 1, 21);
+
Map<Integer, Integer> firewallUidRules = new ArrayMap<>();
doAnswer(arg -> {
int[] uids = arg.getArgument(1);
@@ -2113,7 +2144,111 @@
}
@Test
+ @RequiresFlagsEnabled(Flags.FLAG_NETWORK_BLOCKED_FOR_TOP_SLEEPING_AND_ABOVE)
+ public void testBackgroundChainEnabled() throws Exception {
+ verify(mNetworkManager).setFirewallChainEnabled(FIREWALL_CHAIN_BACKGROUND, true);
+ }
+
+
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_NETWORK_BLOCKED_FOR_TOP_SLEEPING_AND_ABOVE)
+ public void testBackgroundChainOnProcStateChange() throws Exception {
+ // initialization calls setFirewallChainEnabled, so we want to reset the invocations.
+ clearInvocations(mNetworkManager);
+
+ mService.mBackgroundRestrictionDelayMs = 500; // To avoid waiting too long in tests.
+
+ // The app will be blocked when there is no prior proc-state.
+ assertTrue(mService.isUidNetworkingBlocked(UID_A, false));
+
+ int procStateSeq = 23;
+ callAndWaitOnUidStateChanged(UID_A, BACKGROUND_THRESHOLD_STATE - 1, procStateSeq++);
+
+ verify(mNetworkManager).setFirewallUidRule(FIREWALL_CHAIN_BACKGROUND, UID_A,
+ FIREWALL_RULE_ALLOW);
+ assertFalse(mService.isUidNetworkingBlocked(UID_A, false));
+
+ callAndWaitOnUidStateChanged(UID_A, BACKGROUND_THRESHOLD_STATE + 1, procStateSeq++);
+
+ // The app should be blocked after a delay. Posting a message just after the delay and
+ // waiting for it to complete to ensure that the blocking code has executed.
+ waitForDelayedMessageOnHandler(mService.mBackgroundRestrictionDelayMs + 1);
+
+ verify(mNetworkManager).setFirewallUidRule(FIREWALL_CHAIN_BACKGROUND, UID_A,
+ FIREWALL_RULE_DEFAULT);
+ assertTrue(mService.isUidNetworkingBlocked(UID_A, false));
+ }
+
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_NETWORK_BLOCKED_FOR_TOP_SLEEPING_AND_ABOVE)
+ public void testBackgroundChainOnAllowlistChange() throws Exception {
+ // initialization calls setFirewallChainEnabled, so we want to reset the invocations.
+ clearInvocations(mNetworkManager);
+
+ // The apps will be blocked when there is no prior proc-state.
+ assertTrue(mService.isUidNetworkingBlocked(UID_A, false));
+ assertTrue(mService.isUidNetworkingBlocked(UID_B, false));
+
+ final int procStateSeq = 29;
+ callAndWaitOnUidStateChanged(UID_A, BACKGROUND_THRESHOLD_STATE + 1, procStateSeq);
+ assertTrue(mService.isUidNetworkingBlocked(UID_A, false));
+
+ when(mPowerExemptionManager.getAllowListedAppIds(anyBoolean()))
+ .thenReturn(new int[]{APP_ID_A, APP_ID_B});
+ final SparseIntArray firewallUidRules = new SparseIntArray();
+ doAnswer(arg -> {
+ final int[] uids = arg.getArgument(1);
+ final int[] rules = arg.getArgument(2);
+ assertTrue(uids.length == rules.length);
+
+ for (int i = 0; i < uids.length; ++i) {
+ firewallUidRules.put(uids[i], rules[i]);
+ }
+ return null;
+ }).when(mNetworkManager).setFirewallUidRules(eq(FIREWALL_CHAIN_BACKGROUND),
+ any(int[].class), any(int[].class));
+
+ mPowerAllowlistReceiver.onReceive(mServiceContext, null);
+
+ assertEquals(FIREWALL_RULE_ALLOW, firewallUidRules.get(UID_A, -1));
+ assertEquals(FIREWALL_RULE_ALLOW, firewallUidRules.get(UID_B, -1));
+
+ assertFalse(mService.isUidNetworkingBlocked(UID_A, false));
+ assertFalse(mService.isUidNetworkingBlocked(UID_B, false));
+ }
+
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_NETWORK_BLOCKED_FOR_TOP_SLEEPING_AND_ABOVE)
+ public void testBackgroundChainOnTempAllowlistChange() throws Exception {
+ // initialization calls setFirewallChainEnabled, so we want to reset the invocations.
+ clearInvocations(mNetworkManager);
+
+ // The app will be blocked as is no prior proc-state.
+ assertTrue(mService.isUidNetworkingBlocked(UID_A, false));
+
+ final int procStateSeq = 19;
+ callAndWaitOnUidStateChanged(UID_A, BACKGROUND_THRESHOLD_STATE + 1, procStateSeq);
+ assertTrue(mService.isUidNetworkingBlocked(UID_A, false));
+
+ final NetworkPolicyManagerInternal internal = LocalServices.getService(
+ NetworkPolicyManagerInternal.class);
+
+ internal.onTempPowerSaveWhitelistChange(APP_ID_A, true, REASON_OTHER, "testing");
+
+ verify(mNetworkManager).setFirewallUidRule(FIREWALL_CHAIN_BACKGROUND, UID_A,
+ FIREWALL_RULE_ALLOW);
+ assertFalse(mService.isUidNetworkingBlocked(UID_A, false));
+
+ internal.onTempPowerSaveWhitelistChange(APP_ID_A, false, REASON_OTHER, "testing");
+
+ verify(mNetworkManager).setFirewallUidRule(FIREWALL_CHAIN_BACKGROUND, UID_A,
+ FIREWALL_RULE_DEFAULT);
+ assertTrue(mService.isUidNetworkingBlocked(UID_A, false));
+ }
+
+ @Test
public void testLowPowerStandbyAllowlist() throws Exception {
+ // Chain background is also enabled but these procstates are important enough to be exempt.
callAndWaitOnUidStateChanged(UID_A, PROCESS_STATE_TOP, 0);
callAndWaitOnUidStateChanged(UID_B, ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE, 0);
callAndWaitOnUidStateChanged(UID_C, ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE, 0);
@@ -2200,7 +2335,21 @@
ALLOWED_REASON_TOP), BLOCKED_REASON_NONE);
effectiveBlockedReasons.put(Pair.create(BLOCKED_REASON_LOW_POWER_STANDBY,
ALLOWED_REASON_LOW_POWER_STANDBY_ALLOWLIST), BLOCKED_REASON_NONE);
- // TODO: test more combinations of blocked reasons.
+
+ effectiveBlockedReasons.put(Pair.create(BLOCKED_REASON_APP_BACKGROUND,
+ ALLOWED_REASON_NOT_IN_BACKGROUND), BLOCKED_REASON_NONE);
+ effectiveBlockedReasons.put(Pair.create(BLOCKED_REASON_APP_BACKGROUND
+ | BLOCKED_REASON_BATTERY_SAVER, ALLOWED_REASON_NOT_IN_BACKGROUND),
+ BLOCKED_REASON_BATTERY_SAVER);
+ effectiveBlockedReasons.put(Pair.create(BLOCKED_REASON_APP_BACKGROUND
+ | BLOCKED_REASON_DOZE, ALLOWED_REASON_NOT_IN_BACKGROUND),
+ BLOCKED_REASON_DOZE);
+ effectiveBlockedReasons.put(Pair.create(BLOCKED_REASON_APP_BACKGROUND,
+ ALLOWED_REASON_RESTRICTED_MODE_PERMISSIONS), BLOCKED_REASON_APP_BACKGROUND);
+ effectiveBlockedReasons.put(Pair.create(BLOCKED_REASON_APP_BACKGROUND,
+ ALLOWED_REASON_POWER_SAVE_ALLOWLIST), BLOCKED_REASON_NONE);
+ effectiveBlockedReasons.put(Pair.create(BLOCKED_REASON_APP_BACKGROUND,
+ ALLOWED_REASON_POWER_SAVE_EXCEPT_IDLE_ALLOWLIST), BLOCKED_REASON_NONE);
for (Map.Entry<Pair<Integer, Integer>, Integer> test : effectiveBlockedReasons.entrySet()) {
final int expectedEffectiveBlockedReasons = test.getValue();
@@ -2529,7 +2678,6 @@
private FutureIntent mRestrictBackgroundChanged;
private void postMsgAndWaitForCompletion() throws InterruptedException {
- final Handler handler = mService.getHandlerForTesting();
final CountDownLatch latch = new CountDownLatch(1);
mService.getHandlerForTesting().post(latch::countDown);
if (!latch.await(5, TimeUnit.SECONDS)) {
@@ -2537,6 +2685,14 @@
}
}
+ private void waitForDelayedMessageOnHandler(long delayMs) throws InterruptedException {
+ final CountDownLatch latch = new CountDownLatch(1);
+ mService.getHandlerForTesting().postDelayed(latch::countDown, delayMs);
+ if (!latch.await(delayMs + 5_000, TimeUnit.MILLISECONDS)) {
+ fail("Timed out waiting for delayed msg to be handled");
+ }
+ }
+
private void setSubscriptionPlans(int subId, SubscriptionPlan[] plans, String callingPackage)
throws InterruptedException {
mService.setSubscriptionPlans(subId, plans, 0, callingPackage);
diff --git a/services/tests/servicestests/src/com/android/server/om/OverlayActorEnforcerTests.kt b/services/tests/servicestests/src/com/android/server/om/OverlayActorEnforcerTests.kt
index 10f27ca..72fa949 100644
--- a/services/tests/servicestests/src/com/android/server/om/OverlayActorEnforcerTests.kt
+++ b/services/tests/servicestests/src/com/android/server/om/OverlayActorEnforcerTests.kt
@@ -80,7 +80,7 @@
@BeforeClass
@JvmStatic
fun checkAllCasesUniquelyNamed() {
- val duplicateCaseNames = CASES.mapIndexed { caseIndex, testCase ->
+ val duplicateCaseNames = CASES.mapIndexed { _, testCase ->
testCase.failures.map {
makeTestName(testCase, it.first, Params.Type.FAILURE)
} + testCase.allowed.map {
diff --git a/services/tests/servicestests/src/com/android/server/os/BugreportManagerServiceImplTest.java b/services/tests/servicestests/src/com/android/server/os/BugreportManagerServiceImplTest.java
index dc1d2c5..1c6d36b 100644
--- a/services/tests/servicestests/src/com/android/server/os/BugreportManagerServiceImplTest.java
+++ b/services/tests/servicestests/src/com/android/server/os/BugreportManagerServiceImplTest.java
@@ -17,16 +17,19 @@
package com.android.server.os;
import android.app.admin.flags.Flags;
-import static android.app.admin.flags.Flags.onboardingBugreportV2Enabled;
import static com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.when;
import android.app.role.RoleManager;
import android.content.Context;
+import android.content.pm.PackageManager;
import android.os.Binder;
import android.os.BugreportManager.BugreportCallback;
import android.os.IBinder;
@@ -48,6 +51,8 @@
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
import java.io.FileDescriptor;
import java.util.concurrent.CompletableFuture;
@@ -66,6 +71,9 @@
private BugreportManagerServiceImpl mService;
private BugreportManagerServiceImpl.BugreportFileManager mBugreportFileManager;
+ @Mock
+ private PackageManager mPackageManager;
+
private int mCallingUid = 1234;
private String mCallingPackage = "test.package";
private AtomicFile mMappingFile;
@@ -74,7 +82,8 @@
private String mBugreportFile2 = "bugreport-file2.zip";
@Before
- public void setUp() {
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
mContext = InstrumentationRegistry.getInstrumentation().getContext();
mMappingFile = new AtomicFile(mContext.getFilesDir(), "bugreport-mapping.xml");
ArraySet<String> mAllowlistedPackages = new ArraySet<>();
@@ -83,6 +92,7 @@
new BugreportManagerServiceImpl.Injector(mContext, mAllowlistedPackages,
mMappingFile));
mBugreportFileManager = new BugreportManagerServiceImpl.BugreportFileManager(mMappingFile);
+ when(mPackageManager.getPackageUidAsUser(anyString(), anyInt())).thenReturn(mCallingUid);
}
@After
@@ -115,12 +125,13 @@
assertThrows(IllegalArgumentException.class, () ->
mBugreportFileManager.ensureCallerPreviouslyGeneratedFile(
- mContext, callingInfo, Process.myUserHandle().getIdentifier(),
- "unknown-file.zip", /* forceUpdateMapping= */ true));
+ mContext, mPackageManager, callingInfo,
+ Process.myUserHandle().getIdentifier(), "unknown-file.zip",
+ /* forceUpdateMapping= */ true));
// No exception should be thrown.
mBugreportFileManager.ensureCallerPreviouslyGeneratedFile(
- mContext, callingInfo, mContext.getUserId(), mBugreportFile,
+ mContext, mPackageManager, callingInfo, mContext.getUserId(), mBugreportFile,
/* forceUpdateMapping= */ true);
}
@@ -132,7 +143,7 @@
callingInfo, mBugreportFile, /* keepOnRetrieval= */ true);
mBugreportFileManager.ensureCallerPreviouslyGeneratedFile(
- mContext, callingInfo, mContext.getUserId(), mBugreportFile,
+ mContext, mPackageManager, callingInfo, mContext.getUserId(), mBugreportFile,
/* forceUpdateMapping= */ true);
assertThat(mBugreportFileManager.mBugreportFilesToPersist).containsExactly(mBugreportFile);
@@ -148,10 +159,10 @@
// No exception should be thrown.
mBugreportFileManager.ensureCallerPreviouslyGeneratedFile(
- mContext, callingInfo, mContext.getUserId(), mBugreportFile,
+ mContext, mPackageManager, callingInfo, mContext.getUserId(), mBugreportFile,
/* forceUpdateMapping= */ true);
mBugreportFileManager.ensureCallerPreviouslyGeneratedFile(
- mContext, callingInfo, mContext.getUserId(), mBugreportFile2,
+ mContext, mPackageManager, callingInfo, mContext.getUserId(), mBugreportFile2,
/* forceUpdateMapping= */ true);
}
@@ -160,8 +171,9 @@
Pair<Integer, String> callingInfo = new Pair<>(mCallingUid, mCallingPackage);
assertThrows(IllegalArgumentException.class,
() -> mBugreportFileManager.ensureCallerPreviouslyGeneratedFile(
- mContext, callingInfo, Process.myUserHandle().getIdentifier(),
- "test-file.zip", /* forceUpdateMapping= */ true));
+ mContext, mPackageManager, callingInfo,
+ Process.myUserHandle().getIdentifier(), "test-file.zip",
+ /* forceUpdateMapping= */ true));
}
@Test
diff --git a/services/tests/servicestests/src/com/android/server/pm/BaseShortcutManagerTest.java b/services/tests/servicestests/src/com/android/server/pm/BaseShortcutManagerTest.java
index 81df597..0805485 100644
--- a/services/tests/servicestests/src/com/android/server/pm/BaseShortcutManagerTest.java
+++ b/services/tests/servicestests/src/com/android/server/pm/BaseShortcutManagerTest.java
@@ -157,7 +157,6 @@
return mMockDevicePolicyManager;
case Context.APP_SEARCH_SERVICE:
case Context.ROLE_SERVICE:
- case Context.APP_OPS_SERVICE:
// RoleManager is final and cannot be mocked, so we only override the inject
// accessor methods in ShortcutService.
return getTestContext().getSystemService(name);
diff --git a/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java b/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java
index a743fff..06be456 100644
--- a/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java
+++ b/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java
@@ -19,6 +19,7 @@
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
+import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.junit.Assume.assumeTrue;
import static org.testng.Assert.assertEquals;
@@ -33,6 +34,7 @@
import android.content.res.Resources;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
+import android.os.PersistableBundle;
import android.os.UserHandle;
import android.os.UserManager;
import android.platform.test.annotations.Postsubmit;
@@ -1632,6 +1634,106 @@
assertThat(mainUserCount).isEqualTo(1);
}
+ @Test
+ public void testAddUserAccountData_validStringValuesAreSaved_validBundleIsSaved() {
+ assumeManagedUsersSupported();
+
+ String userName = "User";
+ String accountName = "accountName";
+ String accountType = "accountType";
+ String arrayKey = "StringArrayKey";
+ String stringKey = "StringKey";
+ String intKey = "IntKey";
+ String nestedBundleKey = "PersistableBundleKey";
+ String value1 = "Value 1";
+ String value2 = "Value 2";
+ String value3 = "Value 3";
+
+ UserInfo userInfo = mUserManager.createUser(userName,
+ UserManager.USER_TYPE_FULL_SECONDARY, 0);
+
+ PersistableBundle accountOptions = new PersistableBundle();
+ String[] stringArray = {value1, value2};
+ accountOptions.putInt(intKey, 1234);
+ PersistableBundle nested = new PersistableBundle();
+ nested.putString(stringKey, value3);
+ accountOptions.putPersistableBundle(nestedBundleKey, nested);
+ accountOptions.putStringArray(arrayKey, stringArray);
+
+ mUserManager.clearSeedAccountData();
+ mUserManager.setSeedAccountData(mContext.getUserId(), accountName,
+ accountType, accountOptions);
+
+ //assert userName accountName and accountType were saved correctly
+ assertTrue(mUserManager.getUserInfo(userInfo.id).name.equals(userName));
+ assertTrue(mUserManager.getSeedAccountName().equals(accountName));
+ assertTrue(mUserManager.getSeedAccountType().equals(accountType));
+
+ //assert bundle with correct values was added
+ assertThat(mUserManager.getSeedAccountOptions().containsKey(arrayKey)).isTrue();
+ assertThat(mUserManager.getSeedAccountOptions().getPersistableBundle(nestedBundleKey)
+ .getString(stringKey)).isEqualTo(value3);
+ assertThat(mUserManager.getSeedAccountOptions().getStringArray(arrayKey)[0])
+ .isEqualTo(value1);
+
+ mUserManager.removeUser(userInfo.id);
+ }
+
+ @Test
+ public void testAddUserAccountData_invalidStringValuesAreTruncated_invalidBundleIsDropped() {
+ assumeManagedUsersSupported();
+
+ String tooLongString = generateLongString();
+ String userName = "User " + tooLongString;
+ String accountType = "Account Type " + tooLongString;
+ String accountName = "accountName " + tooLongString;
+ String arrayKey = "StringArrayKey";
+ String stringKey = "StringKey";
+ String intKey = "IntKey";
+ String nestedBundleKey = "PersistableBundleKey";
+ String value1 = "Value 1";
+ String value2 = "Value 2";
+
+ UserInfo userInfo = mUserManager.createUser(userName,
+ UserManager.USER_TYPE_FULL_SECONDARY, 0);
+
+ PersistableBundle accountOptions = new PersistableBundle();
+ String[] stringArray = {value1, value2};
+ accountOptions.putInt(intKey, 1234);
+ PersistableBundle nested = new PersistableBundle();
+ nested.putString(stringKey, tooLongString);
+ accountOptions.putPersistableBundle(nestedBundleKey, nested);
+ accountOptions.putStringArray(arrayKey, stringArray);
+ mUserManager.clearSeedAccountData();
+ mUserManager.setSeedAccountData(mContext.getUserId(), accountName,
+ accountType, accountOptions);
+
+ //assert userName was truncated
+ assertTrue(mUserManager.getUserInfo(userInfo.id).name.length()
+ == UserManager.MAX_USER_NAME_LENGTH);
+
+ //assert accountName and accountType got truncated
+ assertTrue(mUserManager.getSeedAccountName().length()
+ == UserManager.MAX_ACCOUNT_STRING_LENGTH);
+ assertTrue(mUserManager.getSeedAccountType().length()
+ == UserManager.MAX_ACCOUNT_STRING_LENGTH);
+
+ //assert bundle with invalid values was dropped
+ assertThat(mUserManager.getSeedAccountOptions() == null).isTrue();
+
+ mUserManager.removeUser(userInfo.id);
+ }
+
+ private String generateLongString() {
+ String partialString = "Test Name Test Name Test Name Test Name Test Name Test Name Test "
+ + "Name Test Name Test Name Test Name "; //String of length 100
+ StringBuilder resultString = new StringBuilder();
+ for (int i = 0; i < 600; i++) {
+ resultString.append(partialString);
+ }
+ return resultString.toString();
+ }
+
private boolean isPackageInstalledForUser(String packageName, int userId) {
try {
return mPackageManager.getPackageInfoAsUser(packageName, 0, userId) != null;
diff --git a/services/tests/servicestests/src/com/android/server/rollback/RollbackStoreTest.java b/services/tests/servicestests/src/com/android/server/rollback/RollbackStoreTest.java
index 9d56a36..5e11e17 100644
--- a/services/tests/servicestests/src/com/android/server/rollback/RollbackStoreTest.java
+++ b/services/tests/servicestests/src/com/android/server/rollback/RollbackStoreTest.java
@@ -18,6 +18,7 @@
import static com.google.common.truth.Truth.assertThat;
+import android.content.pm.PackageManager;
import android.content.pm.VersionedPackage;
import android.content.rollback.PackageRollbackInfo;
import android.util.SparseIntArray;
@@ -81,7 +82,8 @@
+ "'installedUsers':[55,79],"
+ "'ceSnapshotInodes':[]}],'isStaged':false,'causePackages':[{'packageName':'hello',"
+ "'longVersionCode':23},{'packageName':'something','longVersionCode':999}],"
- + "'committedSessionId':45654465},'timestamp':'2019-10-01T12:29:08.855Z',"
+ + "'committedSessionId':45654465, 'rollbackImpactLevel':1},"
+ + "'timestamp':'2019-10-01T12:29:08.855Z',"
+ "'originalSessionId':567,'state':'enabling','apkSessionId':-1,"
+ "'restoreUserDataInProgress':true, 'userId':0,"
+ "'installerPackageName':'some.installer'}";
@@ -138,6 +140,8 @@
assertThat(rollback.getOriginalSessionId()).isEqualTo(567);
assertThat(rollback.info.getRollbackId()).isEqualTo(ID);
assertThat(rollback.info.getPackages()).isEmpty();
+ assertThat(rollback.info.getRollbackImpactLevel()).isEqualTo(
+ PackageManager.ROLLBACK_USER_IMPACT_LOW);
assertThat(rollback.isEnabling()).isTrue();
assertThat(rollback.getExtensionVersions().toString())
.isEqualTo(extensionVersions.toString());
@@ -158,6 +162,8 @@
assertThat(rollback.info.getRollbackId()).isEqualTo(ID);
assertThat(rollback.info.getPackages()).isEmpty();
+ assertThat(rollback.info.getRollbackImpactLevel()).isEqualTo(
+ PackageManager.ROLLBACK_USER_IMPACT_LOW);
assertThat(rollback.isEnabling()).isTrue();
assertThat(rollback.getExtensionVersions().toString())
.isEqualTo(extensionVersions.toString());
@@ -175,6 +181,7 @@
origRb.info.getCausePackages().add(new VersionedPackage("com.made.up", 2));
origRb.info.getCausePackages().add(new VersionedPackage("com.pack.age", 99));
origRb.info.setCommittedSessionId(123456);
+ origRb.info.setRollbackImpactLevel(PackageManager.ROLLBACK_USER_IMPACT_HIGH);
PackageRollbackInfo pkgInfo1 =
new PackageRollbackInfo(new VersionedPackage("com.made.up", 18),
@@ -226,6 +233,7 @@
expectedRb.info.getCausePackages().add(new VersionedPackage("hello", 23));
expectedRb.info.getCausePackages().add(new VersionedPackage("something", 999));
expectedRb.info.setCommittedSessionId(45654465);
+ expectedRb.info.setRollbackImpactLevel(PackageManager.ROLLBACK_USER_IMPACT_HIGH);
PackageRollbackInfo pkgInfo1 = new PackageRollbackInfo(new VersionedPackage("blah", 55),
new VersionedPackage("blah1", 50), new ArrayList<>(), new ArrayList<>(),
diff --git a/services/tests/servicestests/src/com/android/server/systemconfig/SystemConfigNamedActorTest.kt b/services/tests/servicestests/src/com/android/server/systemconfig/SystemConfigNamedActorTest.kt
index 150822b..c07c4d7 100644
--- a/services/tests/servicestests/src/com/android/server/systemconfig/SystemConfigNamedActorTest.kt
+++ b/services/tests/servicestests/src/com/android/server/systemconfig/SystemConfigNamedActorTest.kt
@@ -18,12 +18,13 @@
import android.content.Context
import android.util.Xml
-import androidx.test.InstrumentationRegistry
+import androidx.test.platform.app.InstrumentationRegistry
import com.android.server.SystemConfig
import com.google.common.truth.Truth.assertThat
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertThrows
import org.junit.Rule
import org.junit.Test
-import org.junit.rules.ExpectedException
import org.junit.rules.TemporaryFolder
class SystemConfigNamedActorTest {
@@ -37,14 +38,11 @@
private const val PACKAGE_TWO = "com.test.actor.two"
}
- private val context: Context = InstrumentationRegistry.getContext()
+ private val context: Context = InstrumentationRegistry.getInstrumentation().context
@get:Rule
val tempFolder = TemporaryFolder(context.filesDir)
- @get:Rule
- val expected = ExpectedException.none()
-
private var uniqueCounter = 0
@Test
@@ -193,11 +191,9 @@
</config>
""".write()
- expected.expect(IllegalStateException::class.java)
- expected.expectMessage("Defining $ACTOR_ONE as $PACKAGE_ONE " +
+ val exc = assertThrows(IllegalStateException::class.java) { assertPermissions() }
+ assertEquals(exc.message, "Defining $ACTOR_ONE as $PACKAGE_ONE " +
"for the android namespace is not allowed")
-
- assertPermissions()
}
@Test
@@ -217,11 +213,9 @@
</config>
""".write()
- expected.expect(IllegalStateException::class.java)
- expected.expectMessage("Duplicate actor definition for $NAMESPACE_TEST/$ACTOR_ONE;" +
+ val exc = assertThrows(IllegalStateException::class.java) { assertPermissions() }
+ assertEquals(exc.message, "Duplicate actor definition for $NAMESPACE_TEST/$ACTOR_ONE;" +
" defined as both $PACKAGE_ONE and $PACKAGE_TWO")
-
- assertPermissions()
}
private fun String.write() = tempFolder.root.resolve("${uniqueCounter++}.xml")
@@ -230,5 +224,5 @@
private fun assertPermissions() = SystemConfig(false).apply {
val parser = Xml.newPullParser()
readPermissions(parser, tempFolder.root, 0)
- }. let { assertThat(it.namedActors) }
+ }.let { assertThat(it.namedActors) }
}
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationAttentionHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationAttentionHelperTest.java
index bfd2df2d..e75afcc 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationAttentionHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationAttentionHelperTest.java
@@ -27,12 +27,13 @@
import static android.media.AudioAttributes.USAGE_NOTIFICATION;
import static android.media.AudioAttributes.USAGE_NOTIFICATION_RINGTONE;
-import static junit.framework.Assert.assertFalse;
-import static junit.framework.Assert.assertNull;
-import static junit.framework.Assert.assertTrue;
+import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.anyFloat;
import static org.mockito.ArgumentMatchers.any;
@@ -43,6 +44,7 @@
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.after;
+import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
@@ -59,7 +61,10 @@
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
+import android.content.BroadcastReceiver;
import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.content.pm.UserInfo;
import android.graphics.Color;
@@ -80,6 +85,7 @@
import android.service.notification.NotificationListenerService;
import android.service.notification.StatusBarNotification;
import android.test.suitebuilder.annotation.SmallTest;
+import android.util.Pair;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityManager;
import android.view.accessibility.IAccessibilityManager;
@@ -100,6 +106,7 @@
import java.util.List;
import java.util.Objects;
+import java.util.Set;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
@@ -132,6 +139,8 @@
KeyguardManager mKeyguardManager;
@Mock
private UserManager mUserManager;
+ @Mock
+ private PackageManager mPackageManager;
NotificationRecordLoggerFake mNotificationRecordLogger = new NotificationRecordLoggerFake();
private InstanceIdSequence mNotificationInstanceIdSequence = new InstanceIdSequenceFake(
1 << 30);
@@ -171,11 +180,14 @@
private static final int CUSTOM_LIGHT_OFF = 10000;
private static final int MAX_VIBRATION_DELAY = 1000;
private static final float DEFAULT_VOLUME = 1.0f;
+ private BroadcastReceiver mAvalancheBroadcastReceiver;
@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
getContext().addMockSystemService(Vibrator.class, mVibrator);
+ getContext().addMockSystemService(PackageManager.class, mPackageManager);
+ when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_TELEPHONY)).thenReturn(false);
when(mAudioManager.isAudioFocusExclusive()).thenReturn(false);
when(mAudioManager.getRingtonePlayer()).thenReturn(mRingtonePlayer);
@@ -214,8 +226,9 @@
private void initAttentionHelper(TestableFlagResolver flagResolver) {
mAttentionHelper = new NotificationAttentionHelper(getContext(), mock(LightsManager.class),
- mAccessibilityManager, getContext().getPackageManager(), mUserManager, mUsageStats,
- mService.mNotificationManagerPrivate, mock(ZenModeHelper.class), flagResolver);
+ mAccessibilityManager, mPackageManager, mUserManager, mUsageStats,
+ mService.mNotificationManagerPrivate, mock(ZenModeHelper.class), flagResolver);
+ mAttentionHelper.onSystemReady();
mAttentionHelper.setVibratorHelper(spy(new VibratorHelper(getContext())));
mAttentionHelper.setAudioManager(mAudioManager);
mAttentionHelper.setSystemReady(true);
@@ -226,6 +239,29 @@
mAttentionHelper.setScreenOn(false);
mAttentionHelper.setInCallStateOffHook(false);
mAttentionHelper.mNotificationPulseEnabled = true;
+
+ if (Flags.crossAppPoliteNotifications()) {
+ // Capture BroadcastReceiver for avalanche triggers
+ ArgumentCaptor<BroadcastReceiver> broadcastReceiverCaptor =
+ ArgumentCaptor.forClass(BroadcastReceiver.class);
+ ArgumentCaptor<IntentFilter> intentFilterCaptor =
+ ArgumentCaptor.forClass(IntentFilter.class);
+ verify(getContext(), atLeastOnce()).registerReceiverAsUser(
+ broadcastReceiverCaptor.capture(),
+ any(), intentFilterCaptor.capture(), any(), any());
+ List<BroadcastReceiver> broadcastReceivers = broadcastReceiverCaptor.getAllValues();
+ List<IntentFilter> intentFilters = intentFilterCaptor.getAllValues();
+
+ assertThat(broadcastReceivers.size()).isAtLeast(1);
+ assertThat(intentFilters.size()).isAtLeast(1);
+ for (int i = 0; i < intentFilters.size(); i++) {
+ final IntentFilter filter = intentFilters.get(i);
+ if (filter.hasAction(Intent.ACTION_AIRPLANE_MODE_CHANGED)) {
+ mAvalancheBroadcastReceiver = broadcastReceivers.get(i);
+ }
+ }
+ assertThat(mAvalancheBroadcastReceiver).isNotNull();
+ }
}
//
@@ -2040,7 +2076,7 @@
}
@Test
- public void testBeepVolume_politeNotif_GlobalStrategy() throws Exception {
+ public void testBeepVolume_politeNotif_AvalancheStrategy() throws Exception {
mSetFlagsRule.enableFlags(Flags.FLAG_POLITE_NOTIFICATIONS);
mSetFlagsRule.enableFlags(Flags.FLAG_CROSS_APP_POLITE_NOTIFICATIONS);
TestableFlagResolver flagResolver = new TestableFlagResolver();
@@ -2048,6 +2084,11 @@
flagResolver.setFlagOverride(NotificationFlags.NOTIF_VOLUME2, 0);
initAttentionHelper(flagResolver);
+ // Trigger avalanche trigger intent
+ final Intent intent = new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED);
+ intent.putExtra("state", false);
+ mAvalancheBroadcastReceiver.onReceive(getContext(), intent);
+
NotificationRecord r = getBeepyNotification();
// set up internal state
@@ -2078,7 +2119,8 @@
}
@Test
- public void testBeepVolume_politeNotif_GlobalStrategy_ChannelHasUserSound() throws Exception {
+ public void testBeepVolume_politeNotif_AvalancheStrategy_ChannelHasUserSound()
+ throws Exception {
mSetFlagsRule.enableFlags(Flags.FLAG_POLITE_NOTIFICATIONS);
mSetFlagsRule.enableFlags(Flags.FLAG_CROSS_APP_POLITE_NOTIFICATIONS);
TestableFlagResolver flagResolver = new TestableFlagResolver();
@@ -2086,6 +2128,11 @@
flagResolver.setFlagOverride(NotificationFlags.NOTIF_VOLUME2, 0);
initAttentionHelper(flagResolver);
+ // Trigger avalanche trigger intent
+ final Intent intent = new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED);
+ intent.putExtra("state", false);
+ mAvalancheBroadcastReceiver.onReceive(getContext(), intent);
+
NotificationRecord r = getBeepyNotification();
// set up internal state
@@ -2364,6 +2411,82 @@
assertNotEquals(-1, r.getLastAudiblyAlertedMs());
}
+ @Test
+ public void testAvalancheStrategyTriggers() throws Exception {
+ mSetFlagsRule.enableFlags(Flags.FLAG_POLITE_NOTIFICATIONS);
+ mSetFlagsRule.enableFlags(Flags.FLAG_CROSS_APP_POLITE_NOTIFICATIONS);
+ TestableFlagResolver flagResolver = new TestableFlagResolver();
+ final int avalancheTimeoutMs = 100;
+ flagResolver.setFlagOverride(NotificationFlags.NOTIF_AVALANCHE_TIMEOUT, avalancheTimeoutMs);
+ initAttentionHelper(flagResolver);
+
+ // Trigger avalanche trigger intents
+ for (String intentAction
+ : NotificationAttentionHelper.NOTIFICATION_AVALANCHE_TRIGGER_INTENTS) {
+ // Set the action and extras to trigger the avalanche strategy
+ Intent intent = new Intent(intentAction);
+ Pair<String, Boolean> extras =
+ NotificationAttentionHelper.NOTIFICATION_AVALANCHE_TRIGGER_EXTRAS
+ .get(intentAction);
+ if (extras != null) {
+ intent.putExtra(extras.first, extras.second);
+ }
+ mAvalancheBroadcastReceiver.onReceive(getContext(), intent);
+ assertThat(mAttentionHelper.getPolitenessStrategy().isActive()).isTrue();
+
+ // Wait for avalanche timeout
+ Thread.sleep(avalancheTimeoutMs + 1);
+
+ // Check that avalanche strategy is inactive
+ assertThat(mAttentionHelper.getPolitenessStrategy().isActive()).isFalse();
+ }
+ }
+
+ @Test
+ public void testAvalancheStrategyTriggers_disabledExtras() throws Exception {
+ mSetFlagsRule.enableFlags(Flags.FLAG_POLITE_NOTIFICATIONS);
+ mSetFlagsRule.enableFlags(Flags.FLAG_CROSS_APP_POLITE_NOTIFICATIONS);
+ TestableFlagResolver flagResolver = new TestableFlagResolver();
+ initAttentionHelper(flagResolver);
+
+ for (String intentAction
+ : NotificationAttentionHelper.NOTIFICATION_AVALANCHE_TRIGGER_INTENTS) {
+ Intent intent = new Intent(intentAction);
+ Pair<String, Boolean> extras =
+ NotificationAttentionHelper.NOTIFICATION_AVALANCHE_TRIGGER_EXTRAS
+ .get(intentAction);
+ // Test only for intents with extras
+ if (extras != null) {
+ // Set the action extras to NOT trigger the avalanche strategy
+ intent.putExtra(extras.first, !extras.second);
+ mAvalancheBroadcastReceiver.onReceive(getContext(), intent);
+ // Check that avalanche strategy is inactive
+ assertThat(mAttentionHelper.getPolitenessStrategy().isActive()).isFalse();
+ }
+ }
+ }
+
+ @Test
+ public void testAvalancheStrategyTriggers_nonAvalancheIntents() throws Exception {
+ mSetFlagsRule.enableFlags(Flags.FLAG_POLITE_NOTIFICATIONS);
+ mSetFlagsRule.enableFlags(Flags.FLAG_CROSS_APP_POLITE_NOTIFICATIONS);
+ TestableFlagResolver flagResolver = new TestableFlagResolver();
+ initAttentionHelper(flagResolver);
+
+ // Broadcast intents that are not avalanche triggers
+ final Set<String> notAvalancheTriggerIntents = Set.of(
+ Intent.ACTION_USER_ADDED,
+ Intent.ACTION_SCREEN_ON,
+ Intent.ACTION_POWER_CONNECTED
+ );
+ for (String intentAction : notAvalancheTriggerIntents) {
+ Intent intent = new Intent(intentAction);
+ mAvalancheBroadcastReceiver.onReceive(getContext(), intent);
+ // Check that avalanche strategy is inactive
+ assertThat(mAttentionHelper.getPolitenessStrategy().isActive()).isFalse();
+ }
+ }
+
static class VibrateRepeatMatcher implements ArgumentMatcher<VibrationEffect> {
private final int mRepeatIndex;
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationVisitUrisTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationVisitUrisTest.java
index ea948ca..863cda4 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationVisitUrisTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationVisitUrisTest.java
@@ -118,12 +118,19 @@
// Constructors that should be used to create instances of specific classes. Overrides scoring.
private static final ImmutableMap<Class<?>, Constructor<?>> PREFERRED_CONSTRUCTORS;
+ // Setter methods that receive String parameters, but where those Strings represent Uris
+ // (and are visited/validated).
+ private static final ImmutableSet<Method> SETTERS_WITH_STRING_AS_URI;
+
static {
try {
PREFERRED_CONSTRUCTORS = ImmutableMap.of(
Notification.Builder.class,
Notification.Builder.class.getConstructor(Context.class, String.class));
+ SETTERS_WITH_STRING_AS_URI = ImmutableSet.of(
+ Person.Builder.class.getMethod("setUri", String.class));
+
EXCLUDED_SETTERS_OVERLOADS = ImmutableMultimap.<Class<?>, Method>builder()
.put(RemoteViews.class,
// b/245950570: Tries to connect to service and will crash.
@@ -257,7 +264,7 @@
@Nullable Class<?> styleClass, @Nullable Class<?> extenderClass,
@Nullable Class<?> actionExtenderClass, boolean includeRemoteViews) {
SpecialParameterGenerator specialGenerator = new SpecialParameterGenerator(context);
- Set<Class<?>> excludedClasses = includeRemoteViews
+ ImmutableSet<Class<?>> excludedClasses = includeRemoteViews
? ImmutableSet.of()
: ImmutableSet.of(RemoteViews.class);
Location location = Location.root(Notification.Builder.class);
@@ -294,7 +301,7 @@
}
private static Object generateObject(Class<?> clazz, Location where,
- Set<Class<?>> excludingClasses, SpecialParameterGenerator specialGenerator) {
+ ImmutableSet<Class<?>> excludingClasses, SpecialParameterGenerator specialGenerator) {
if (excludingClasses.contains(clazz)) {
throw new IllegalArgumentException(
String.format("Asked to generate a %s but it's part of the excluded set (%s)",
@@ -369,7 +376,7 @@
}
private static Object constructEmpty(Class<?> clazz, Location where,
- Set<Class<?>> excludingClasses, SpecialParameterGenerator specialGenerator) {
+ ImmutableSet<Class<?>> excludingClasses, SpecialParameterGenerator specialGenerator) {
Constructor<?> bestConstructor;
if (PREFERRED_CONSTRUCTORS.containsKey(clazz)) {
// Use the preferred constructor.
@@ -431,7 +438,7 @@
}
private static void invokeAllSetters(Object instance, Location where, boolean allOverloads,
- boolean includingVoidMethods, Set<Class<?>> excludingParameterTypes,
+ boolean includingVoidMethods, ImmutableSet<Class<?>> excludingParameterTypes,
SpecialParameterGenerator specialGenerator) {
for (Method setter : ReflectionUtils.getAllSetters(instance.getClass(), where,
allOverloads, includingVoidMethods, excludingParameterTypes)) {
@@ -462,24 +469,34 @@
}
private static Object[] generateParameters(Executable executable, Location where,
- Set<Class<?>> excludingClasses, SpecialParameterGenerator specialGenerator) {
+ ImmutableSet<Class<?>> excludingClasses, SpecialParameterGenerator specialGenerator) {
Log.i(TAG, "About to generate parameters for " + ReflectionUtils.methodToString(executable)
+ " in " + where);
Type[] parameterTypes = executable.getGenericParameterTypes();
Object[] parameterValues = new Object[parameterTypes.length];
for (int i = 0; i < parameterTypes.length; i++) {
- parameterValues[i] = generateParameter(
- parameterTypes[i],
+ boolean generateUriAsString = false;
+ Type parameterType = parameterTypes[i];
+ if (SETTERS_WITH_STRING_AS_URI.contains(executable)
+ && parameterType.equals(String.class)) {
+ generateUriAsString = true;
+ }
+ Object value = generateParameter(
+ generateUriAsString ? Uri.class : parameterType,
where.plus(executable,
String.format("[%d,%s]", i, parameterTypes[i].getTypeName())),
excludingClasses,
specialGenerator);
+ if (generateUriAsString) {
+ value = ((Uri) value).toString();
+ }
+ parameterValues[i] = value;
}
return parameterValues;
}
private static Object generateParameter(Type parameterType, Location where,
- Set<Class<?>> excludingClasses, SpecialParameterGenerator specialGenerator) {
+ ImmutableSet<Class<?>> excludingClasses, SpecialParameterGenerator specialGenerator) {
if (parameterType instanceof Class<?> parameterClass) {
return generateObject(
parameterClass,
@@ -487,7 +504,8 @@
excludingClasses,
specialGenerator);
} else if (parameterType instanceof ParameterizedType parameterizedType) {
- if (parameterizedType.getRawType().equals(List.class)
+ if ((parameterizedType.getRawType().equals(List.class)
+ || parameterizedType.getRawType().equals(ArrayList.class))
&& parameterizedType.getActualTypeArguments()[0] instanceof Class<?>) {
ArrayList listValue = new ArrayList();
for (int i = 0; i < NUM_ELEMENTS_IN_ARRAY; i++) {
@@ -503,12 +521,14 @@
}
private static class ReflectionUtils {
- static Set<Class<?>> getConcreteSubclasses(Class<?> clazz, Class<?> containerClass) {
- return Arrays.stream(containerClass.getDeclaredClasses())
- .filter(
- innerClass -> clazz.isAssignableFrom(innerClass)
- && !Modifier.isAbstract(innerClass.getModifiers()))
- .collect(Collectors.toSet());
+ static ImmutableSet<Class<?>> getConcreteSubclasses(Class<?> clazz,
+ Class<?> containerClass) {
+ return ImmutableSet.copyOf(
+ Arrays.stream(containerClass.getDeclaredClasses())
+ .filter(
+ innerClass -> clazz.isAssignableFrom(innerClass)
+ && !Modifier.isAbstract(innerClass.getModifiers()))
+ .collect(Collectors.toSet()));
}
static String methodToString(Executable executable) {
@@ -611,9 +631,16 @@
}
private static class SpecialParameterGenerator {
+
+ private static final ImmutableSet<Class<?>> INTERESTING_CLASSES_WITH_SPECIAL_GENERATION =
+ ImmutableSet.of(Uri.class, Icon.class, Intent.class, PendingIntent.class,
+ RemoteViews.class);
+
private static final ImmutableSet<Class<?>> INTERESTING_CLASSES =
- ImmutableSet.of(Person.class, Uri.class, Icon.class, Intent.class,
- PendingIntent.class, RemoteViews.class);
+ new ImmutableSet.Builder<Class<?>>()
+ .addAll(INTERESTING_CLASSES_WITH_SPECIAL_GENERATION)
+ .add(Person.class) // Constructed via reflection, but high-score.
+ .build();
private static final ImmutableSet<Class<?>> MOCKED_CLASSES = ImmutableSet.of();
private static final ImmutableMap<Class<?>, Object> PRIMITIVE_VALUES =
@@ -637,7 +664,7 @@
}
static boolean canGenerate(Class<?> clazz) {
- return INTERESTING_CLASSES.contains(clazz)
+ return INTERESTING_CLASSES_WITH_SPECIAL_GENERATION.contains(clazz)
|| MOCKED_CLASSES.contains(clazz)
|| clazz.equals(Context.class)
|| clazz.equals(Bundle.class)
@@ -672,17 +699,6 @@
return Icon.createWithContentUri(iconUri);
}
- if (clazz == Person.class) {
- // TODO(b/310189261): Person.setUri takes a string instead of a URI. We should
- // find a way to use the SpecialParameterGenerator instead of this custom one.
- Uri personUri = generateUri(
- where.plus(Person.Builder.class).plus("setUri", String.class));
- Uri iconUri = generateUri(where.plus(Person.Builder.class).plus("setIcon",
- Icon.class).plus(Icon.class).plus("createWithContentUri", Uri.class));
- return new Person.Builder().setUri(personUri.toString()).setIcon(
- Icon.createWithContentUri(iconUri)).setName("John Doe").build();
- }
-
if (clazz == Intent.class) {
return new Intent("action", generateUri(where.plus(Intent.class)));
}
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenAdaptersTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenAdaptersTest.java
index 0e20daf..99d5a6d 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ZenAdaptersTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenAdaptersTest.java
@@ -143,12 +143,13 @@
Policy.policyState(false, true), 0);
ZenPolicy zenPolicy = notificationPolicyToZenPolicy(policy);
- assertThat(zenPolicy.getPriorityChannels()).isEqualTo(ZenPolicy.STATE_ALLOW);
+ assertThat(zenPolicy.getPriorityChannelsAllowed()).isEqualTo(ZenPolicy.STATE_ALLOW);
Policy notAllowed = new Policy(0, 0, 0, 0,
Policy.policyState(false, false), 0);
ZenPolicy zenPolicyNotAllowed = notificationPolicyToZenPolicy(notAllowed);
- assertThat(zenPolicyNotAllowed.getPriorityChannels()).isEqualTo(ZenPolicy.STATE_DISALLOW);
+ assertThat(zenPolicyNotAllowed.getPriorityChannelsAllowed()).isEqualTo(
+ ZenPolicy.STATE_DISALLOW);
}
@Test
@@ -158,11 +159,12 @@
Policy.policyState(false, true), 0);
ZenPolicy zenPolicy = notificationPolicyToZenPolicy(policy);
- assertThat(zenPolicy.getPriorityChannels()).isEqualTo(ZenPolicy.STATE_UNSET);
+ assertThat(zenPolicy.getPriorityChannelsAllowed()).isEqualTo(ZenPolicy.STATE_UNSET);
Policy notAllowed = new Policy(0, 0, 0, 0,
Policy.policyState(false, false), 0);
ZenPolicy zenPolicyNotAllowed = notificationPolicyToZenPolicy(notAllowed);
- assertThat(zenPolicyNotAllowed.getPriorityChannels()).isEqualTo(ZenPolicy.STATE_UNSET);
+ assertThat(zenPolicyNotAllowed.getPriorityChannelsAllowed()).isEqualTo(
+ ZenPolicy.STATE_UNSET);
}
}
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java
index e523e79f..539bb37 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java
@@ -284,7 +284,7 @@
actual.getPriorityConversationSenders());
assertEquals(expected.getPriorityCallSenders(), actual.getPriorityCallSenders());
assertEquals(expected.getPriorityMessageSenders(), actual.getPriorityMessageSenders());
- assertEquals(expected.getPriorityChannels(), actual.getPriorityChannels());
+ assertEquals(expected.getPriorityChannelsAllowed(), actual.getPriorityChannelsAllowed());
}
@Test
@@ -716,7 +716,7 @@
assertEquals(policy.getPriorityCategorySystem(), fromXml.getPriorityCategorySystem());
assertEquals(policy.getPriorityCategoryReminders(), fromXml.getPriorityCategoryReminders());
assertEquals(policy.getPriorityCategoryEvents(), fromXml.getPriorityCategoryEvents());
- assertEquals(policy.getPriorityChannels(), fromXml.getPriorityChannels());
+ assertEquals(policy.getPriorityChannelsAllowed(), fromXml.getPriorityChannelsAllowed());
assertEquals(policy.getVisualEffectFullScreenIntent(),
fromXml.getVisualEffectFullScreenIntent());
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
index 2486838..227265a 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
@@ -59,6 +59,7 @@
import static android.service.notification.ZenModeConfig.UPDATE_ORIGIN_UNKNOWN;
import static android.service.notification.ZenModeConfig.UPDATE_ORIGIN_USER;
import static android.service.notification.ZenPolicy.PEOPLE_TYPE_CONTACTS;
+import static android.service.notification.ZenPolicy.PEOPLE_TYPE_NONE;
import static android.service.notification.ZenPolicy.PEOPLE_TYPE_STARRED;
import static com.android.internal.config.sysui.SystemUiSystemPropertiesFlags.NotificationFlags.LOG_DND_STATE_EVENTS;
@@ -123,6 +124,7 @@
import android.os.Process;
import android.os.SimpleClock;
import android.os.UserHandle;
+import android.platform.test.annotations.DisableFlags;
import android.platform.test.annotations.EnableFlags;
import android.platform.test.flag.junit.SetFlagsRule;
import android.provider.Settings;
@@ -238,15 +240,19 @@
public TestWithLooperRule mLooperRule = new TestWithLooperRule();
ConditionProviders mConditionProviders;
- @Mock NotificationManager mNotificationManager;
- @Mock PackageManager mPackageManager;
+ @Mock
+ NotificationManager mNotificationManager;
+ @Mock
+ PackageManager mPackageManager;
private Resources mResources;
private TestableLooper mTestableLooper;
private final TestClock mTestClock = new TestClock();
private ZenModeHelper mZenModeHelper;
private ContentResolver mContentResolver;
- @Mock DeviceEffectsApplier mDeviceEffectsApplier;
- @Mock AppOpsManager mAppOps;
+ @Mock
+ DeviceEffectsApplier mDeviceEffectsApplier;
+ @Mock
+ AppOpsManager mAppOps;
TestableFlagResolver mTestFlagResolver = new TestableFlagResolver();
ZenModeEventLoggerFake mZenModeEventLogger;
@@ -290,7 +296,7 @@
when(mPackageManager.getPackageUidAsUser(eq(CUSTOM_PKG_NAME), anyInt()))
.thenReturn(CUSTOM_PKG_UID);
when(mPackageManager.getPackagesForUid(anyInt())).thenReturn(
- new String[] {pkg});
+ new String[]{pkg});
ApplicationInfo appInfoSpy = spy(new ApplicationInfo());
appInfoSpy.icon = ICON_RES_ID;
@@ -305,24 +311,26 @@
}
private XmlResourceParser getDefaultConfigParser() throws IOException, XmlPullParserException {
- String xml = "<zen version=\"8\" user=\"0\">\n"
- + "<allow calls=\"false\" repeatCallers=\"false\" messages=\"true\" "
- + "reminders=\"false\" events=\"false\" callsFrom=\"1\" messagesFrom=\"2\" "
- + "visualScreenOff=\"true\" alarms=\"true\" "
- + "media=\"true\" system=\"false\" conversations=\"true\""
- + " conversationsFrom=\"2\"/>\n"
- + "<automatic ruleId=\"" + EVENTS_DEFAULT_RULE_ID
- + "\" enabled=\"false\" snoozing=\"false\""
- + " name=\"Event\" zen=\"1\""
- + " component=\"android/com.android.server.notification.EventConditionProvider\""
- + " conditionId=\"condition://android/event?userId=-10000&calendar=&"
+ String xml = "<zen version=\"10\">\n"
+ + "<allow alarms=\"true\" media=\"true\" system=\"false\" calls=\"true\" "
+ + "callsFrom=\"2\" messages=\"true\"\n"
+ + "messagesFrom=\"2\" reminders=\"false\" events=\"false\" "
+ + "repeatCallers=\"true\" convos=\"true\"\n"
+ + "convosFrom=\"2\"/>\n"
+ + "<automatic ruleId=" + EVENTS_DEFAULT_RULE_ID
+ + " enabled=\"false\" snoozing=\"false\""
+ + " name=\"Event\" zen=\"1\"\n"
+ + " component=\"android/com.android.server.notification.EventConditionProvider\"\n"
+ + " conditionId=\"condition://android/event?userId=-10000&calendar=&"
+ "reply=1\"/>\n"
- + "<automatic ruleId=\"" + SCHEDULE_DEFAULT_RULE_ID + "\" enabled=\"false\""
- + " snoozing=\"false\" name=\"Sleeping\" zen=\"1\""
- + " component=\"android/com.android.server.notification.ScheduleConditionProvider\""
- + " conditionId=\"condition://android/schedule?days=1.2.3.4.5.6.7 &start=22.0"
- + "&end=7.0&exitAtAlarm=true\"/>"
- + "<disallow visualEffects=\"511\" />"
+ + "<automatic ruleId=" + SCHEDULE_DEFAULT_RULE_ID + " enabled=\"false\""
+ + " snoozing=\"false\" name=\"Sleeping\"\n zen=\"1\""
+ + " component=\"android/com.android.server.notification"
+ + ".ScheduleConditionProvider\"\n"
+ + " conditionId=\"condition://android/schedule?days=1.2.3.4.5.6.7&start=22.0"
+ + "&end=7.0&exitAtAlarm=true\"/>\n"
+ + "<disallow visualEffects=\"157\" />\n"
+ + "<state areChannelsBypassingDnd=\"false\" />\n"
+ "</zen>";
TypedXmlPullParser parser = Xml.newFastPullParser();
parser.setInput(new BufferedInputStream(new ByteArrayInputStream(xml.getBytes())), null);
@@ -408,7 +416,7 @@
@Test
public void testZenOff_NoMuteApplied() {
mZenModeHelper.mZenMode = ZEN_MODE_OFF;
- mZenModeHelper.setPriorityOnlyDndExemptPackages(new String[] {PKG_O});
+ mZenModeHelper.setPriorityOnlyDndExemptPackages(new String[]{PKG_O});
mZenModeHelper.mConsolidatedPolicy = new Policy(Policy.PRIORITY_CATEGORY_ALARMS
| PRIORITY_CATEGORY_MEDIA, 0, 0, 0, 0, 0);
mZenModeHelper.applyRestrictions();
@@ -421,7 +429,7 @@
@Test
public void testZenOn_NotificationApplied() {
mZenModeHelper.mZenMode = ZEN_MODE_IMPORTANT_INTERRUPTIONS;
- mZenModeHelper.setPriorityOnlyDndExemptPackages(new String[] {PKG_O});
+ mZenModeHelper.setPriorityOnlyDndExemptPackages(new String[]{PKG_O});
// The most permissive policy
mZenModeHelper.mConsolidatedPolicy = new Policy(Policy.PRIORITY_CATEGORY_ALARMS
| PRIORITY_CATEGORY_MEDIA | PRIORITY_CATEGORY_MESSAGES
@@ -442,7 +450,7 @@
@Test
public void testZenOn_StarredCallers_CallTypesBlocked() {
mZenModeHelper.mZenMode = ZEN_MODE_IMPORTANT_INTERRUPTIONS;
- mZenModeHelper.setPriorityOnlyDndExemptPackages(new String[] {PKG_O});
+ mZenModeHelper.setPriorityOnlyDndExemptPackages(new String[]{PKG_O});
// The most permissive policy
mZenModeHelper.mConsolidatedPolicy = new Policy(Policy.PRIORITY_CATEGORY_ALARMS
| PRIORITY_CATEGORY_MEDIA | PRIORITY_CATEGORY_MESSAGES
@@ -462,7 +470,7 @@
@Test
public void testZenOn_AllCallers_CallTypesAllowed() {
mZenModeHelper.mZenMode = ZEN_MODE_IMPORTANT_INTERRUPTIONS;
- mZenModeHelper.setPriorityOnlyDndExemptPackages(new String[] {PKG_O});
+ mZenModeHelper.setPriorityOnlyDndExemptPackages(new String[]{PKG_O});
// The most permissive policy
mZenModeHelper.mConsolidatedPolicy = new Policy(Policy.PRIORITY_CATEGORY_ALARMS
| PRIORITY_CATEGORY_MEDIA | PRIORITY_CATEGORY_MESSAGES
@@ -481,7 +489,7 @@
@Test
public void testZenOn_AllowAlarmsMedia_NoAlarmMediaMuteApplied() {
mZenModeHelper.mZenMode = ZEN_MODE_IMPORTANT_INTERRUPTIONS;
- mZenModeHelper.setPriorityOnlyDndExemptPackages(new String[] {PKG_O});
+ mZenModeHelper.setPriorityOnlyDndExemptPackages(new String[]{PKG_O});
mZenModeHelper.mConsolidatedPolicy = new Policy(Policy.PRIORITY_CATEGORY_ALARMS
| PRIORITY_CATEGORY_MEDIA, 0, 0, 0, 0, 0);
@@ -493,7 +501,7 @@
@Test
public void testZenOn_DisallowAlarmsMedia_AlarmMediaMuteApplied() {
mZenModeHelper.mZenMode = ZEN_MODE_IMPORTANT_INTERRUPTIONS;
- mZenModeHelper.setPriorityOnlyDndExemptPackages(new String[] {PKG_O});
+ mZenModeHelper.setPriorityOnlyDndExemptPackages(new String[]{PKG_O});
mZenModeHelper.mConsolidatedPolicy = new Policy(0, 0, 0, 0, 0, 0);
mZenModeHelper.applyRestrictions();
verifyApplyRestrictions(true, true, AudioAttributes.USAGE_ALARM);
@@ -506,7 +514,7 @@
@Test
public void testTotalSilence() {
mZenModeHelper.mZenMode = Settings.Global.ZEN_MODE_NO_INTERRUPTIONS;
- mZenModeHelper.setPriorityOnlyDndExemptPackages(new String[] {PKG_O});
+ mZenModeHelper.setPriorityOnlyDndExemptPackages(new String[]{PKG_O});
mZenModeHelper.mConsolidatedPolicy = new Policy(Policy.PRIORITY_CATEGORY_ALARMS
| PRIORITY_CATEGORY_MEDIA, 0, 0, 0, 0, 0);
mZenModeHelper.applyRestrictions();
@@ -525,7 +533,7 @@
@Test
public void testAlarmsOnly_alarmMediaMuteNotApplied() {
mZenModeHelper.mZenMode = Settings.Global.ZEN_MODE_ALARMS;
- mZenModeHelper.setPriorityOnlyDndExemptPackages(new String[] {PKG_O});
+ mZenModeHelper.setPriorityOnlyDndExemptPackages(new String[]{PKG_O});
mZenModeHelper.mConsolidatedPolicy = new Policy(0, 0, 0, 0, 0, 0);
mZenModeHelper.applyRestrictions();
@@ -545,7 +553,7 @@
@Test
public void testAlarmsOnly_callsMuteApplied() {
mZenModeHelper.mZenMode = Settings.Global.ZEN_MODE_ALARMS;
- mZenModeHelper.setPriorityOnlyDndExemptPackages(new String[] {PKG_O});
+ mZenModeHelper.setPriorityOnlyDndExemptPackages(new String[]{PKG_O});
mZenModeHelper.mConsolidatedPolicy = new Policy(0, 0, 0, 0, 0, 0);
mZenModeHelper.applyRestrictions();
@@ -559,7 +567,7 @@
public void testAlarmsOnly_allZenConfigToggledCannotBypass_alarmMuteNotApplied() {
// Only audio attributes with SUPPRESIBLE_NEVER can bypass
mZenModeHelper.mZenMode = Settings.Global.ZEN_MODE_ALARMS;
- mZenModeHelper.setPriorityOnlyDndExemptPackages(new String[] {PKG_O});
+ mZenModeHelper.setPriorityOnlyDndExemptPackages(new String[]{PKG_O});
mZenModeHelper.mConsolidatedPolicy = new Policy(0, 0, 0, 0, 0, 0);
mZenModeHelper.applyRestrictions();
@@ -571,7 +579,7 @@
// Only audio attributes with SUPPRESIBLE_NEVER can bypass
// with special case USAGE_ASSISTANCE_SONIFICATION
mZenModeHelper.mZenMode = ZEN_MODE_IMPORTANT_INTERRUPTIONS;
- mZenModeHelper.setPriorityOnlyDndExemptPackages(new String[] {PKG_O});
+ mZenModeHelper.setPriorityOnlyDndExemptPackages(new String[]{PKG_O});
mZenModeHelper.mConsolidatedPolicy = new Policy(0, 0, 0, 0, 0, 0);
mZenModeHelper.applyRestrictions();
@@ -592,7 +600,7 @@
@Test
public void testApplyRestrictions_whitelist_priorityOnlyMode() {
- mZenModeHelper.setPriorityOnlyDndExemptPackages(new String[] {PKG_O});
+ mZenModeHelper.setPriorityOnlyDndExemptPackages(new String[]{PKG_O});
mZenModeHelper.mZenMode = ZEN_MODE_IMPORTANT_INTERRUPTIONS;
mZenModeHelper.mConsolidatedPolicy = new Policy(0, 0, 0, 0, 0, 0);
mZenModeHelper.applyRestrictions();
@@ -607,7 +615,7 @@
@Test
public void testApplyRestrictions_whitelist_alarmsOnlyMode() {
- mZenModeHelper.setPriorityOnlyDndExemptPackages(new String[] {PKG_O});
+ mZenModeHelper.setPriorityOnlyDndExemptPackages(new String[]{PKG_O});
mZenModeHelper.mZenMode = Global.ZEN_MODE_ALARMS;
mZenModeHelper.mConsolidatedPolicy = new Policy(0, 0, 0, 0, 0, 0);
mZenModeHelper.applyRestrictions();
@@ -622,7 +630,7 @@
@Test
public void testApplyRestrictions_whitelist_totalSilenceMode() {
- mZenModeHelper.setPriorityOnlyDndExemptPackages(new String[] {PKG_O});
+ mZenModeHelper.setPriorityOnlyDndExemptPackages(new String[]{PKG_O});
mZenModeHelper.mZenMode = Global.ZEN_MODE_NO_INTERRUPTIONS;
mZenModeHelper.mConsolidatedPolicy = new Policy(0, 0, 0, 0, 0, 0);
mZenModeHelper.applyRestrictions();
@@ -1007,7 +1015,7 @@
mZenModeHelper.readXml(parser, false, UserHandle.USER_ALL);
assertEquals("Config mismatch: current vs expected: "
- + new ZenModeDiff.ConfigDiff(mZenModeHelper.mConfig, expected), expected,
+ + new ZenModeDiff.ConfigDiff(mZenModeHelper.mConfig, expected), expected,
mZenModeHelper.mConfig);
}
@@ -1336,7 +1344,7 @@
mZenModeHelper.readXml(parser, true, UserHandle.USER_SYSTEM);
assertEquals("Config mismatch: current vs original: "
- + new ZenModeDiff.ConfigDiff(mZenModeHelper.mConfig, original),
+ + new ZenModeDiff.ConfigDiff(mZenModeHelper.mConfig, original),
original, mZenModeHelper.mConfig);
assertEquals(original.hashCode(), mZenModeHelper.mConfig.hashCode());
}
@@ -1778,6 +1786,225 @@
}
@Test
+ @EnableFlags(Flags.FLAG_MODES_API)
+ public void testReadXml_onModesApi_noUpgrade() throws Exception {
+ // When reading XML for something that is already on the modes API system, make sure no
+ // rules' policies get changed.
+ setupZenConfig();
+
+ // Shared for rules
+ ArrayMap<String, ZenModeConfig.ZenRule> enabledAutoRules = new ArrayMap<>();
+ final ScheduleInfo weeknights = new ScheduleInfo();
+
+ // Custom rule with a custom policy
+ ZenModeConfig.ZenRule customRule = new ZenModeConfig.ZenRule();
+ customRule.enabled = true;
+ customRule.name = "Custom Rule";
+ customRule.zenMode = ZEN_MODE_IMPORTANT_INTERRUPTIONS;
+ customRule.conditionId = ZenModeConfig.toScheduleConditionId(weeknights);
+ customRule.component = new ComponentName("android", "ScheduleConditionProvider");
+ ZenPolicy policy = new ZenPolicy.Builder()
+ .allowCalls(PEOPLE_TYPE_CONTACTS)
+ .allowAlarms(true)
+ .allowRepeatCallers(false)
+ .build();
+ // Fill in policy fields, since on modes api we do not expect any rules to have unset fields
+ customRule.zenPolicy = mZenModeHelper.getDefaultZenPolicy().overwrittenWith(policy);
+ enabledAutoRules.put("customRule", customRule);
+ mZenModeHelper.mConfig.automaticRules = enabledAutoRules;
+
+ // set version to post-modes-API = 11
+ ByteArrayOutputStream baos = writeXmlAndPurge(11);
+ TypedXmlPullParser parser = Xml.newFastPullParser();
+ parser.setInput(new BufferedInputStream(
+ new ByteArrayInputStream(baos.toByteArray())), null);
+ parser.nextTag();
+ mZenModeHelper.readXml(parser, false, UserHandle.USER_ALL);
+
+ // basic check: global config maintained
+ setupZenConfigMaintained();
+
+ // Find our automatic rules.
+ ArrayMap<String, ZenModeConfig.ZenRule> rules = mZenModeHelper.mConfig.automaticRules;
+ assertThat(rules).hasSize(1);
+ assertThat(rules).containsKey("customRule");
+ ZenRule rule = rules.get("customRule");
+ assertThat(rule.zenPolicy).isEqualTo(customRule.zenPolicy);
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_MODES_API)
+ public void testReadXml_upgradeToModesApi_makesCustomPolicies() throws Exception {
+ // When reading in an XML file written from a pre-modes-API version, confirm that we create
+ // a custom policy matching the global config for any automatic rule with no specified
+ // policy.
+ setupZenConfig();
+
+ ArrayMap<String, ZenModeConfig.ZenRule> enabledAutoRule = new ArrayMap<>();
+ ZenModeConfig.ZenRule customRule = new ZenModeConfig.ZenRule();
+ final ScheduleInfo weeknights = new ScheduleInfo();
+ customRule.enabled = true;
+ customRule.name = "Custom Rule";
+ customRule.zenMode = ZEN_MODE_IMPORTANT_INTERRUPTIONS;
+ customRule.conditionId = ZenModeConfig.toScheduleConditionId(weeknights);
+ customRule.component = new ComponentName("android", "ScheduleConditionProvider");
+ enabledAutoRule.put("customRule", customRule); // no custom policy set
+ mZenModeHelper.mConfig.automaticRules = enabledAutoRule;
+
+ // set version to pre-modes-API = 10
+ ByteArrayOutputStream baos = writeXmlAndPurge(10);
+ TypedXmlPullParser parser = Xml.newFastPullParser();
+ parser.setInput(new BufferedInputStream(
+ new ByteArrayInputStream(baos.toByteArray())), null);
+ parser.nextTag();
+ mZenModeHelper.readXml(parser, false, UserHandle.USER_ALL);
+
+ // basic check: global config maintained
+ setupZenConfigMaintained();
+
+ // Find our automatic rule and check that it has a policy set now
+ ArrayMap<String, ZenModeConfig.ZenRule> rules = mZenModeHelper.mConfig.automaticRules;
+ assertThat(rules).hasSize(1);
+ assertThat(rules).containsKey("customRule");
+ ZenRule rule = rules.get("customRule");
+ assertThat(rule.zenPolicy).isNotNull();
+
+ // Check policy values as set up in setupZenConfig() to confirm they match
+ assertThat(rule.zenPolicy.getPriorityCategoryAlarms()).isEqualTo(STATE_DISALLOW);
+ assertThat(rule.zenPolicy.getPriorityCategoryMedia()).isEqualTo(STATE_DISALLOW);
+ assertThat(rule.zenPolicy.getPriorityCategorySystem()).isEqualTo(STATE_DISALLOW);
+ assertThat(rule.zenPolicy.getPriorityCategoryReminders()).isEqualTo(STATE_ALLOW);
+ assertThat(rule.zenPolicy.getPriorityCategoryCalls()).isEqualTo(STATE_ALLOW);
+ assertThat(rule.zenPolicy.getPriorityCallSenders()).isEqualTo(PEOPLE_TYPE_STARRED);
+ assertThat(rule.zenPolicy.getPriorityCategoryMessages()).isEqualTo(STATE_ALLOW);
+ assertThat(rule.zenPolicy.getPriorityCategoryConversations()).isEqualTo(STATE_ALLOW);
+ assertThat(rule.zenPolicy.getPriorityCategoryEvents()).isEqualTo(STATE_ALLOW);
+ assertThat(rule.zenPolicy.getPriorityCategoryRepeatCallers()).isEqualTo(STATE_ALLOW);
+ assertThat(rule.zenPolicy.getVisualEffectBadge()).isEqualTo(STATE_DISALLOW);
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_MODES_API)
+ public void testReadXml_upgradeToModesApi_fillsInCustomPolicies() throws Exception {
+ // When reading in an XML file written from a pre-modes-API version, confirm that for an
+ // underspecified ZenPolicy, we fill in all of the gaps with things from the global config
+ // in order to maintain consistency of behavior.
+ setupZenConfig();
+
+ ArrayMap<String, ZenModeConfig.ZenRule> enabledAutoRule = new ArrayMap<>();
+ ZenModeConfig.ZenRule customRule = new ZenModeConfig.ZenRule();
+ final ScheduleInfo weeknights = new ScheduleInfo();
+ customRule.enabled = true;
+ customRule.name = "Custom Rule";
+ customRule.zenMode = ZEN_MODE_IMPORTANT_INTERRUPTIONS;
+ customRule.conditionId = ZenModeConfig.toScheduleConditionId(weeknights);
+ customRule.component = new ComponentName("android", "ScheduleConditionProvider");
+ customRule.zenPolicy = new ZenPolicy.Builder()
+ .allowAlarms(true)
+ .allowMedia(true)
+ .allowRepeatCallers(false)
+ .build();
+ enabledAutoRule.put("customRule", customRule);
+ mZenModeHelper.mConfig.automaticRules = enabledAutoRule;
+
+ // set version to pre-modes-API = 10
+ ByteArrayOutputStream baos = writeXmlAndPurge(10);
+ TypedXmlPullParser parser = Xml.newFastPullParser();
+ parser.setInput(new BufferedInputStream(
+ new ByteArrayInputStream(baos.toByteArray())), null);
+ parser.nextTag();
+ mZenModeHelper.readXml(parser, false, UserHandle.USER_ALL);
+
+ // basic check: global config maintained
+ setupZenConfigMaintained();
+
+ // Find our automatic rule and check that it has a policy set now
+ ArrayMap<String, ZenModeConfig.ZenRule> rules = mZenModeHelper.mConfig.automaticRules;
+ assertThat(rules).hasSize(1);
+ assertThat(rules).containsKey("customRule");
+ ZenRule rule = rules.get("customRule");
+ assertThat(rule.zenPolicy).isNotNull();
+
+ // Check unset policy values match values in setupZenConfig().
+ // Check that set policy values match the values set in the policy.
+ assertThat(rule.zenPolicy.getPriorityCategoryAlarms()).isEqualTo(STATE_ALLOW);
+ assertThat(rule.zenPolicy.getPriorityCategoryMedia()).isEqualTo(STATE_ALLOW);
+ assertThat(rule.zenPolicy.getPriorityCategoryRepeatCallers()).isEqualTo(STATE_DISALLOW);
+
+ // Check that the rest is filled in from the default
+ assertThat(rule.zenPolicy.getPriorityCategorySystem()).isEqualTo(STATE_DISALLOW);
+ assertThat(rule.zenPolicy.getPriorityCategoryReminders()).isEqualTo(STATE_ALLOW);
+ assertThat(rule.zenPolicy.getPriorityCategoryCalls()).isEqualTo(STATE_ALLOW);
+ assertThat(rule.zenPolicy.getPriorityCallSenders()).isEqualTo(PEOPLE_TYPE_STARRED);
+ assertThat(rule.zenPolicy.getPriorityCategoryMessages()).isEqualTo(STATE_ALLOW);
+ assertThat(rule.zenPolicy.getPriorityCategoryConversations()).isEqualTo(STATE_ALLOW);
+ assertThat(rule.zenPolicy.getPriorityCategoryEvents()).isEqualTo(STATE_ALLOW);
+ assertThat(rule.zenPolicy.getVisualEffectBadge()).isEqualTo(STATE_DISALLOW);
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_MODES_API)
+ public void testReadXml_upgradeToModesApi_existingDefaultRulesGetCustomPolicy()
+ throws Exception {
+ setupZenConfig();
+
+ // Default rules, if they exist and have no policies, should get a snapshot of the global
+ // policy, even if they are disabled upon upgrade.
+ ArrayMap<String, ZenModeConfig.ZenRule> automaticRules = new ArrayMap<>();
+ ZenModeConfig.ZenRule defaultScheduleRule = new ZenModeConfig.ZenRule();
+ final ScheduleInfo defaultScheduleRuleInfo = new ScheduleInfo();
+ defaultScheduleRule.enabled = false;
+ defaultScheduleRule.name = "Default Schedule Rule";
+ defaultScheduleRule.zenMode = ZEN_MODE_IMPORTANT_INTERRUPTIONS;
+ defaultScheduleRule.conditionId = ZenModeConfig.toScheduleConditionId(
+ defaultScheduleRuleInfo);
+ defaultScheduleRule.id = ZenModeConfig.EVERY_NIGHT_DEFAULT_RULE_ID;
+ automaticRules.put(ZenModeConfig.EVERY_NIGHT_DEFAULT_RULE_ID, defaultScheduleRule);
+
+ ZenModeConfig.ZenRule defaultEventRule = new ZenModeConfig.ZenRule();
+ final ScheduleInfo defaultEventRuleInfo = new ScheduleInfo();
+ defaultEventRule.enabled = false;
+ defaultEventRule.name = "Default Event Rule";
+ defaultEventRule.zenMode = ZEN_MODE_IMPORTANT_INTERRUPTIONS;
+ defaultEventRule.conditionId = ZenModeConfig.toScheduleConditionId(
+ defaultEventRuleInfo);
+ defaultEventRule.id = ZenModeConfig.EVENTS_DEFAULT_RULE_ID;
+ automaticRules.put(ZenModeConfig.EVENTS_DEFAULT_RULE_ID, defaultEventRule);
+
+ mZenModeHelper.mConfig.automaticRules = automaticRules;
+
+ // set previous version
+ ByteArrayOutputStream baos = writeXmlAndPurge(10);
+ TypedXmlPullParser parser = Xml.newFastPullParser();
+ parser.setInput(new BufferedInputStream(
+ new ByteArrayInputStream(baos.toByteArray())), null);
+ parser.nextTag();
+ mZenModeHelper.readXml(parser, false, UserHandle.USER_ALL);
+
+ // check default rules
+ ArrayMap<String, ZenModeConfig.ZenRule> rules = mZenModeHelper.mConfig.automaticRules;
+ assertThat(rules.size()).isGreaterThan(0);
+ for (String defaultId : ZenModeConfig.DEFAULT_RULE_IDS) {
+ assertThat(rules).containsKey(defaultId);
+ ZenRule rule = rules.get(defaultId);
+ assertThat(rule.zenPolicy).isNotNull();
+
+ // Check policy values as set up in setupZenConfig() to confirm they match
+ assertThat(rule.zenPolicy.getPriorityCategoryAlarms()).isEqualTo(STATE_DISALLOW);
+ assertThat(rule.zenPolicy.getPriorityCategoryMedia()).isEqualTo(STATE_DISALLOW);
+ assertThat(rule.zenPolicy.getPriorityCategorySystem()).isEqualTo(STATE_DISALLOW);
+ assertThat(rule.zenPolicy.getPriorityCategoryReminders()).isEqualTo(STATE_ALLOW);
+ assertThat(rule.zenPolicy.getPriorityCategoryCalls()).isEqualTo(STATE_ALLOW);
+ assertThat(rule.zenPolicy.getPriorityCallSenders()).isEqualTo(PEOPLE_TYPE_STARRED);
+ assertThat(rule.zenPolicy.getPriorityCategoryMessages()).isEqualTo(STATE_ALLOW);
+ assertThat(rule.zenPolicy.getPriorityCategoryConversations()).isEqualTo(STATE_ALLOW);
+ assertThat(rule.zenPolicy.getPriorityCategoryEvents()).isEqualTo(STATE_ALLOW);
+ assertThat(rule.zenPolicy.getPriorityCategoryRepeatCallers()).isEqualTo(STATE_ALLOW);
+ assertThat(rule.zenPolicy.getVisualEffectBadge()).isEqualTo(STATE_DISALLOW);
+ }
+ }
+
+ @Test
public void testCountdownConditionSubscription() throws Exception {
ZenModeConfig config = new ZenModeConfig();
mZenModeHelper.mConfig = config;
@@ -2036,6 +2263,69 @@
}
@Test
+ @EnableFlags(Flags.FLAG_MODES_API)
+ public void testAddAutomaticZenRule_modesApi_fillsInDefaultValues() {
+ // When a new automatic zen rule is added with only some fields filled in, ensure that
+ // all unset fields are filled in with device defaults.
+
+ // Zen rule with null policy: should get entirely the default state
+ AutomaticZenRule zenRule1 = new AutomaticZenRule("name",
+ new ComponentName("android", "ScheduleConditionProvider"),
+ ZenModeConfig.toScheduleConditionId(new ScheduleInfo()),
+ NotificationManager.INTERRUPTION_FILTER_PRIORITY, true);
+ String id1 = mZenModeHelper.addAutomaticZenRule("android", zenRule1,
+ UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, "test", Process.SYSTEM_UID);
+
+ // Zen rule with partially-filled policy: should get all of the filled fields set, and the
+ // rest filled with default state
+ AutomaticZenRule zenRule2 = new AutomaticZenRule("name",
+ null,
+ new ComponentName(mContext.getPackageName(), "ScheduleConditionProvider"),
+ ZenModeConfig.toScheduleConditionId(new ScheduleInfo()),
+ new ZenPolicy.Builder()
+ .allowCalls(PEOPLE_TYPE_NONE)
+ .allowMessages(PEOPLE_TYPE_CONTACTS)
+ .showFullScreenIntent(true)
+ .build(),
+ NotificationManager.INTERRUPTION_FILTER_PRIORITY, true);
+ String id2 = mZenModeHelper.addAutomaticZenRule("android", zenRule2,
+ UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, "test", Process.SYSTEM_UID);
+
+ // rule 1 should exist
+ assertThat(id1).isNotNull();
+ ZenModeConfig.ZenRule rule1InConfig = mZenModeHelper.mConfig.automaticRules.get(id1);
+ assertThat(rule1InConfig).isNotNull();
+ assertThat(rule1InConfig.zenPolicy).isNotNull(); // we passed in null; it should now not be
+
+ // all of rule 1 should be the device default's policy
+ assertThat(rule1InConfig.zenPolicy).isEqualTo(mZenModeHelper.getDefaultZenPolicy());
+
+ // rule 2 should exist
+ assertThat(id2).isNotNull();
+ ZenModeConfig.ZenRule rule2InConfig = mZenModeHelper.mConfig.automaticRules.get(id2);
+ assertThat(rule2InConfig).isNotNull();
+
+ // rule 2: values set from the policy itself
+ assertThat(rule2InConfig.zenPolicy.getPriorityCallSenders()).isEqualTo(PEOPLE_TYPE_NONE);
+ assertThat(rule2InConfig.zenPolicy.getPriorityMessageSenders())
+ .isEqualTo(PEOPLE_TYPE_CONTACTS);
+ assertThat(rule2InConfig.zenPolicy.getVisualEffectFullScreenIntent())
+ .isEqualTo(ZenPolicy.STATE_ALLOW);
+
+ // the rest of rule 2's settings should be the device defaults
+ assertThat(rule2InConfig.zenPolicy.getPriorityConversationSenders())
+ .isEqualTo(CONVERSATION_SENDERS_IMPORTANT);
+ assertThat(rule2InConfig.zenPolicy.getPriorityCategorySystem())
+ .isEqualTo(ZenPolicy.STATE_DISALLOW);
+ assertThat(rule2InConfig.zenPolicy.getPriorityCategoryAlarms())
+ .isEqualTo(ZenPolicy.STATE_ALLOW);
+ assertThat(rule2InConfig.zenPolicy.getVisualEffectPeek())
+ .isEqualTo(ZenPolicy.STATE_DISALLOW);
+ assertThat(rule2InConfig.zenPolicy.getVisualEffectNotificationList())
+ .isEqualTo(ZenPolicy.STATE_ALLOW);
+ }
+
+ @Test
public void testSetAutomaticZenRuleState_nullPkg() {
AutomaticZenRule zenRule = new AutomaticZenRule("name",
null,
@@ -2357,6 +2647,68 @@
@Test
@EnableFlags(Flags.FLAG_MODES_API)
+ public void updateAutomaticZenRule_nullPolicy_doesNothing() {
+ // Test that when updateAutomaticZenRule is called with a null policy, nothing changes
+ // about the existing policy.
+ String ruleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(),
+ new AutomaticZenRule.Builder("Rule", CONDITION_ID)
+ .setOwner(OWNER)
+ .setZenPolicy(new ZenPolicy.Builder()
+ .allowCalls(ZenPolicy.PEOPLE_TYPE_NONE) // default is stars
+ .build())
+ .build(),
+ UPDATE_ORIGIN_APP, "reasons", 0);
+
+ mZenModeHelper.updateAutomaticZenRule(ruleId,
+ new AutomaticZenRule.Builder("Rule", CONDITION_ID)
+ // no zen policy
+ .build(),
+ UPDATE_ORIGIN_APP, "reasons", 0);
+
+ AutomaticZenRule savedRule = mZenModeHelper.getAutomaticZenRule(ruleId);
+ assertThat(savedRule.getZenPolicy().getPriorityCategoryCalls())
+ .isEqualTo(ZenPolicy.STATE_DISALLOW);
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_MODES_API)
+ public void updateAutomaticZenRule_overwritesExistingPolicy() {
+ // Test that when updating an automatic zen rule with an existing policy, the newly set
+ // fields overwrite those from the previous policy, but unset fields in the new policy
+ // keep values from the previous one.
+ String ruleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(),
+ new AutomaticZenRule.Builder("Rule", CONDITION_ID)
+ .setOwner(OWNER)
+ .setZenPolicy(new ZenPolicy.Builder()
+ .allowCalls(ZenPolicy.PEOPLE_TYPE_NONE) // default is stars
+ .allowAlarms(false)
+ .allowReminders(true)
+ .build())
+ .build(),
+ UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, "reasons", 0);
+
+ mZenModeHelper.updateAutomaticZenRule(ruleId,
+ new AutomaticZenRule.Builder("Rule", CONDITION_ID)
+ .setZenPolicy(new ZenPolicy.Builder()
+ .allowCalls(ZenPolicy.PEOPLE_TYPE_CONTACTS)
+ .build())
+ .build(),
+ UPDATE_ORIGIN_APP, "reasons", 0);
+
+ AutomaticZenRule savedRule = mZenModeHelper.getAutomaticZenRule(ruleId);
+ assertThat(savedRule.getZenPolicy().getPriorityCategoryCalls())
+ .isEqualTo(ZenPolicy.STATE_ALLOW); // from update
+ assertThat(savedRule.getZenPolicy().getPriorityCallSenders())
+ .isEqualTo(ZenPolicy.PEOPLE_TYPE_CONTACTS); // from update
+ assertThat(savedRule.getZenPolicy().getPriorityCategoryAlarms())
+ .isEqualTo(ZenPolicy.STATE_DISALLOW); // from original
+ assertThat(savedRule.getZenPolicy().getPriorityCategoryReminders())
+ .isEqualTo(ZenPolicy.STATE_ALLOW); // from original
+ }
+
+
+ @Test
+ @EnableFlags(Flags.FLAG_MODES_API)
public void addAutomaticZenRule_withTypeBedtime_replacesDisabledSleeping() {
ZenRule sleepingRule = createCustomAutomaticRule(ZEN_MODE_IMPORTANT_INTERRUPTIONS,
ZenModeConfig.EVERY_NIGHT_DEFAULT_RULE_ID);
@@ -2460,7 +2812,8 @@
DISABLED(false, /* originForUserActionInSystemUi= */ UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI);
private final boolean mEnabled;
- @ConfigChangeOrigin private final int mOriginForUserActionInSystemUi;
+ @ConfigChangeOrigin
+ private final int mOriginForUserActionInSystemUi;
ModesApiFlag(boolean enabled, @ConfigChangeOrigin int originForUserActionInSystemUi) {
this.mEnabled = enabled;
@@ -2506,7 +2859,7 @@
// - rules active = 1
// - user action = true (system-based turning zen mode on)
// - package uid = system (as set above)
- // - resulting DNDPolicyProto the same as the values in setupZenConfig()
+ // - resulting DNDPolicyProto the same as the values in setupZenConfig() (global policy)
assertEquals(ZenModeEventLogger.ZenStateChangedEvent.DND_TURNED_ON.getId(),
mZenModeEventLogger.getEventId(0));
assertEquals(ZEN_MODE_OFF, mZenModeEventLogger.getPrevZenMode(0));
@@ -2600,7 +2953,7 @@
// - 1 rule (newly) active
// - automatic (is not a user action)
// - package UID is written to be the rule *owner* even though it "comes from system"
- // - zen policy is the same as the set-up zen config
+ // - zen policy is the default as it's unspecified
assertEquals(ZenModeEventLogger.ZenStateChangedEvent.DND_TURNED_ON.getId(),
mZenModeEventLogger.getEventId(0));
assertEquals(ZEN_MODE_OFF, mZenModeEventLogger.getPrevZenMode(0));
@@ -2609,10 +2962,10 @@
assertEquals(1, mZenModeEventLogger.getNumRulesActive(0));
assertFalse(mZenModeEventLogger.getIsUserAction(0));
assertEquals(CUSTOM_PKG_UID, mZenModeEventLogger.getPackageUid(0));
- checkDndProtoMatchesSetupZenConfig(mZenModeEventLogger.getPolicyProto(0));
+ checkDndProtoMatchesDefaultZenConfig(mZenModeEventLogger.getPolicyProto(0));
// When the automatic rule is disabled, this should turn off zen mode and also count as a
- // user action.
+ // user action. We don't care what the consolidated policy is when DND turns off.
assertEquals(ZenModeEventLogger.ZenStateChangedEvent.DND_TURNED_OFF.getId(),
mZenModeEventLogger.getEventId(1));
assertEquals(ZEN_MODE_IMPORTANT_INTERRUPTIONS, mZenModeEventLogger.getPrevZenMode(1));
@@ -2835,28 +3188,28 @@
// First: turn on rule 1
mZenModeHelper.setAutomaticZenRuleState(id,
new Condition(zenRule.getConditionId(), "", STATE_TRUE),
- UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, Process.SYSTEM_UID);
+ UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, Process.SYSTEM_UID);
// Second: turn on rule 2
mZenModeHelper.setAutomaticZenRuleState(id2,
new Condition(zenRule2.getConditionId(), "", STATE_TRUE),
- UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, Process.SYSTEM_UID);
+ UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, Process.SYSTEM_UID);
// Third: turn on rule 3
mZenModeHelper.setAutomaticZenRuleState(id3,
new Condition(zenRule3.getConditionId(), "", STATE_TRUE),
- UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, Process.SYSTEM_UID);
+ UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, Process.SYSTEM_UID);
// Fourth: Turn *off* rule 2
mZenModeHelper.setAutomaticZenRuleState(id2,
new Condition(zenRule2.getConditionId(), "", STATE_FALSE),
- UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, Process.SYSTEM_UID);
+ UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, Process.SYSTEM_UID);
// This should result in a total of four events
assertEquals(4, mZenModeEventLogger.numLoggedChanges());
// Event 1: rule 1 turns on. We expect this to turn on DND (zen mode) overall, so that's
- // what the event should reflect. At this time, the policy is the same as initial setup.
+ // what the event should reflect. At this time, the policy is the default.
assertEquals(ZenModeEventLogger.ZenStateChangedEvent.DND_TURNED_ON.getId(),
mZenModeEventLogger.getEventId(0));
assertEquals(ZEN_MODE_OFF, mZenModeEventLogger.getPrevZenMode(0));
@@ -2864,7 +3217,7 @@
assertEquals(1, mZenModeEventLogger.getNumRulesActive(0));
assertFalse(mZenModeEventLogger.getIsUserAction(0));
assertEquals(CUSTOM_PKG_UID, mZenModeEventLogger.getPackageUid(0));
- checkDndProtoMatchesSetupZenConfig(mZenModeEventLogger.getPolicyProto(0));
+ checkDndProtoMatchesDefaultZenConfig(mZenModeEventLogger.getPolicyProto(0));
// Event 2: rule 2 turns on. This should not change anything about the policy, so the only
// change is that there are more rules active now.
@@ -2873,7 +3226,7 @@
assertEquals(2, mZenModeEventLogger.getNumRulesActive(1));
assertFalse(mZenModeEventLogger.getIsUserAction(1));
assertEquals(CUSTOM_PKG_UID, mZenModeEventLogger.getPackageUid(1));
- checkDndProtoMatchesSetupZenConfig(mZenModeEventLogger.getPolicyProto(1));
+ checkDndProtoMatchesDefaultZenConfig(mZenModeEventLogger.getPolicyProto(1));
// Event 3: rule 3 turns on. This should trigger a policy change, and be classified as such,
// but meanwhile also change the number of active rules.
@@ -2926,12 +3279,16 @@
mTestFlagResolver.setFlagOverride(LOG_DND_STATE_EVENTS, true);
setupZenConfig();
+ // Explicitly set up all rules with the same policy as the manual rule so there will be
+ // no policy changes in this test case.
+ ZenPolicy manualRulePolicy = mZenModeHelper.mConfig.toZenPolicy();
+
// Rule 1, owned by a package
AutomaticZenRule zenRule = new AutomaticZenRule("name",
null,
new ComponentName(CUSTOM_PKG_NAME, "ScheduleConditionProvider"),
ZenModeConfig.toScheduleConditionId(new ScheduleInfo()),
- null,
+ manualRulePolicy,
NotificationManager.INTERRUPTION_FILTER_PRIORITY, true);
String id = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), zenRule,
UPDATE_ORIGIN_APP, "test", Process.SYSTEM_UID);
@@ -2941,7 +3298,7 @@
null,
new ComponentName("android", "ScheduleConditionProvider"),
ZenModeConfig.toScheduleConditionId(new ScheduleInfo()),
- null,
+ manualRulePolicy,
NotificationManager.INTERRUPTION_FILTER_PRIORITY, true);
String id2 = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), zenRule2,
modesApiFlag.mOriginForUserActionInSystemUi, "test", Process.SYSTEM_UID);
@@ -3172,7 +3529,8 @@
}
@Test
- public void testUpdateConsolidatedPolicy_defaultRulesOnly() {
+ @DisableFlags(Flags.FLAG_MODES_API)
+ public void testUpdateConsolidatedPolicy_preModesApiDefaultRulesOnly_takesGlobalDefault() {
setupZenConfig();
// When there's one automatic rule active and it doesn't specify a policy, test that the
@@ -3205,12 +3563,39 @@
}
@Test
- public void testUpdateConsolidatedPolicy_customPolicyOnly() {
+ @EnableFlags(Flags.FLAG_MODES_API)
+ public void testUpdateConsolidatedPolicy_modesApiDefaultRulesOnly_takesDeviceDefault() {
+ setupZenConfig();
+
+ // When there's one automatic rule active and it doesn't specify a policy, test that the
+ // resulting consolidated policy is one that matches the default *device* settings.
+ AutomaticZenRule zenRule = new AutomaticZenRule("name",
+ null,
+ new ComponentName(CUSTOM_PKG_NAME, "ScheduleConditionProvider"),
+ ZenModeConfig.toScheduleConditionId(new ScheduleInfo()),
+ null, // null policy
+ NotificationManager.INTERRUPTION_FILTER_PRIORITY, true);
+ String id = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), zenRule,
+ UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, "test", Process.SYSTEM_UID);
+
+ // enable the rule
+ mZenModeHelper.setAutomaticZenRuleState(id,
+ new Condition(zenRule.getConditionId(), "", STATE_TRUE),
+ UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, Process.SYSTEM_UID);
+
+ // inspect the consolidated policy, which should match the device default settings.
+ assertThat(ZenAdapters.notificationPolicyToZenPolicy(mZenModeHelper.mConsolidatedPolicy))
+ .isEqualTo(mZenModeHelper.getDefaultZenPolicy());
+ }
+
+ @Test
+ @DisableFlags(Flags.FLAG_MODES_API)
+ public void testUpdateConsolidatedPolicy_preModesApiCustomPolicyOnly_fillInWithGlobal() {
setupZenConfig();
// when there's only one automatic rule active and it has a custom policy, make sure that's
- // what the consolidated policy reflects whether or not it's stricter than what the default
- // would specify.
+ // what the consolidated policy reflects whether or not it's stricter than what the global
+ // config would specify.
ZenPolicy customPolicy = new ZenPolicy.Builder()
.allowAlarms(true) // more lenient than default
.allowMedia(true) // more lenient than default
@@ -3249,7 +3634,51 @@
}
@Test
- public void testUpdateConsolidatedPolicy_defaultAndCustomActive() {
+ @EnableFlags(Flags.FLAG_MODES_API)
+ public void testUpdateConsolidatedPolicy_modesApiCustomPolicyOnly_fillInWithDeviceDefault() {
+ setupZenConfig();
+
+ // when there's only one automatic rule active and it has a custom policy, make sure that's
+ // what the consolidated policy reflects whether or not it's stricter than what the default
+ // would specify.
+ ZenPolicy customPolicy = new ZenPolicy.Builder()
+ .allowSystem(true) // more lenient than default
+ .allowRepeatCallers(false) // more restrictive than default
+ .allowCalls(ZenPolicy.PEOPLE_TYPE_NONE) // more restrictive than default
+ .showFullScreenIntent(true) // more lenient
+ .showBadges(false) // more restrictive
+ .build();
+
+ AutomaticZenRule zenRule = new AutomaticZenRule("name",
+ null,
+ new ComponentName(CUSTOM_PKG_NAME, "ScheduleConditionProvider"),
+ ZenModeConfig.toScheduleConditionId(new ScheduleInfo()),
+ customPolicy,
+ NotificationManager.INTERRUPTION_FILTER_PRIORITY, true);
+ String id = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), zenRule,
+ UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, "test", Process.SYSTEM_UID);
+
+ // enable the rule; this will update the consolidated policy
+ mZenModeHelper.setAutomaticZenRuleState(id,
+ new Condition(zenRule.getConditionId(), "", STATE_TRUE),
+ UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, Process.SYSTEM_UID);
+
+ // since this is the only active rule, the consolidated policy should match the custom
+ // policy for every field specified, and take default values for unspecified things
+ assertThat(mZenModeHelper.mConsolidatedPolicy.allowAlarms()).isTrue(); // default
+ assertThat(mZenModeHelper.mConsolidatedPolicy.allowMedia()).isTrue(); // default
+ assertThat(mZenModeHelper.mConsolidatedPolicy.allowSystem()).isTrue(); // custom
+ assertThat(mZenModeHelper.mConsolidatedPolicy.allowReminders()).isFalse(); // default
+ assertThat(mZenModeHelper.mConsolidatedPolicy.allowCalls()).isFalse(); // custom
+ assertThat(mZenModeHelper.mConsolidatedPolicy.allowMessages()).isTrue(); // default
+ assertThat(mZenModeHelper.mConsolidatedPolicy.allowRepeatCallers()).isFalse(); // custom
+ assertThat(mZenModeHelper.mConsolidatedPolicy.showBadges()).isFalse(); // custom
+ assertThat(mZenModeHelper.mConsolidatedPolicy.showFullScreenIntents()).isTrue(); // custom
+ }
+
+ @Test
+ @DisableFlags(Flags.FLAG_MODES_API)
+ public void testUpdateConsolidatedPolicy_preModesApiDefaultAndCustomActive_mergesWithGlobal() {
setupZenConfig();
// when there are two rules active, one inheriting the default policy and one setting its
@@ -3309,6 +3738,68 @@
}
@Test
+ @EnableFlags(Flags.FLAG_MODES_API)
+ public void testUpdateConsolidatedPolicy_modesApiDefaultAndCustomActive_mergesWithDefault() {
+ setupZenConfig();
+
+ // when there are two rules active, one inheriting the default policy and one setting its
+ // own custom policy, they should be merged to form the most restrictive combination.
+
+ // rule 1: no custom policy
+ AutomaticZenRule zenRule = new AutomaticZenRule("name",
+ null,
+ new ComponentName(CUSTOM_PKG_NAME, "ScheduleConditionProvider"),
+ ZenModeConfig.toScheduleConditionId(new ScheduleInfo()),
+ null,
+ NotificationManager.INTERRUPTION_FILTER_PRIORITY, true);
+ String id = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), zenRule,
+ UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, "test", Process.SYSTEM_UID);
+
+ // enable rule 1
+ mZenModeHelper.setAutomaticZenRuleState(id,
+ new Condition(zenRule.getConditionId(), "", STATE_TRUE),
+ UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, Process.SYSTEM_UID);
+
+ // custom policy for rule 2
+ ZenPolicy customPolicy = new ZenPolicy.Builder()
+ .allowAlarms(false) // more restrictive than default
+ .allowSystem(true) // more lenient than default
+ .allowRepeatCallers(false) // more restrictive than default
+ .allowCalls(ZenPolicy.PEOPLE_TYPE_NONE) // more restrictive than default
+ .showBadges(false) // more restrictive
+ .showPeeking(true) // more lenient
+ .build();
+
+ AutomaticZenRule zenRule2 = new AutomaticZenRule("name2",
+ null,
+ new ComponentName(CUSTOM_PKG_NAME, "ScheduleConditionProvider"),
+ ZenModeConfig.toScheduleConditionId(new ScheduleInfo()),
+ customPolicy,
+ NotificationManager.INTERRUPTION_FILTER_PRIORITY, true);
+ String id2 = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), zenRule2,
+ UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, "test", Process.SYSTEM_UID);
+
+ // enable rule 2; this will update the consolidated policy
+ mZenModeHelper.setAutomaticZenRuleState(id2,
+ new Condition(zenRule2.getConditionId(), "", STATE_TRUE),
+ UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, Process.SYSTEM_UID);
+
+ // now both rules should be on, and the consolidated policy should reflect the most
+ // restrictive option of each of the two
+ assertThat(mZenModeHelper.mConsolidatedPolicy.allowAlarms()).isFalse(); // custom stricter
+ assertThat(mZenModeHelper.mConsolidatedPolicy.allowMedia()).isTrue(); // default
+ assertThat(mZenModeHelper.mConsolidatedPolicy.allowSystem()).isFalse(); // default stricter
+ assertThat(mZenModeHelper.mConsolidatedPolicy.allowReminders()).isFalse(); // default
+ assertThat(mZenModeHelper.mConsolidatedPolicy.allowCalls()).isFalse(); // custom stricter
+ assertThat(mZenModeHelper.mConsolidatedPolicy.allowMessages()).isTrue(); // default
+ assertThat(mZenModeHelper.mConsolidatedPolicy.allowConversations()).isTrue(); // default
+ assertThat(mZenModeHelper.mConsolidatedPolicy.allowRepeatCallers())
+ .isFalse(); // custom stricter
+ assertThat(mZenModeHelper.mConsolidatedPolicy.showBadges()).isFalse(); // custom stricter
+ assertThat(mZenModeHelper.mConsolidatedPolicy.showPeeking()).isFalse(); // default stricter
+ }
+
+ @Test
public void testUpdateConsolidatedPolicy_allowChannels() {
mSetFlagsRule.enableFlags(Flags.FLAG_MODES_API);
setupZenConfig();
@@ -3372,7 +3863,10 @@
null,
new ComponentName(CUSTOM_PKG_NAME, "cls"),
Uri.parse("priority"),
- new ZenPolicy.Builder().allowMedia(true).build(),
+ new ZenPolicy.Builder()
+ .allowMedia(true)
+ .allowSystem(true)
+ .build(),
NotificationManager.INTERRUPTION_FILTER_PRIORITY, true);
String rule1Id = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(),
zenRuleWithPriority, UPDATE_ORIGIN_APP, "test", CUSTOM_PKG_UID);
@@ -3394,10 +3888,10 @@
UPDATE_ORIGIN_APP, CUSTOM_PKG_UID);
// Consolidated Policy should be default + rule1.
- assertThat(mZenModeHelper.mConsolidatedPolicy.allowAlarms()).isFalse(); // default
+ assertThat(mZenModeHelper.mConsolidatedPolicy.allowAlarms()).isTrue(); // default
assertThat(mZenModeHelper.mConsolidatedPolicy.allowMedia()).isTrue(); // priority rule
- assertThat(mZenModeHelper.mConsolidatedPolicy.allowSystem()).isFalse(); // default
- assertThat(mZenModeHelper.mConsolidatedPolicy.allowReminders()).isTrue(); // default
+ assertThat(mZenModeHelper.mConsolidatedPolicy.allowSystem()).isTrue(); // priority rule
+ assertThat(mZenModeHelper.mConsolidatedPolicy.allowReminders()).isFalse(); // default
assertThat(mZenModeHelper.mConsolidatedPolicy.allowCalls()).isTrue(); // default
assertThat(mZenModeHelper.mConsolidatedPolicy.allowMessages()).isTrue(); // default
assertThat(mZenModeHelper.mConsolidatedPolicy.allowConversations()).isTrue(); // default
@@ -3408,7 +3902,7 @@
public void zenRuleToAutomaticZenRule_allFields() {
mSetFlagsRule.enableFlags(Flags.FLAG_MODES_API);
when(mPackageManager.getPackagesForUid(anyInt())).thenReturn(
- new String[] {OWNER.getPackageName()});
+ new String[]{OWNER.getPackageName()});
ZenModeConfig.ZenRule rule = new ZenModeConfig.ZenRule();
rule.configurationActivity = CONFIG_ACTIVITY;
@@ -3452,7 +3946,7 @@
public void automaticZenRuleToZenRule_allFields() {
mSetFlagsRule.enableFlags(Flags.FLAG_MODES_API);
when(mPackageManager.getPackagesForUid(anyInt())).thenReturn(
- new String[] {OWNER.getPackageName()});
+ new String[]{OWNER.getPackageName()});
AutomaticZenRule azr = new AutomaticZenRule.Builder(NAME, CONDITION_ID)
.setEnabled(true)
@@ -3478,7 +3972,8 @@
assertEquals(CONDITION_ID, storedRule.conditionId);
assertEquals(INTERRUPTION_FILTER_ZR, storedRule.zenMode);
assertEquals(ENABLED, storedRule.enabled);
- assertEquals(POLICY, storedRule.zenPolicy);
+ assertEquals(mZenModeHelper.getDefaultZenPolicy().overwrittenWith(POLICY),
+ storedRule.zenPolicy);
assertEquals(CONFIG_ACTIVITY, storedRule.configurationActivity);
assertEquals(TYPE, storedRule.type);
assertEquals(ALLOW_MANUAL, storedRule.allowManualInvocation);
@@ -3561,12 +4056,12 @@
// Modifies the zen policy and device effects
ZenPolicy policy = new ZenPolicy.Builder(rule.getZenPolicy())
- .allowPriorityChannels(true)
+ .allowPriorityChannels(false)
.build();
ZenDeviceEffects deviceEffects =
new ZenDeviceEffects.Builder(rule.getDeviceEffects())
- .setShouldDisplayGrayscale(true)
- .build();
+ .setShouldDisplayGrayscale(true)
+ .build();
AutomaticZenRule azrUpdate = new AutomaticZenRule.Builder(rule)
.setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY)
.setZenPolicy(policy)
@@ -3580,7 +4075,9 @@
// UPDATE_ORIGIN_USER should change the bitmask and change the values.
assertThat(rule.getInterruptionFilter()).isEqualTo(INTERRUPTION_FILTER_PRIORITY);
- assertThat(rule.getZenPolicy().getPriorityChannels()).isEqualTo(ZenPolicy.STATE_ALLOW);
+ assertThat(rule.getZenPolicy().getPriorityChannelsAllowed()).isEqualTo(
+ ZenPolicy.STATE_DISALLOW);
+
assertThat(rule.getDeviceEffects().shouldDisplayGrayscale()).isTrue();
ZenRule storedRule = mZenModeHelper.mConfig.automaticRules.get(ruleId);
@@ -3770,9 +4267,9 @@
@Test
@EnableFlags(Flags.FLAG_MODES_API)
public void updateAutomaticZenRule_nullPolicyUpdate() {
- // Adds a starting rule with empty zen policies and device effects
+ // Adds a starting rule with set zen policy and empty device effects
AutomaticZenRule azrBase = new AutomaticZenRule.Builder(NAME, CONDITION_ID)
- .setZenPolicy(new ZenPolicy.Builder().build())
+ .setZenPolicy(POLICY)
.build();
// Adds the rule using the app, to avoid having any user modified bits set.
String ruleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(),
@@ -3790,8 +4287,10 @@
Process.SYSTEM_UID);
rule = mZenModeHelper.getAutomaticZenRule(ruleId);
- // When AZR's ZenPolicy is null, we expect the updated rule's policy to be null.
- assertThat(rule.getZenPolicy()).isNull();
+ // When AZR's ZenPolicy is null, we expect the updated rule's policy to be unchanged
+ // (equivalent to the provided policy, with additional fields filled in with defaults).
+ assertThat(rule.getZenPolicy()).isEqualTo(
+ mZenModeHelper.getDefaultZenPolicy().overwrittenWith(POLICY));
}
@Test
@@ -3841,19 +4340,20 @@
// New ZenPolicy differs from the default config
assertThat(rule.getZenPolicy()).isNotNull();
- assertThat(rule.getZenPolicy().getPriorityChannels()).isEqualTo(ZenPolicy.STATE_DISALLOW);
+ assertThat(rule.getZenPolicy().getPriorityChannelsAllowed()).isEqualTo(
+ ZenPolicy.STATE_DISALLOW);
ZenRule storedRule = mZenModeHelper.mConfig.automaticRules.get(ruleId);
assertThat(storedRule.canBeUpdatedByApp()).isFalse();
assertThat(storedRule.zenPolicyUserModifiedFields).isEqualTo(
ZenPolicy.FIELD_ALLOW_CHANNELS
- | ZenPolicy.FIELD_PRIORITY_CATEGORY_REMINDERS
- | ZenPolicy.FIELD_PRIORITY_CATEGORY_EVENTS
- | ZenPolicy.FIELD_PRIORITY_CATEGORY_SYSTEM
- | ZenPolicy.FIELD_VISUAL_EFFECT_FULL_SCREEN_INTENT
- | ZenPolicy.FIELD_VISUAL_EFFECT_LIGHTS
- | ZenPolicy.FIELD_VISUAL_EFFECT_PEEK
- | ZenPolicy.FIELD_VISUAL_EFFECT_AMBIENT
+ | ZenPolicy.FIELD_PRIORITY_CATEGORY_REMINDERS
+ | ZenPolicy.FIELD_PRIORITY_CATEGORY_EVENTS
+ | ZenPolicy.FIELD_PRIORITY_CATEGORY_SYSTEM
+ | ZenPolicy.FIELD_VISUAL_EFFECT_FULL_SCREEN_INTENT
+ | ZenPolicy.FIELD_VISUAL_EFFECT_LIGHTS
+ | ZenPolicy.FIELD_VISUAL_EFFECT_PEEK
+ | ZenPolicy.FIELD_VISUAL_EFFECT_AMBIENT
);
}
@@ -4016,6 +4516,7 @@
final int[] actualStatus = new int[2];
ZenModeHelper.Callback callback = new ZenModeHelper.Callback() {
int i = 0;
+
@Override
void onAutomaticRuleStatusChanged(int userId, String pkg, String id, int status) {
if (Objects.equals(createdId, id)) {
@@ -4056,6 +4557,7 @@
final int[] actualStatus = new int[2];
ZenModeHelper.Callback callback = new ZenModeHelper.Callback() {
int i = 0;
+
@Override
void onAutomaticRuleStatusChanged(int userId, String pkg, String id, int status) {
if (Objects.equals(createdId, id)) {
@@ -4202,6 +4704,7 @@
.build()),
eq(UPDATE_ORIGIN_APP));
}
+
@Test
public void testDeviceEffects_noChangeToConsolidatedEffects_notApplied() {
mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API);
@@ -4607,7 +5110,8 @@
.comparingElementsUsing(IGNORE_METADATA)
.containsExactly(
expectedImplicitRule(CUSTOM_PKG_NAME, ZEN_MODE_IMPORTANT_INTERRUPTIONS,
- null, true));
+ mZenModeHelper.mConfig.toZenPolicy(), // copy of global config
+ true));
}
@Test
@@ -4626,7 +5130,9 @@
assertThat(mZenModeHelper.mConfig.automaticRules.values())
.comparingElementsUsing(IGNORE_METADATA)
.containsExactly(
- expectedImplicitRule(CUSTOM_PKG_NAME, ZEN_MODE_ALARMS, null, true));
+ expectedImplicitRule(CUSTOM_PKG_NAME, ZEN_MODE_ALARMS,
+ mZenModeHelper.mConfig.toZenPolicy(), // copy of global config
+ true));
}
@Test
@@ -4820,6 +5326,10 @@
mZenModeHelper.applyGlobalPolicyAsImplicitZenRule(pkg, CUSTOM_PKG_UID, originalPolicy);
String ruleId = getOnlyElement(mZenModeHelper.mConfig.automaticRules.keySet());
+ // Store this for checking later.
+ ZenPolicy originalEffectiveZenPolicy = new ZenPolicy.Builder(
+ mZenModeHelper.mConfig.toZenPolicy()).allowMedia(true).build();
+
// From user, update that rule's policy.
AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(ruleId);
ZenPolicy userUpdateZenPolicy = new ZenPolicy.Builder().disallowAllSounds()
@@ -4839,7 +5349,9 @@
.comparingElementsUsing(IGNORE_METADATA)
.containsExactly(
expectedImplicitRule(pkg, ZEN_MODE_IMPORTANT_INTERRUPTIONS,
- userUpdateZenPolicy,
+ // the final policy for the rule should contain the user's update
+ // overlaid on top of the original existing policy.
+ originalEffectiveZenPolicy.overwrittenWith(userUpdateZenPolicy),
/* conditionActive= */ null));
}
@@ -4854,6 +5366,10 @@
mZenModeHelper.applyGlobalPolicyAsImplicitZenRule(pkg, CUSTOM_PKG_UID, originalPolicy);
String ruleId = getOnlyElement(mZenModeHelper.mConfig.automaticRules.keySet());
+ // Store this for checking later.
+ ZenPolicy originalEffectiveZenPolicy = new ZenPolicy.Builder(
+ mZenModeHelper.mConfig.toZenPolicy()).allowMedia(true).build();
+
// From user, update something in that rule, but not the ZenPolicy.
AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(ruleId);
AutomaticZenRule userUpdateRule = new AutomaticZenRule.Builder(rule)
@@ -4873,7 +5389,7 @@
.allowPriorityChannels(true)
.build();
assertThat(getOnlyElement(mZenModeHelper.mConfig.automaticRules.values()).zenPolicy)
- .isEqualTo(appsSecondZenPolicy);
+ .isEqualTo(originalEffectiveZenPolicy.overwrittenWith(appsSecondZenPolicy));
}
@Test
@@ -4905,14 +5421,18 @@
}
@Test
- public void getNotificationPolicyFromImplicitZenRule_ruleWithoutPolicy_returnsGlobalPolicy() {
+ public void getNotificationPolicyFromImplicitZenRule_ruleWithoutPolicy_copiesGlobalPolicy() {
mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API);
-
- mZenModeHelper.applyGlobalZenModeAsImplicitZenRule(CUSTOM_PKG_NAME, CUSTOM_PKG_UID,
- ZEN_MODE_ALARMS);
mZenModeHelper.mConfig.allowCalls = true;
mZenModeHelper.mConfig.allowConversations = false;
+ // Implicit rule will get the global policy at the time of rule creation.
+ mZenModeHelper.applyGlobalZenModeAsImplicitZenRule(CUSTOM_PKG_NAME, CUSTOM_PKG_UID,
+ ZEN_MODE_ALARMS);
+
+ // If the policy then changes afterwards, we should keep the snapshotted version.
+ mZenModeHelper.mConfig.allowCalls = false;
+
Policy readPolicy = mZenModeHelper.getNotificationPolicyFromImplicitZenRule(
CUSTOM_PKG_NAME);
@@ -4952,7 +5472,7 @@
p.recycle();
}
},
- "Ignoring timestamp and userModifiedFields");
+ "Ignoring timestamp and userModifiedFields");
private ZenRule expectedImplicitRule(String ownerPkg, int zenMode, ZenPolicy policy,
@Nullable Boolean conditionActive) {
@@ -4962,7 +5482,7 @@
if (conditionActive != null) {
rule.condition = conditionActive
? new Condition(rule.conditionId,
- mContext.getString(R.string.zen_mode_implicit_activated), STATE_TRUE)
+ mContext.getString(R.string.zen_mode_implicit_activated), STATE_TRUE)
: new Condition(rule.conditionId,
mContext.getString(R.string.zen_mode_implicit_deactivated),
STATE_FALSE);
@@ -5030,8 +5550,35 @@
assertEquals(STATE_ALLOW, dndProto.getNotificationList().getNumber());
}
+ private void checkDndProtoMatchesDefaultZenConfig(DNDPolicyProto dndProto) {
+ if (!Flags.modesApi()) {
+ checkDndProtoMatchesSetupZenConfig(dndProto);
+ return;
+ }
+
+ // When modes_api flag is on, the default zen config is the device defaults.
+ assertThat(dndProto.getAlarms().getNumber()).isEqualTo(STATE_ALLOW);
+ assertThat(dndProto.getMedia().getNumber()).isEqualTo(STATE_ALLOW);
+ assertThat(dndProto.getSystem().getNumber()).isEqualTo(STATE_DISALLOW);
+ assertThat(dndProto.getReminders().getNumber()).isEqualTo(STATE_DISALLOW);
+ assertThat(dndProto.getCalls().getNumber()).isEqualTo(STATE_ALLOW);
+ assertThat(dndProto.getAllowCallsFrom().getNumber()).isEqualTo(PEOPLE_STARRED);
+ assertThat(dndProto.getMessages().getNumber()).isEqualTo(STATE_ALLOW);
+ assertThat(dndProto.getAllowMessagesFrom().getNumber()).isEqualTo(PEOPLE_STARRED);
+ assertThat(dndProto.getEvents().getNumber()).isEqualTo(STATE_DISALLOW);
+ assertThat(dndProto.getRepeatCallers().getNumber()).isEqualTo(STATE_ALLOW);
+ assertThat(dndProto.getFullscreen().getNumber()).isEqualTo(STATE_DISALLOW);
+ assertThat(dndProto.getLights().getNumber()).isEqualTo(STATE_DISALLOW);
+ assertThat(dndProto.getPeek().getNumber()).isEqualTo(STATE_DISALLOW);
+ assertThat(dndProto.getStatusBar().getNumber()).isEqualTo(STATE_ALLOW);
+ assertThat(dndProto.getBadge().getNumber()).isEqualTo(STATE_ALLOW);
+ assertThat(dndProto.getAmbient().getNumber()).isEqualTo(STATE_DISALLOW);
+ assertThat(dndProto.getNotificationList().getNumber()).isEqualTo(STATE_ALLOW);
+ }
+
private static void withoutWtfCrash(Runnable test) {
- Log.TerribleFailureHandler oldHandler = Log.setWtfHandler((tag, what, system) -> {});
+ Log.TerribleFailureHandler oldHandler = Log.setWtfHandler((tag, what, system) -> {
+ });
try {
test.run();
} finally {
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenPolicyTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenPolicyTest.java
index 4ed55df..3a88294 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ZenPolicyTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenPolicyTest.java
@@ -218,7 +218,7 @@
// unset applied, channels setting keeps its state
channelsPriority.apply(unset);
- assertThat(channelsPriority.getPriorityChannels()).isEqualTo(ZenPolicy.STATE_ALLOW);
+ assertThat(channelsPriority.getPriorityChannelsAllowed()).isEqualTo(ZenPolicy.STATE_ALLOW);
}
@Test
@@ -234,7 +234,7 @@
// priority channels (less strict state) cannot override a setting that sets it to none
none.apply(priority);
- assertThat(none.getPriorityChannels()).isEqualTo(ZenPolicy.STATE_DISALLOW);
+ assertThat(none.getPriorityChannelsAllowed()).isEqualTo(ZenPolicy.STATE_DISALLOW);
}
@Test
@@ -250,7 +250,7 @@
// applying a policy with channelType=none overrides priority setting
priority.apply(none);
- assertThat(priority.getPriorityChannels()).isEqualTo(ZenPolicy.STATE_DISALLOW);
+ assertThat(priority.getPriorityChannelsAllowed()).isEqualTo(ZenPolicy.STATE_DISALLOW);
}
@Test
@@ -265,7 +265,79 @@
// applying a policy with a set channel type actually goes through
unset.apply(priority);
- assertThat(unset.getPriorityChannels()).isEqualTo(ZenPolicy.STATE_ALLOW);
+ assertThat(unset.getPriorityChannelsAllowed()).isEqualTo(ZenPolicy.STATE_ALLOW);
+ }
+
+ @Test
+ public void testZenPolicyOverwrite_allUnsetPolicies() {
+ mSetFlagsRule.enableFlags(Flags.FLAG_MODES_API);
+
+ ZenPolicy.Builder builder = new ZenPolicy.Builder();
+ ZenPolicy unset = builder.build();
+
+ builder.allowCalls(ZenPolicy.PEOPLE_TYPE_CONTACTS);
+ builder.allowMedia(false);
+ builder.allowEvents(true);
+ builder.showFullScreenIntent(false);
+ builder.showInNotificationList(false);
+ ZenPolicy set = builder.build();
+
+ ZenPolicy overwritten = set.overwrittenWith(unset);
+ assertThat(overwritten).isEqualTo(set);
+
+ // should actually work the other way too.
+ ZenPolicy overwrittenWithSet = unset.overwrittenWith(set);
+ assertThat(overwrittenWithSet).isEqualTo(set);
+ }
+
+ @Test
+ public void testZenPolicyOverwrite_someOverlappingFields_takeNewPolicy() {
+ mSetFlagsRule.enableFlags(Flags.FLAG_MODES_API);
+
+ ZenPolicy p1 = new ZenPolicy.Builder()
+ .allowCalls(ZenPolicy.PEOPLE_TYPE_CONTACTS)
+ .allowMessages(ZenPolicy.PEOPLE_TYPE_STARRED)
+ .allowMedia(false)
+ .showBadges(true)
+ .build();
+
+ ZenPolicy p2 = new ZenPolicy.Builder()
+ .allowRepeatCallers(false)
+ .allowConversations(ZenPolicy.CONVERSATION_SENDERS_IMPORTANT)
+ .allowMessages(ZenPolicy.PEOPLE_TYPE_NONE)
+ .showBadges(false)
+ .showPeeking(true)
+ .build();
+
+ // when p1 is overwritten with p2, all values from p2 win regardless of strictness, and
+ // remaining fields take values from p1.
+ ZenPolicy p1OverwrittenWithP2 = p1.overwrittenWith(p2);
+ assertThat(p1OverwrittenWithP2.getPriorityCallSenders())
+ .isEqualTo(ZenPolicy.PEOPLE_TYPE_CONTACTS); // from p1
+ assertThat(p1OverwrittenWithP2.getPriorityMessageSenders())
+ .isEqualTo(ZenPolicy.PEOPLE_TYPE_NONE); // from p2
+ assertThat(p1OverwrittenWithP2.getPriorityCategoryRepeatCallers())
+ .isEqualTo(ZenPolicy.STATE_DISALLOW); // from p2
+ assertThat(p1OverwrittenWithP2.getPriorityCategoryMedia())
+ .isEqualTo(ZenPolicy.STATE_DISALLOW); // from p1
+ assertThat(p1OverwrittenWithP2.getVisualEffectBadge())
+ .isEqualTo(ZenPolicy.STATE_DISALLOW); // from p2
+ assertThat(p1OverwrittenWithP2.getVisualEffectPeek())
+ .isEqualTo(ZenPolicy.STATE_ALLOW); // from p2
+
+ ZenPolicy p2OverwrittenWithP1 = p2.overwrittenWith(p1);
+ assertThat(p2OverwrittenWithP1.getPriorityCallSenders())
+ .isEqualTo(ZenPolicy.PEOPLE_TYPE_CONTACTS); // from p1
+ assertThat(p2OverwrittenWithP1.getPriorityMessageSenders())
+ .isEqualTo(ZenPolicy.PEOPLE_TYPE_STARRED); // from p1
+ assertThat(p2OverwrittenWithP1.getPriorityCategoryRepeatCallers())
+ .isEqualTo(ZenPolicy.STATE_DISALLOW); // from p2
+ assertThat(p2OverwrittenWithP1.getPriorityCategoryMedia())
+ .isEqualTo(ZenPolicy.STATE_DISALLOW); // from p1
+ assertThat(p2OverwrittenWithP1.getVisualEffectBadge())
+ .isEqualTo(ZenPolicy.STATE_ALLOW); // from p1
+ assertThat(p2OverwrittenWithP1.getVisualEffectPeek())
+ .isEqualTo(ZenPolicy.STATE_ALLOW); // from p2
}
@Test
@@ -307,7 +379,7 @@
ZenPolicy.Builder builder = new ZenPolicy.Builder();
ZenPolicy policy = builder.build();
- assertThat(policy.getPriorityChannels()).isEqualTo(ZenPolicy.STATE_UNSET);
+ assertThat(policy.getPriorityChannelsAllowed()).isEqualTo(ZenPolicy.STATE_UNSET);
}
@Test
@@ -624,7 +696,7 @@
builder.allowPriorityChannels(true);
ZenPolicy policy = builder.build();
- assertThat(policy.getPriorityChannels()).isEqualTo(ZenPolicy.STATE_UNSET);
+ assertThat(policy.getPriorityChannelsAllowed()).isEqualTo(ZenPolicy.STATE_UNSET);
assertThat(policy.toString().contains("allowChannels")).isFalse();
}
@@ -636,12 +708,12 @@
ZenPolicy.Builder builder = new ZenPolicy.Builder();
builder.allowPriorityChannels(true);
ZenPolicy policy = builder.build();
- assertThat(policy.getPriorityChannels()).isEqualTo(ZenPolicy.STATE_ALLOW);
+ assertThat(policy.getPriorityChannelsAllowed()).isEqualTo(ZenPolicy.STATE_ALLOW);
// disallow priority channels
builder.allowPriorityChannels(false);
policy = builder.build();
- assertThat(policy.getPriorityChannels()).isEqualTo(ZenPolicy.STATE_DISALLOW);
+ assertThat(policy.getPriorityChannelsAllowed()).isEqualTo(ZenPolicy.STATE_DISALLOW);
}
@Test
@@ -662,7 +734,7 @@
assertThat(newPolicy.getVisualEffectBadge()).isEqualTo(ZenPolicy.STATE_DISALLOW);
assertThat(newPolicy.getVisualEffectPeek()).isEqualTo(ZenPolicy.STATE_UNSET);
- assertThat(newPolicy.getPriorityChannels()).isEqualTo(ZenPolicy.STATE_ALLOW);
+ assertThat(newPolicy.getPriorityChannelsAllowed()).isEqualTo(ZenPolicy.STATE_ALLOW);
}
@Test
diff --git a/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java b/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java
index 7c2f7ee..c8abd8d 100644
--- a/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java
+++ b/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java
@@ -166,6 +166,7 @@
@Mock
private PhoneWindowManager.ButtonOverridePermissionChecker mButtonOverridePermissionChecker;
+ @Mock private WindowWakeUpPolicy mWindowWakeUpPolicy;
@Mock private IBinder mInputToken;
@Mock private IBinder mImeTargetWindowToken;
@@ -230,6 +231,10 @@
TalkbackShortcutController getTalkbackShortcutController() {
return new TestTalkbackShortcutController(mContext);
}
+
+ WindowWakeUpPolicy getWindowWakeUpPolicy() {
+ return mWindowWakeUpPolicy;
+ }
}
TestPhoneWindowManager(Context context, boolean supportSettingsUpdate) {
@@ -620,7 +625,8 @@
void assertPowerWakeUp() {
mTestLooper.dispatchAll();
- verify(mPowerManager).wakeUp(anyLong(), anyInt(), anyString());
+ verify(mWindowWakeUpPolicy)
+ .wakeUpFromKey(anyLong(), eq(KeyEvent.KEYCODE_POWER), anyBoolean());
}
void assertNoPowerSleep() {
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 ba7b52e..31d6fa3 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
@@ -1762,32 +1762,6 @@
assertEquals(1, task.getChildCount());
}
- /**
- * Test that an activity will not be destroyed if it is marked as non-destroyable.
- */
- @Test
- public void testSafelyDestroy_nonDestroyable() {
- final ActivityRecord activity = createActivityWithTask();
- doReturn(false).when(activity).isDestroyable();
-
- activity.safelyDestroy("test");
-
- verify(activity, never()).destroyImmediately(anyString());
- }
-
- /**
- * Test that an activity will not be destroyed if it is marked as non-destroyable.
- */
- @Test
- public void testSafelyDestroy_destroyable() {
- final ActivityRecord activity = createActivityWithTask();
- doReturn(true).when(activity).isDestroyable();
-
- activity.safelyDestroy("test");
-
- verify(activity).destroyImmediately(anyString());
- }
-
@Test
public void testRemoveImmediately() {
final Consumer<Consumer<ActivityRecord>> test = setup -> {
@@ -3748,6 +3722,68 @@
assertFalse(ar.moveFocusableActivityToTop("test"));
}
+ @Test
+ public void testPauseConfigDispatch() throws RemoteException {
+ final Task task = new TaskBuilder(mSupervisor)
+ .setDisplay(mDisplayContent).setCreateActivity(true).build();
+ final ActivityRecord activity = task.getTopNonFinishingActivity();
+ final WindowManager.LayoutParams attrs = new WindowManager.LayoutParams(
+ TYPE_BASE_APPLICATION);
+ attrs.setTitle("AppWindow");
+ final TestWindowState appWindow = createWindowState(attrs, activity);
+ activity.addWindow(appWindow);
+
+ clearInvocations(mClientLifecycleManager);
+ clearInvocations(activity);
+
+ Configuration ro = activity.getRequestedOverrideConfiguration();
+ ro.windowConfiguration.setBounds(new Rect(20, 0, 120, 200));
+ activity.onRequestedOverrideConfigurationChanged(ro);
+ activity.ensureActivityConfiguration();
+ mWm.mRoot.performSurfacePlacement();
+
+ // policy will center the bounds, so just check for matching size here.
+ assertEquals(100, activity.getWindowConfiguration().getBounds().width());
+ assertEquals(100, appWindow.getWindowConfiguration().getBounds().width());
+ // No scheduled transactions since it asked for a restart.
+ verify(mClientLifecycleManager, times(1)).scheduleTransaction(any());
+ verify(activity, times(1)).setLastReportedConfiguration(any(), any());
+ assertTrue(appWindow.mResizeReported);
+
+ // act like everything drew and went idle
+ appWindow.mResizeReported = false;
+ makeLastConfigReportedToClient(appWindow, true);
+
+ // Now pause dispatch and try to resize
+ activity.pauseConfigurationDispatch();
+
+ ro.windowConfiguration.setBounds(new Rect(20, 0, 150, 200));
+ activity.onRequestedOverrideConfigurationChanged(ro);
+ activity.ensureActivityConfiguration();
+ mWm.mRoot.performSurfacePlacement();
+
+ // Activity should get new config (core-side)
+ assertEquals(130, activity.getWindowConfiguration().getBounds().width());
+ // But windows should not get new config.
+ assertEquals(100, appWindow.getWindowConfiguration().getBounds().width());
+ // The client shouldn't receive any changes
+ verify(mClientLifecycleManager, times(1)).scheduleTransaction(any());
+ // and lastReported shouldn't be set.
+ verify(activity, times(1)).setLastReportedConfiguration(any(), any());
+ // There should be no resize reported to client.
+ assertFalse(appWindow.mResizeReported);
+
+ // Now resume dispatch
+ activity.resumeConfigurationDispatch();
+ mWm.mRoot.performSurfacePlacement();
+
+ // Windows and client should now receive updates
+ verify(activity, times(2)).setLastReportedConfiguration(any(), any());
+ verify(mClientLifecycleManager, times(2)).scheduleTransaction(any());
+ assertEquals(130, appWindow.getWindowConfiguration().getBounds().width());
+ assertTrue(appWindow.mResizeReported);
+ }
+
private ICompatCameraControlCallback getCompatCameraControlCallback() {
return new ICompatCameraControlCallback.Stub() {
@Override
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java
index 1c57623..29faed1 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java
@@ -132,8 +132,10 @@
import org.mockito.MockitoSession;
import org.mockito.quality.Strictness;
+import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
+import java.util.List;
import java.util.Set;
/**
@@ -563,6 +565,86 @@
return Pair.create(splitPrimaryActivity, splitSecondActivity);
}
+ /**
+ * This test ensures that if the intent is being delivered to a desktop mode unfocused task
+ * while it is already on top, reports it as delivering to top.
+ */
+ @Test
+ public void testDesktopModeDeliverToTop() {
+ final ActivityStarter starter = prepareStarter(
+ FLAG_ACTIVITY_RESET_TASK_IF_NEEDED | FLAG_ACTIVITY_SINGLE_TOP,
+ false /* mockGetRootTask */);
+ final List<ActivityRecord> activities = createActivitiesInDesktopMode();
+
+ // Set focus back to the first task.
+ activities.get(0).moveFocusableActivityToTop("testDesktopModeDeliverToTop");
+
+ // Start activity and delivered new intent.
+ starter.getIntent().setComponent(activities.get(3).mActivityComponent);
+ doReturn(activities.get(3)).when(mRootWindowContainer).findTask(any(), any());
+ final int result = starter.setReason("testDesktopModeDeliverToTop").execute();
+
+ // Ensure result is delivering intent to top.
+ assertEquals(START_DELIVERED_TO_TOP, result);
+ }
+
+ /**
+ * This test ensures that if the intent is being delivered to a desktop mode unfocused task
+ * reports it is brought to front instead of delivering to top.
+ */
+ @Test
+ public void testDesktopModeTaskToFront() {
+ final ActivityStarter starter = prepareStarter(
+ FLAG_ACTIVITY_RESET_TASK_IF_NEEDED | FLAG_ACTIVITY_SINGLE_TOP, false);
+ final List<ActivityRecord> activities = createActivitiesInDesktopMode();
+ final ActivityRecord desktopModeFocusActivity = activities.get(0);
+ final ActivityRecord desktopModeReusableActivity = activities.get(1);
+ final ActivityRecord desktopModeTopActivity = new ActivityBuilder(mAtm).setCreateTask(true)
+ .setParentTask(desktopModeReusableActivity.getRootTask()).build();
+ assertTrue(desktopModeTopActivity.inMultiWindowMode());
+
+ // Let first stack has focus.
+ desktopModeFocusActivity.moveFocusableActivityToTop("testDesktopModeTaskToFront");
+
+ // Start activity and delivered new intent.
+ starter.getIntent().setComponent(desktopModeReusableActivity.mActivityComponent);
+ doReturn(desktopModeReusableActivity).when(mRootWindowContainer).findTask(any(), any());
+ final int result = starter.setReason("testDesktopModeMoveToFront").execute();
+
+ // Ensure result is moving task to front.
+ assertEquals(START_TASK_TO_FRONT, result);
+ }
+
+ /** Returns 4 activities. */
+ private List<ActivityRecord> createActivitiesInDesktopMode() {
+ final TestDesktopOrganizer desktopOrganizer = new TestDesktopOrganizer(mAtm);
+ List<ActivityRecord> activityRecords = new ArrayList<>();
+
+ for (int i = 0; i < 4; i++) {
+ Rect bounds = new Rect(desktopOrganizer.getDefaultDesktopTaskBounds());
+ bounds.offset(20 * i, 20 * i);
+ desktopOrganizer.createTask(bounds);
+ }
+
+ for (int i = 0; i < 4; i++) {
+ activityRecords.add(new TaskBuilder(mSupervisor)
+ .setParentTask(desktopOrganizer.mTasks.get(i))
+ .setCreateActivity(true)
+ .build()
+ .getTopMostActivity());
+ }
+
+ for (int i = 0; i < 4; i++) {
+ activityRecords.get(i).setVisibleRequested(true);
+ }
+
+ for (int i = 0; i < 4; i++) {
+ assertEquals(desktopOrganizer.mTasks.get(i), activityRecords.get(i).getRootTask());
+ }
+
+ return activityRecords;
+ }
+
@Test
public void testMoveVisibleTaskToFront() {
final ActivityRecord activity = new TaskBuilder(mSupervisor)
diff --git a/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java
index 402cbcc..c44be7b 100644
--- a/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java
@@ -56,6 +56,7 @@
import android.os.RemoteCallback;
import android.os.RemoteException;
import android.platform.test.annotations.Presubmit;
+import android.platform.test.annotations.RequiresFlagsEnabled;
import android.util.ArraySet;
import android.view.WindowManager;
import android.window.BackAnimationAdapter;
@@ -69,6 +70,7 @@
import android.window.WindowOnBackInvokedDispatcher;
import com.android.server.LocalServices;
+import com.android.window.flags.Flags;
import org.junit.Before;
import org.junit.Test;
@@ -81,6 +83,12 @@
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
+/**
+ * Tests for the {@link BackNavigationController} class.
+ *
+ * Build/Install/Run:
+ * atest WmTests:BackNavigationControllerTests
+ */
@Presubmit
@RunWith(WindowTestRunner.class)
public class BackNavigationControllerTests extends WindowTestsBase {
@@ -623,6 +631,22 @@
0, navigationObserver.getCount());
}
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_EMBEDDED_ACTIVITY_BACK_NAV_FLAG)
+ public void testAdjacentFocusInActivityEmbedding() {
+ Task task = createTask(mDefaultDisplay);
+ TaskFragment primary = createTaskFragmentWithActivity(task);
+ TaskFragment secondary = createTaskFragmentWithActivity(task);
+ primary.setAdjacentTaskFragment(secondary);
+ secondary.setAdjacentTaskFragment(primary);
+
+ WindowState windowState = mock(WindowState.class);
+ doReturn(windowState).when(mWm).getFocusedWindowLocked();
+ doReturn(primary).when(windowState).getTaskFragment();
+
+ startBackNavigation();
+ verify(mWm).moveFocusToAdjacentWindow(any(), anyInt());
+ }
/**
* Test with
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
index 782d89c..95850ac 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
@@ -2131,8 +2131,8 @@
// Once transition starts, rotation is applied and transition shows DC rotating.
testPlayer.startTransition();
waitUntilHandlersIdle();
- verify(activity1).ensureActivityConfiguration(anyBoolean(), anyBoolean());
- verify(activity2).ensureActivityConfiguration(anyBoolean(), anyBoolean());
+ verify(activity1).ensureActivityConfiguration(anyBoolean());
+ verify(activity2).ensureActivityConfiguration(anyBoolean());
assertNotEquals(origRot, dc.getConfiguration().windowConfiguration.getRotation());
assertNotNull(testPlayer.mLastReady);
assertTrue(testPlayer.mController.isPlaying());
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java
index be96e60..9e00f92 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java
@@ -283,12 +283,12 @@
policy.screenTurnedOff();
policy.setAwake(false);
- policy.screenTurnedOn(null /* screenOnListener */);
+ policy.screenTurningOn(null /* screenOnListener */);
assertTrue(wpc.isShowingUiWhileDozing());
policy.screenTurnedOff();
assertFalse(wpc.isShowingUiWhileDozing());
- policy.screenTurnedOn(null /* screenOnListener */);
+ policy.screenTurningOn(null /* screenOnListener */);
assertTrue(wpc.isShowingUiWhileDozing());
policy.setAwake(true);
assertFalse(wpc.isShowingUiWhileDozing());
diff --git a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
index 5518c60..0c1a9c3 100644
--- a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
@@ -197,10 +197,10 @@
mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */);
// Translucent Activity
final ActivityRecord translucentActivity = new ActivityBuilder(mAtm)
+ .setActivityTheme(android.R.style.Theme_Translucent)
.setLaunchedFromUid(mActivity.getUid())
.setScreenOrientation(SCREEN_ORIENTATION_PORTRAIT)
.build();
- doReturn(false).when(translucentActivity).fillsParent();
mTask.addChild(translucentActivity);
translucentActivity.setState(DESTROYED, "testing");
@@ -225,10 +225,10 @@
// Translucent Activity
final ActivityRecord translucentActivity = new ActivityBuilder(mAtm)
+ .setActivityTheme(android.R.style.Theme_Translucent)
.setLaunchedFromUid(mActivity.getUid())
.setScreenOrientation(SCREEN_ORIENTATION_PORTRAIT)
.build();
- doReturn(false).when(translucentActivity).fillsParent();
mTask.addChild(translucentActivity);
spyOn(translucentActivity.mLetterboxUiController);
@@ -300,10 +300,10 @@
// Translucent Activity
final ActivityRecord translucentActivity = new ActivityBuilder(mAtm)
+ .setActivityTheme(android.R.style.Theme_Translucent)
.setLaunchedFromUid(mActivity.getUid())
.setScreenOrientation(SCREEN_ORIENTATION_LANDSCAPE)
.build();
- doReturn(false).when(translucentActivity).fillsParent();
mTask.addChild(translucentActivity);
spyOn(translucentActivity.mLetterboxUiController);
@@ -376,10 +376,10 @@
// Launch translucent Activity
final ActivityRecord translucentActivity = new ActivityBuilder(mAtm)
+ .setActivityTheme(android.R.style.Theme_Translucent)
.setLaunchedFromUid(mActivity.getUid())
.setScreenOrientation(SCREEN_ORIENTATION_PORTRAIT)
.build();
- doReturn(false).when(translucentActivity).fillsParent();
mTask.addChild(translucentActivity);
// Transparent strategy applied
assertTrue(translucentActivity.mLetterboxUiController.hasInheritedLetterboxBehavior());
@@ -404,10 +404,10 @@
// Launch translucent Activity
final ActivityRecord translucentActivity = new ActivityBuilder(mAtm)
+ .setActivityTheme(android.R.style.Theme_Translucent)
.setLaunchedFromUid(mActivity.getUid())
.setScreenOrientation(SCREEN_ORIENTATION_PORTRAIT)
.build();
- doReturn(false).when(translucentActivity).fillsParent();
mTask.addChild(translucentActivity);
// Transparent strategy applied
assertTrue(translucentActivity.mLetterboxUiController.hasInheritedLetterboxBehavior());
@@ -441,10 +441,10 @@
// Launch translucent Activity
final ActivityRecord translucentActivity = new ActivityBuilder(mAtm)
+ .setActivityTheme(android.R.style.Theme_Translucent)
.setLaunchedFromUid(mActivity.getUid())
.setScreenOrientation(SCREEN_ORIENTATION_PORTRAIT)
.build();
- doReturn(false).when(translucentActivity).fillsParent();
mTask.addChild(translucentActivity);
// Transparent strategy applied
assertTrue(translucentActivity.mLetterboxUiController.hasInheritedLetterboxBehavior());
@@ -465,12 +465,12 @@
mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */);
// Translucent Activity
final ActivityRecord translucentActivity = new ActivityBuilder(mAtm)
+ .setActivityTheme(android.R.style.Theme_Translucent)
.setLaunchedFromUid(mActivity.getUid())
.setScreenOrientation(SCREEN_ORIENTATION_LANDSCAPE)
.setMinAspectRatio(1.1f)
.setMaxAspectRatio(3f)
.build();
- doReturn(false).when(translucentActivity).fillsParent();
mTask.addChild(translucentActivity);
// We check bounds
final Rect opaqueBounds = mActivity.getConfiguration().windowConfiguration.getBounds();
@@ -493,9 +493,9 @@
mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */);
// Translucent Activity
final ActivityRecord translucentActivity = new ActivityBuilder(mAtm)
+ .setActivityTheme(android.R.style.Theme_Translucent)
.setLaunchedFromUid(mActivity.getUid())
.build();
- doReturn(false).when(translucentActivity).fillsParent();
final Configuration requestedConfig =
translucentActivity.getRequestedOverrideConfiguration();
final WindowConfiguration translucentWinConf = requestedConfig.windowConfiguration;
@@ -525,12 +525,12 @@
mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */);
// Translucent Activity
final ActivityRecord translucentActivity = new ActivityBuilder(mAtm)
+ .setActivityTheme(android.R.style.Theme_Translucent)
.setLaunchedFromUid(mActivity.getUid())
.setScreenOrientation(SCREEN_ORIENTATION_LANDSCAPE)
.setMinAspectRatio(1.1f)
.setMaxAspectRatio(3f)
.build();
- doReturn(false).when(translucentActivity).fillsParent();
mTask.addChild(translucentActivity);
// We check bounds
final Rect opaqueBounds = mActivity.getConfiguration().windowConfiguration.getBounds();
@@ -538,10 +538,10 @@
assertEquals(opaqueBounds, translucentRequestedBounds);
// Launch another translucent activity
final ActivityRecord translucentActivity2 = new ActivityBuilder(mAtm)
+ .setActivityTheme(android.R.style.Theme_Translucent)
.setLaunchedFromUid(mActivity.getUid())
.setScreenOrientation(SCREEN_ORIENTATION_LANDSCAPE)
.build();
- doReturn(false).when(translucentActivity2).fillsParent();
mTask.addChild(translucentActivity2);
// We check bounds
final Rect translucent2RequestedBounds = translucentActivity2.getRequestedOverrideBounds();
@@ -558,9 +558,9 @@
// simplicity.
doReturn(true).when(mActivity).isEmbedded();
// Translucent Activity
- final ActivityRecord translucentActivity = new ActivityBuilder(mAtm).build();
+ final ActivityRecord translucentActivity = new ActivityBuilder(mAtm)
+ .setActivityTheme(android.R.style.Theme_Translucent).build();
doReturn(false).when(translucentActivity).matchParentBounds();
- doReturn(false).when(translucentActivity).fillsParent();
mTask.addChild(translucentActivity);
// Check the strategy has not being applied
assertFalse(translucentActivity.mLetterboxUiController.hasInheritedLetterboxBehavior());
@@ -580,10 +580,10 @@
assertFalse(mActivity.inSizeCompatMode());
// We launch a transparent activity
final ActivityRecord translucentActivity = new ActivityBuilder(mAtm)
+ .setActivityTheme(android.R.style.Theme_Translucent)
.setLaunchedFromUid(mActivity.getUid())
.setScreenOrientation(SCREEN_ORIENTATION_PORTRAIT)
.build();
- doReturn(false).when(translucentActivity).fillsParent();
mTask.addChild(translucentActivity);
// It should not be in SCM
assertFalse(translucentActivity.inSizeCompatMode());
@@ -600,12 +600,16 @@
mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */);
// Translucent Activity
final ActivityRecord translucentActivity = new ActivityBuilder(mAtm)
+ .setActivityTheme(android.R.style.Theme_Translucent)
.setLaunchedFromUid(mActivity.getUid())
.build();
- doReturn(false).when(translucentActivity).fillsParent();
- spyOn(mActivity);
+ assertFalse(translucentActivity.fillsParent());
+ assertTrue(mActivity.fillsParent());
+ mActivity.finishing = true;
+ assertFalse(mActivity.occludesParent());
mTask.addChild(translucentActivity);
- verify(mActivity).isFinishing();
+ // The translucent activity won't inherit letterbox behavior from a finishing activity.
+ assertFalse(translucentActivity.mLetterboxUiController.hasInheritedLetterboxBehavior());
}
@Test
@@ -619,10 +623,10 @@
prepareUnresizable(mActivity, SCREEN_ORIENTATION_PORTRAIT);
// We launch a transparent activity
final ActivityRecord translucentActivity = new ActivityBuilder(mAtm)
+ .setActivityTheme(android.R.style.Theme_Translucent)
.setLaunchedFromUid(mActivity.getUid())
.setScreenOrientation(SCREEN_ORIENTATION_PORTRAIT)
.build();
- doReturn(false).when(translucentActivity).fillsParent();
mTask.addChild(translucentActivity);
assertEquals(translucentActivity.getBounds(), mActivity.getBounds());
@@ -655,10 +659,10 @@
// We launch a transparent activity
final ActivityRecord translucentActivity = new ActivityBuilder(mAtm)
+ .setActivityTheme(android.R.style.Theme_Translucent)
.setLaunchedFromUid(mActivity.getUid())
.setScreenOrientation(SCREEN_ORIENTATION_PORTRAIT)
.build();
- doReturn(false).when(translucentActivity).fillsParent();
mTask.addChild(translucentActivity);
// The transparent activity inherits the compat display insets of the opaque activity
@@ -882,8 +886,6 @@
spyOn(mActivity.mLetterboxUiController);
doReturn(true).when(mActivity.mLetterboxUiController)
- .isSurfaceReadyToShow(any());
- doReturn(true).when(mActivity.mLetterboxUiController)
.isSurfaceVisible(any());
assertTrue(mActivity.mLetterboxUiController.shouldShowLetterboxUi(
@@ -1020,8 +1022,17 @@
// Activity is sandboxed due to fixed aspect ratio.
assertActivityMaxBoundsSandboxed();
+ // Prepare the states for verifying relaunching after changing orientation.
+ mActivity.finishRelaunching();
+ mActivity.setState(RESUMED, "testFixedAspectRatioOrientationChangeOrientation");
+ mActivity.setLastReportedConfiguration(mAtm.getGlobalConfiguration(),
+ mActivity.getConfiguration());
+
// Change the fixed orientation.
mActivity.setRequestedOrientation(SCREEN_ORIENTATION_LANDSCAPE);
+ assertTrue(mActivity.isRelaunching());
+ assertTrue(mActivity.mLetterboxUiController
+ .getIsRelaunchingAfterRequestedOrientationChanged());
assertFitted();
assertEquals(originalBounds.width(), mActivity.getBounds().height());
@@ -4781,6 +4792,7 @@
new WindowManager.LayoutParams(TYPE_STATUS_BAR);
final Binder owner = new Binder();
attrs.gravity = android.view.Gravity.TOP;
+ attrs.height = STATUS_BAR_HEIGHT;
attrs.layoutInDisplayCutoutMode =
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
attrs.setFitInsetsTypes(0 /* types */);
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java
index 7c7e562..245b2c5 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java
@@ -444,6 +444,10 @@
// Not ready if the task is still visible when the TaskFragment becomes empty.
doReturn(true).when(task).isVisibleRequested();
assertFalse(taskFragment.isReadyToTransit());
+
+ // Ready if the mAllowTransitionWhenEmpty flag is true.
+ taskFragment.setAllowTransitionWhenEmpty(true);
+ assertTrue(taskFragment.isReadyToTransit());
}
@Test
diff --git a/services/tests/wmtests/src/com/android/server/wm/TestDisplayContent.java b/services/tests/wmtests/src/com/android/server/wm/TestDisplayContent.java
index 45ecc3f..00ecd00 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TestDisplayContent.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TestDisplayContent.java
@@ -215,7 +215,7 @@
doReturn(false).when(newDisplay).supportsSystemDecorations();
}
// Update the display policy to make the screen fully turned on so animation is allowed
- displayPolicy.screenTurnedOn(null /* screenOnListener */);
+ displayPolicy.screenTurningOn(null /* screenOnListener */);
displayPolicy.finishKeyguardDrawn();
displayPolicy.finishWindowsDrawn();
displayPolicy.finishScreenTurningOn();
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 51df1d4..7d8eb90 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java
@@ -1602,6 +1602,33 @@
}
@Test
+ public void testTransientWithParallelLaunch() {
+ final Task recentTask = mDisplayContent.getDefaultTaskDisplayArea().getRootHomeTask();
+ final ActivityRecord recent = new ActivityBuilder(mAtm).setTask(recentTask)
+ .setVisible(false).build();
+ final ActivityRecord app = new ActivityBuilder(mAtm).setCreateTask(true).build();
+ final Task appTask = app.getTask();
+ registerTestTransitionPlayer();
+ final TransitionController controller = mRootWindowContainer.mTransitionController;
+ final Transition transition = createTestTransition(TRANSIT_OPEN, controller);
+ transition.mParallelCollectType = Transition.PARALLEL_TYPE_RECENTS;
+ controller.moveToCollecting(transition);
+ transition.collect(recentTask);
+ transition.collect(appTask);
+ transition.setTransientLaunch(recent, appTask);
+ recentTask.moveToFront("move-recent-to-front");
+ transition.setAllReady();
+ transition.start();
+ // Assume that the app starts another activity in its task.
+ final Transition newTransition = controller.createAndStartCollecting(TRANSIT_OPEN);
+
+ assertEquals(newTransition, controller.getCollectingTransition());
+ assertTrue(controller.mWaitingTransitions.contains(transition));
+ assertTrue(controller.isTransientHide(appTask));
+ assertTrue(controller.isTransientVisible(appTask));
+ }
+
+ @Test
public void testNotReadyPushPop() {
final TransitionController controller = new TestTransitionController(mAtm);
controller.setSyncEngine(mWm.mSyncEngine);
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java
index fe9d837..06afa38 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java
@@ -1068,7 +1068,7 @@
invocationOnMock.callRealMethod();
return null;
}).when(surface).lockCanvas(any());
- mWm.mAccessibilityController.drawMagnifiedRegionBorderIfNeeded(displayId, mTransaction);
+ mWm.mAccessibilityController.drawMagnifiedRegionBorderIfNeeded(displayId);
waitUntilHandlersIdle();
try {
verify(surface).lockCanvas(any());
@@ -1076,14 +1076,9 @@
clearInvocations(surface);
// Invalidate and redraw.
mWm.mAccessibilityController.onDisplaySizeChanged(mDisplayContent);
- mWm.mAccessibilityController.drawMagnifiedRegionBorderIfNeeded(displayId, mTransaction);
+ mWm.mAccessibilityController.drawMagnifiedRegionBorderIfNeeded(displayId);
// Turn off magnification to release surface.
mWm.mAccessibilityController.setMagnificationCallbacks(displayId, null);
- if (!com.android.window.flags.Flags.drawMagnifierBorderOutsideWmlock()) {
- verify(surface).release();
- assertTrue(lockCanvasInWmLock[0]);
- return;
- }
waitUntilHandlersIdle();
// lockCanvas must not be called after releasing.
verify(surface, never()).lockCanvas(any());
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
index 114b9c3..a0bafb6 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
@@ -20,6 +20,7 @@
import static android.app.WindowConfiguration.ACTIVITY_TYPE_DREAM;
import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
import static android.app.WindowConfiguration.ROTATION_UNDEFINED;
+import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
@@ -133,7 +134,9 @@
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
+import java.util.ArrayList;
import java.util.HashMap;
+import java.util.List;
/** Common base class for window manager unit test classes. */
class WindowTestsBase extends SystemServiceTestsBase {
@@ -226,7 +229,7 @@
mDefaultDisplay = mWm.mRoot.getDefaultDisplay();
// Update the display policy to make the screen fully turned on so animation is allowed
final DisplayPolicy displayPolicy = mDefaultDisplay.getDisplayPolicy();
- displayPolicy.screenTurnedOn(null /* screenOnListener */);
+ displayPolicy.screenTurningOn(null /* screenOnListener */);
displayPolicy.finishKeyguardDrawn();
displayPolicy.finishWindowsDrawn();
displayPolicy.finishScreenTurningOn();
@@ -1892,6 +1895,55 @@
}
}
+ static class TestDesktopOrganizer extends WindowOrganizerTests.StubOrganizer {
+ final int mDesktopModeDefaultWidthDp = 840;
+ final int mDesktopModeDefaultHeightDp = 630;
+ final int mDesktopDensity = 284;
+
+ final ActivityTaskManagerService mService;
+ final TaskDisplayArea mDefaultTDA;
+ List<Task> mTasks;
+ final DisplayContent mDisplay;
+ Rect mStableBounds;
+
+ TestDesktopOrganizer(ActivityTaskManagerService service, DisplayContent display) {
+ mService = service;
+ mDefaultTDA = display.getDefaultTaskDisplayArea();
+ mDisplay = display;
+ mService.mTaskOrganizerController.registerTaskOrganizer(this);
+ mTasks = new ArrayList<>();
+ mStableBounds = display.getBounds();
+ }
+
+ TestDesktopOrganizer(ActivityTaskManagerService service) {
+ this(service, service.mTaskSupervisor.mRootWindowContainer.getDefaultDisplay());
+ }
+
+ public Task createTask(Rect bounds) {
+ Task task = mService.mTaskOrganizerController.createRootTask(
+ mDisplay, WINDOWING_MODE_FREEFORM, null);
+ task.setBounds(bounds);
+ mTasks.add(task);
+ spyOn(task);
+ return task;
+ }
+
+ public Rect getDefaultDesktopTaskBounds() {
+ int width = (int) (mDesktopModeDefaultWidthDp * mDesktopDensity + 0.5f);
+ int height = (int) (mDesktopModeDefaultHeightDp * mDesktopDensity + 0.5f);
+ Rect outBounds = new Rect();
+
+ outBounds.set(0, 0, width, height);
+ // Center the task in stable bounds
+ outBounds.offset(
+ mStableBounds.centerX() - outBounds.centerX(),
+ mStableBounds.centerY() - outBounds.centerY()
+ );
+ return outBounds;
+ }
+
+ }
+
static TestWindowToken createTestWindowToken(int type, DisplayContent dc) {
return createTestWindowToken(type, dc, false /* persistOnEmpty */);
}
diff --git a/telecomm/java/android/telecom/CallControl.java b/telecomm/java/android/telecom/CallControl.java
index fe699af..a1407869 100644
--- a/telecomm/java/android/telecom/CallControl.java
+++ b/telecomm/java/android/telecom/CallControl.java
@@ -21,7 +21,6 @@
import android.annotation.CallbackExecutor;
import android.annotation.FlaggedApi;
import android.annotation.NonNull;
-import android.annotation.Nullable;
import android.annotation.SuppressLint;
import android.os.Binder;
import android.os.Bundle;
@@ -31,7 +30,6 @@
import android.os.ResultReceiver;
import android.text.TextUtils;
-import com.android.internal.telecom.ClientTransactionalServiceRepository;
import com.android.internal.telecom.ICallControl;
import com.android.server.telecom.flags.Flags;
@@ -52,20 +50,13 @@
@SuppressLint("NotCloseable")
public final class CallControl {
private static final String TAG = CallControl.class.getSimpleName();
- private static final String INTERFACE_ERROR_MSG = "Call Control is not available";
private final String mCallId;
private final ICallControl mServerInterface;
- private final PhoneAccountHandle mPhoneAccountHandle;
- private final ClientTransactionalServiceRepository mRepository;
/** @hide */
- public CallControl(@NonNull String callId, @Nullable ICallControl serverInterface,
- @NonNull ClientTransactionalServiceRepository repository,
- @NonNull PhoneAccountHandle pah) {
+ public CallControl(@NonNull String callId, @NonNull ICallControl serverInterface) {
mCallId = callId;
mServerInterface = serverInterface;
- mRepository = repository;
- mPhoneAccountHandle = pah;
}
/**
@@ -97,16 +88,14 @@
*/
public void setActive(@CallbackExecutor @NonNull Executor executor,
@NonNull OutcomeReceiver<Void, CallException> callback) {
- if (mServerInterface != null) {
- try {
- mServerInterface.setActive(mCallId,
- new CallControlResultReceiver("setActive", executor, callback));
+ Objects.requireNonNull(executor);
+ Objects.requireNonNull(callback);
+ try {
+ mServerInterface.setActive(mCallId,
+ new CallControlResultReceiver("setActive", executor, callback));
- } catch (RemoteException e) {
- throw e.rethrowAsRuntimeException();
- }
- } else {
- throw new IllegalStateException(INTERFACE_ERROR_MSG);
+ } catch (RemoteException e) {
+ throw e.rethrowAsRuntimeException();
}
}
@@ -134,16 +123,12 @@
validateVideoState(videoState);
Objects.requireNonNull(executor);
Objects.requireNonNull(callback);
- if (mServerInterface != null) {
- try {
- mServerInterface.answer(videoState, mCallId,
- new CallControlResultReceiver("answer", executor, callback));
+ try {
+ mServerInterface.answer(videoState, mCallId,
+ new CallControlResultReceiver("answer", executor, callback));
- } catch (RemoteException e) {
- throw e.rethrowAsRuntimeException();
- }
- } else {
- throw new IllegalStateException(INTERFACE_ERROR_MSG);
+ } catch (RemoteException e) {
+ throw e.rethrowAsRuntimeException();
}
}
@@ -165,16 +150,14 @@
*/
public void setInactive(@CallbackExecutor @NonNull Executor executor,
@NonNull OutcomeReceiver<Void, CallException> callback) {
- if (mServerInterface != null) {
- try {
- mServerInterface.setInactive(mCallId,
- new CallControlResultReceiver("setInactive", executor, callback));
+ Objects.requireNonNull(executor);
+ Objects.requireNonNull(callback);
+ try {
+ mServerInterface.setInactive(mCallId,
+ new CallControlResultReceiver("setInactive", executor, callback));
- } catch (RemoteException e) {
- throw e.rethrowAsRuntimeException();
- }
- } else {
- throw new IllegalStateException(INTERFACE_ERROR_MSG);
+ } catch (RemoteException e) {
+ throw e.rethrowAsRuntimeException();
}
}
@@ -213,15 +196,11 @@
Objects.requireNonNull(executor);
Objects.requireNonNull(callback);
validateDisconnectCause(disconnectCause);
- if (mServerInterface != null) {
- try {
- mServerInterface.disconnect(mCallId, disconnectCause,
- new CallControlResultReceiver("disconnect", executor, callback));
- } catch (RemoteException e) {
- throw e.rethrowAsRuntimeException();
- }
- } else {
- throw new IllegalStateException(INTERFACE_ERROR_MSG);
+ try {
+ mServerInterface.disconnect(mCallId, disconnectCause,
+ new CallControlResultReceiver("disconnect", executor, callback));
+ } catch (RemoteException e) {
+ throw e.rethrowAsRuntimeException();
}
}
@@ -245,15 +224,13 @@
*/
public void startCallStreaming(@CallbackExecutor @NonNull Executor executor,
@NonNull OutcomeReceiver<Void, CallException> callback) {
- if (mServerInterface != null) {
- try {
- mServerInterface.startCallStreaming(mCallId,
- new CallControlResultReceiver("startCallStreaming", executor, callback));
- } catch (RemoteException e) {
- throw e.rethrowAsRuntimeException();
- }
- } else {
- throw new IllegalStateException(INTERFACE_ERROR_MSG);
+ Objects.requireNonNull(executor);
+ Objects.requireNonNull(callback);
+ try {
+ mServerInterface.startCallStreaming(mCallId,
+ new CallControlResultReceiver("startCallStreaming", executor, callback));
+ } catch (RemoteException e) {
+ throw e.rethrowAsRuntimeException();
}
}
@@ -281,15 +258,11 @@
Objects.requireNonNull(callEndpoint);
Objects.requireNonNull(executor);
Objects.requireNonNull(callback);
- if (mServerInterface != null) {
- try {
- mServerInterface.requestCallEndpointChange(callEndpoint,
- new CallControlResultReceiver("endpointChange", executor, callback));
- } catch (RemoteException e) {
- throw e.rethrowAsRuntimeException();
- }
- } else {
- throw new IllegalStateException(INTERFACE_ERROR_MSG);
+ try {
+ mServerInterface.requestCallEndpointChange(callEndpoint,
+ new CallControlResultReceiver("requestCallEndpointChange", executor, callback));
+ } catch (RemoteException e) {
+ throw e.rethrowAsRuntimeException();
}
}
@@ -313,20 +286,16 @@
* passed that details why the operation failed.
*/
@FlaggedApi(Flags.FLAG_SET_MUTE_STATE)
- public void setMuteState(boolean isMuted, @CallbackExecutor @NonNull Executor executor,
+ public void requestMuteState(boolean isMuted, @CallbackExecutor @NonNull Executor executor,
@NonNull OutcomeReceiver<Void, CallException> callback) {
Objects.requireNonNull(executor);
Objects.requireNonNull(callback);
- if (mServerInterface != null) {
- try {
- mServerInterface.setMuteState(isMuted,
- new CallControlResultReceiver("setMuteState", executor, callback));
+ try {
+ mServerInterface.setMuteState(isMuted,
+ new CallControlResultReceiver("requestMuteState", executor, callback));
- } catch (RemoteException e) {
- throw e.rethrowAsRuntimeException();
- }
- } else {
- throw new IllegalStateException(INTERFACE_ERROR_MSG);
+ } catch (RemoteException e) {
+ throw e.rethrowAsRuntimeException();
}
}
@@ -352,14 +321,10 @@
public void sendEvent(@NonNull String event, @NonNull Bundle extras) {
Objects.requireNonNull(event);
Objects.requireNonNull(extras);
- if (mServerInterface != null) {
- try {
- mServerInterface.sendEvent(mCallId, event, extras);
- } catch (RemoteException e) {
- throw e.rethrowAsRuntimeException();
- }
- } else {
- throw new IllegalStateException(INTERFACE_ERROR_MSG);
+ try {
+ mServerInterface.sendEvent(mCallId, event, extras);
+ } catch (RemoteException e) {
+ throw e.rethrowAsRuntimeException();
}
}
diff --git a/telecomm/java/android/telecom/PhoneAccount.java b/telecomm/java/android/telecom/PhoneAccount.java
index a089f5c..63db297 100644
--- a/telecomm/java/android/telecom/PhoneAccount.java
+++ b/telecomm/java/android/telecom/PhoneAccount.java
@@ -580,6 +580,9 @@
mExtras = phoneAccount.getExtras();
mGroupId = phoneAccount.getGroupId();
mSupportedAudioRoutes = phoneAccount.getSupportedAudioRoutes();
+ if (phoneAccount.hasSimultaneousCallingRestriction()) {
+ mSimultaneousCallingRestriction = phoneAccount.getSimultaneousCallingRestriction();
+ }
}
/**
diff --git a/telecomm/java/android/telecom/TelecomManager.java b/telecomm/java/android/telecom/TelecomManager.java
index e81f482..2c6e1e4 100644
--- a/telecomm/java/android/telecom/TelecomManager.java
+++ b/telecomm/java/android/telecom/TelecomManager.java
@@ -1360,7 +1360,7 @@
/**
* Returns a list of {@link PhoneAccountHandle}s which can be used to make and receive phone
* calls. The returned list includes those accounts which have been explicitly enabled by
- * the user or other users visible to the user.
+ * the user or enabled by other users but visible to the user.
*
* @see #EXTRA_PHONE_ACCOUNT_HANDLE
* @return A list of {@code PhoneAccountHandle} objects.
@@ -1486,7 +1486,7 @@
return service.getCallCapablePhoneAccounts(includeDisabledAccounts,
mContext.getOpPackageName(), mContext.getAttributionTag(), true).getList();
} catch (RemoteException e) {
- throw e.rethrowAsRuntimeException();
+ throw e.rethrowFromSystemServer();
}
}
diff --git a/telecomm/java/com/android/internal/telecom/ClientTransactionalServiceWrapper.java b/telecomm/java/com/android/internal/telecom/ClientTransactionalServiceWrapper.java
index 71e9184..467e89c 100644
--- a/telecomm/java/com/android/internal/telecom/ClientTransactionalServiceWrapper.java
+++ b/telecomm/java/com/android/internal/telecom/ClientTransactionalServiceWrapper.java
@@ -208,8 +208,7 @@
if (resultCode == TELECOM_TRANSACTION_SUCCESS) {
// create the interface object that the client will interact with
- CallControl control = new CallControl(callId, callControl, mRepository,
- mPhoneAccountHandle);
+ CallControl control = new CallControl(callId, callControl);
// give the client the object via the OR that was passed into addCall
pendingControl.onResult(control);
diff --git a/telephony/java/android/telephony/CarrierConfigManager.java b/telephony/java/android/telephony/CarrierConfigManager.java
index 1badf67..a73c46b 100644
--- a/telephony/java/android/telephony/CarrierConfigManager.java
+++ b/telephony/java/android/telephony/CarrierConfigManager.java
@@ -9430,16 +9430,6 @@
"missed_incoming_call_sms_originator_string_array";
/**
- * String array of Apn Type configurations.
- * The entries should be of form "APN_TYPE_NAME:priority".
- * priority is an integer that is sorted from highest to lowest.
- * example: cbs:5
- *
- * @hide
- */
- public static final String KEY_APN_PRIORITY_STRING_ARRAY = "apn_priority_string_array";
-
- /**
* Network capability priority for determine the satisfy order in telephony. The priority is
* from the lowest 0 to the highest 100. The long-lived network shall have the lowest priority.
* This allows other short-lived requests like MMS requests to be established. Emergency request
@@ -10755,17 +10745,14 @@
TimeUnit.DAYS.toMillis(1));
sDefaults.putStringArray(KEY_MISSED_INCOMING_CALL_SMS_ORIGINATOR_STRING_ARRAY,
new String[0]);
- sDefaults.putStringArray(KEY_APN_PRIORITY_STRING_ARRAY, new String[] {
- "enterprise:0", "default:1", "mms:2", "supl:2", "dun:2", "hipri:3", "fota:2",
- "ims:2", "cbs:2", "ia:2", "emergency:2", "mcx:3", "xcap:3"
- });
// Do not modify the priority unless you know what you are doing. This will have significant
// impacts on the order of data network setup.
sDefaults.putStringArray(
KEY_TELEPHONY_NETWORK_CAPABILITY_PRIORITIES_STRING_ARRAY, new String[] {
"eims:90", "supl:80", "mms:70", "xcap:70", "cbs:50", "mcx:50", "fota:50",
- "ims:40", "dun:30", "enterprise:20", "internet:20"
+ "ims:40", "rcs:40", "dun:30", "enterprise:20", "internet:20",
+ "prioritize_bandwidth:20", "prioritize_latency:20"
});
sDefaults.putStringArray(
KEY_TELEPHONY_DATA_SETUP_RETRY_RULES_STRING_ARRAY, new String[] {
@@ -10777,9 +10764,10 @@
// registration state changes) retry can still happen.
"permanent_fail_causes=8|27|28|29|30|32|33|35|50|51|111|-5|-6|65537|65538|"
+ "-3|65543|65547|2252|2253|2254, retry_interval=2500",
- "capabilities=mms|supl|cbs, retry_interval=2000",
- "capabilities=internet|enterprise|dun|ims|fota, retry_interval=2500|3000|"
- + "5000|10000|15000|20000|40000|60000|120000|240000|"
+ "capabilities=mms|supl|cbs|rcs, retry_interval=2000",
+ "capabilities=internet|enterprise|dun|ims|fota|xcap|mcx|"
+ + "prioritize_bandwidth|prioritize_latency, retry_interval="
+ + "2500|3000|5000|10000|15000|20000|40000|60000|120000|240000|"
+ "600000|1200000|1800000, maximum_retries=20"
});
sDefaults.putStringArray(
diff --git a/telephony/java/android/telephony/SubscriptionManager.java b/telephony/java/android/telephony/SubscriptionManager.java
index a5c6d57..1bf11df 100644
--- a/telephony/java/android/telephony/SubscriptionManager.java
+++ b/telephony/java/android/telephony/SubscriptionManager.java
@@ -1618,14 +1618,15 @@
}
/**
- * Register for changes to the list of active {@link SubscriptionInfo} records or to the
- * individual records themselves. When a change occurs the onSubscriptionsChanged method of
- * the listener will be invoked immediately if there has been a notification. The
- * onSubscriptionChanged method will also be triggered once initially when calling this
- * function.
+ * Register for changes to the list of {@link SubscriptionInfo} records or to the
+ * individual records (active or inactive) themselves. When a change occurs, the
+ * {@link OnSubscriptionsChangedListener#onSubscriptionsChanged()} method of
+ * the listener will be invoked immediately. The
+ * {@link OnSubscriptionsChangedListener#onSubscriptionsChanged()} method will also be invoked
+ * once initially when calling this method.
*
* @param listener an instance of {@link OnSubscriptionsChangedListener} with
- * onSubscriptionsChanged overridden.
+ * {@link OnSubscriptionsChangedListener#onSubscriptionsChanged()} overridden.
* @param executor the executor that will execute callbacks.
*/
public void addOnSubscriptionsChangedListener(
@@ -1953,7 +1954,6 @@
* {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}.
*/
@RequiresPermission(android.Manifest.permission.READ_PHONE_STATE)
- // @RequiresPermission(TODO(b/308809058))
public List<SubscriptionInfo> getActiveSubscriptionInfoList() {
List<SubscriptionInfo> activeList = null;
@@ -2010,6 +2010,9 @@
* Create a new subscription manager instance that can see all subscriptions across
* user profiles.
*
+ * The permission check for accessing all subscriptions will be enforced upon calling the
+ * individual APIs linked below.
+ *
* @return a SubscriptionManager that can see all subscriptions regardless its user profile
* association.
*
@@ -2018,9 +2021,7 @@
* @see UserHandle
*/
@FlaggedApi(Flags.FLAG_ENFORCE_SUBSCRIPTION_USER_FILTER)
- // @RequiresPermission(TODO(b/308809058))
- // The permission check for accessing all subscriptions will be enforced upon calling the
- // individual APIs linked above.
+ @RequiresPermission(android.Manifest.permission.INTERACT_ACROSS_PROFILES)
@NonNull public SubscriptionManager createForAllUserProfiles() {
return new SubscriptionManager(mContext, true/*isForAllUserProfiles*/);
}
@@ -2215,7 +2216,6 @@
* {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}.
*/
@RequiresPermission(android.Manifest.permission.READ_PHONE_STATE)
- // @RequiresPermission(TODO(b/308809058))
public int getActiveSubscriptionInfoCount() {
int result = 0;
diff --git a/telephony/java/android/telephony/TelephonyManager.java b/telephony/java/android/telephony/TelephonyManager.java
index 9ec5f7a..cbd5524 100644
--- a/telephony/java/android/telephony/TelephonyManager.java
+++ b/telephony/java/android/telephony/TelephonyManager.java
@@ -6888,6 +6888,7 @@
}
}
+ // TODO(b/316183370): replace all @code with @link in javadoc after feature is released
/**
* @return true if the current device is "voice capable".
* <p>
@@ -6901,7 +6902,10 @@
* PackageManager.FEATURE_TELEPHONY system feature, which is available
* on any device with a telephony radio, even if the device is
* data-only.
- * @deprecated Replaced by {@link #isDeviceVoiceCapable()}
+ * @deprecated Replaced by {@code #isDeviceVoiceCapable()}. Starting from Android 15, voice
+ * capability may also be overridden by carriers for a given subscription. For voice capable
+ * device (when {@code #isDeviceVoiceCapable} return {@code true}), caller should check for
+ * subscription-level voice capability as well. See {@code #isDeviceVoiceCapable} for details.
*/
@RequiresFeature(PackageManager.FEATURE_TELEPHONY_CALLING)
@Deprecated
@@ -6923,9 +6927,10 @@
* .FEATURE_TELEPHONY system feature, which is available on any device with a telephony
* radio, even if the device is data-only.
* <p>
- * To check if a subscription is "voice capable", call method
- * {@link SubscriptionInfo#getServiceCapabilities()} and compare the result with
- * bitmask {@link SubscriptionManager#SERVICE_CAPABILITY_VOICE}.
+ * Starting from Android 15, voice capability may also be overridden by carrier for a given
+ * subscription on a voice capable device. To check if a subscription is "voice capable",
+ * call method {@code SubscriptionInfo#getServiceCapabilities()} and check if
+ * {@code SubscriptionManager#SERVICE_CAPABILITY_VOICE} is included.
*
* @see SubscriptionInfo#getServiceCapabilities()
*/
@@ -6943,7 +6948,10 @@
* <p>
* Note: Voicemail waiting sms, cell broadcasting sms, and MMS are
* disabled when device doesn't support sms.
- * @deprecated Replaced by {@link #isDeviceSmsCapable()}
+ * @deprecated Replaced by {@code #isDeviceSmsCapable()}. Starting from Android 15, SMS
+ * capability may also be overridden by carriers for a given subscription. For SMS capable
+ * device (when {@code #isDeviceSmsCapable} return {@code true}), caller should check for
+ * subscription-level SMS capability as well. See {@code #isDeviceSmsCapable} for details.
*/
@RequiresFeature(PackageManager.FEATURE_TELEPHONY_MESSAGING)
public boolean isSmsCapable() {
@@ -6961,9 +6969,10 @@
* Note: Voicemail waiting SMS, cell broadcasting SMS, and MMS are
* disabled when device doesn't support SMS.
* <p>
- * To check if a subscription is "SMS capable", call method
- * {@link SubscriptionInfo#getServiceCapabilities()} and compare result with
- * bitmask {@link SubscriptionManager#SERVICE_CAPABILITY_SMS}.
+ * Starting from Android 15, SMS capability may also be overridden by carriers for a given
+ * subscription on an SMS capable device. To check if a subscription is "SMS capable",
+ * call method {@code SubscriptionInfo#getServiceCapabilities()} and check if
+ * {@code SubscriptionManager#SERVICE_CAPABILITY_SMS} is included.
*
* @see SubscriptionInfo#getServiceCapabilities()
*/
@@ -13139,7 +13148,7 @@
*/
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
public ServiceState getServiceStateForSubscriber(int subId) {
- return getServiceStateForSubscriber(getSubId(), false, false);
+ return getServiceStateForSubscriber(subId, false, false);
}
/**
@@ -18483,7 +18492,6 @@
/**
* Get last known cell identity.
- * Require appropriate permissions, otherwise throws SecurityException.
*
* If there is current registered network this value will be same as the registered cell
* identity. If the device goes out of service the previous cell identity is cached and
@@ -18495,7 +18503,7 @@
@SystemApi
@FlaggedApi(com.android.server.telecom.flags.Flags.FLAG_TELECOM_RESOLVE_HIDDEN_DEPENDENCIES)
@RequiresPermission(allOf = {Manifest.permission.ACCESS_FINE_LOCATION,
- "com.android.phone.permission.ACCESS_LAST_KNOWN_CELL_ID"})
+ Manifest.permission.ACCESS_LAST_KNOWN_CELL_ID})
public @Nullable CellIdentity getLastKnownCellIdentity() {
try {
ITelephony telephony = getITelephony();
diff --git a/telephony/java/android/telephony/satellite/SatelliteManager.java b/telephony/java/android/telephony/satellite/SatelliteManager.java
index 70047a6..a1ac477 100644
--- a/telephony/java/android/telephony/satellite/SatelliteManager.java
+++ b/telephony/java/android/telephony/satellite/SatelliteManager.java
@@ -34,7 +34,6 @@
import android.os.OutcomeReceiver;
import android.os.RemoteException;
import android.os.ResultReceiver;
-import android.os.ServiceSpecificException;
import android.telephony.SubscriptionManager;
import android.telephony.TelephonyCallback;
import android.telephony.TelephonyFrameworkInitializer;
@@ -336,6 +335,12 @@
@FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
public static final int SATELLITE_RESULT_MODEM_BUSY = 22;
+ /**
+ * Telephony process is not currently available or satellite is not supported.
+ */
+ @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
+ public static final int SATELLITE_RESULT_ILLEGAL_STATE = 23;
+
/** @hide */
@IntDef(prefix = {"SATELLITE_RESULT_"}, value = {
SATELLITE_RESULT_SUCCESS,
@@ -360,7 +365,8 @@
SATELLITE_RESULT_NOT_AUTHORIZED,
SATELLITE_RESULT_NOT_SUPPORTED,
SATELLITE_RESULT_REQUEST_IN_PROGRESS,
- SATELLITE_RESULT_MODEM_BUSY
+ SATELLITE_RESULT_MODEM_BUSY,
+ SATELLITE_RESULT_ILLEGAL_STATE
})
@Retention(RetentionPolicy.SOURCE)
public @interface SatelliteResult {}
@@ -510,7 +516,7 @@
}
} catch (RemoteException ex) {
Rlog.e(TAG, "requestSatelliteEnabled() RemoteException: ", ex);
- ex.rethrowFromSystemServer();
+ ex.rethrowAsRuntimeException();
}
}
@@ -526,7 +532,6 @@
* will return a {@link SatelliteException} with the {@link SatelliteResult}.
*
* @throws SecurityException if the caller doesn't have required permission.
- * @throws IllegalStateException if the Telephony process is not currently available.
*/
@RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION)
@FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
@@ -561,11 +566,12 @@
};
telephony.requestIsSatelliteEnabled(mSubId, receiver);
} else {
- throw new IllegalStateException("telephony service is null.");
+ executor.execute(() -> Binder.withCleanCallingIdentity(() -> callback.onError(
+ new SatelliteException(SATELLITE_RESULT_ILLEGAL_STATE))));
}
} catch (RemoteException ex) {
loge("requestIsSatelliteEnabled() RemoteException: " + ex);
- ex.rethrowFromSystemServer();
+ ex.rethrowAsRuntimeException();
}
}
@@ -581,7 +587,6 @@
* will return a {@link SatelliteException} with the {@link SatelliteResult}.
*
* @throws SecurityException if the caller doesn't have required permission.
- * @throws IllegalStateException if the Telephony process is not currently available.
*/
@RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION)
@FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
@@ -616,11 +621,12 @@
};
telephony.requestIsDemoModeEnabled(mSubId, receiver);
} else {
- throw new IllegalStateException("telephony service is null.");
+ executor.execute(() -> Binder.withCleanCallingIdentity(() -> callback.onError(
+ new SatelliteException(SATELLITE_RESULT_ILLEGAL_STATE))));
}
} catch (RemoteException ex) {
loge("requestIsDemoModeEnabled() RemoteException: " + ex);
- ex.rethrowFromSystemServer();
+ ex.rethrowAsRuntimeException();
}
}
@@ -639,8 +645,6 @@
* service is supported on the device and {@code false} otherwise.
* If the request is not successful, {@link OutcomeReceiver#onError(Throwable)}
* will return a {@link SatelliteException} with the {@link SatelliteResult}.
- *
- * @throws IllegalStateException if the Telephony process is not currently available.
*/
@FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
public void requestIsSatelliteSupported(@NonNull @CallbackExecutor Executor executor,
@@ -674,11 +678,12 @@
};
telephony.requestIsSatelliteSupported(mSubId, receiver);
} else {
- throw new IllegalStateException("telephony service is null.");
+ executor.execute(() -> Binder.withCleanCallingIdentity(() -> callback.onError(
+ new SatelliteException(SATELLITE_RESULT_ILLEGAL_STATE))));
}
} catch (RemoteException ex) {
loge("requestIsSatelliteSupported() RemoteException: " + ex);
- ex.rethrowFromSystemServer();
+ ex.rethrowAsRuntimeException();
}
}
@@ -693,7 +698,6 @@
* will return a {@link SatelliteException} with the {@link SatelliteResult}.
*
* @throws SecurityException if the caller doesn't have required permission.
- * @throws IllegalStateException if the Telephony process is not currently available.
*/
@RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION)
@FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
@@ -729,11 +733,12 @@
};
telephony.requestSatelliteCapabilities(mSubId, receiver);
} else {
- throw new IllegalStateException("telephony service is null.");
+ executor.execute(() -> Binder.withCleanCallingIdentity(() -> callback.onError(
+ new SatelliteException(SATELLITE_RESULT_ILLEGAL_STATE))));
}
} catch (RemoteException ex) {
loge("requestSatelliteCapabilities() RemoteException: " + ex);
- ex.rethrowFromSystemServer();
+ ex.rethrowAsRuntimeException();
}
}
@@ -959,7 +964,6 @@
* @param callback The callback to notify of satellite transmission updates.
*
* @throws SecurityException if the caller doesn't have required permission.
- * @throws IllegalStateException if the Telephony process is not currently available.
*/
@RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION)
@FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
@@ -1009,11 +1013,12 @@
telephony.startSatelliteTransmissionUpdates(mSubId, errorCallback,
internalCallback);
} else {
- throw new IllegalStateException("telephony service is null.");
+ executor.execute(() -> Binder.withCleanCallingIdentity(
+ () -> resultListener.accept(SATELLITE_RESULT_ILLEGAL_STATE)));
}
} catch (RemoteException ex) {
loge("startSatelliteTransmissionUpdates() RemoteException: " + ex);
- ex.rethrowFromSystemServer();
+ ex.rethrowAsRuntimeException();
}
}
@@ -1029,7 +1034,6 @@
* @param resultListener Listener for the {@link SatelliteResult} result of the operation.
*
* @throws SecurityException if the caller doesn't have required permission.
- * @throws IllegalStateException if the Telephony process is not currently available.
*/
@RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION)
@FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
@@ -1063,11 +1067,12 @@
() -> resultListener.accept(SATELLITE_RESULT_INVALID_ARGUMENTS)));
}
} else {
- throw new IllegalStateException("telephony service is null.");
+ executor.execute(() -> Binder.withCleanCallingIdentity(
+ () -> resultListener.accept(SATELLITE_RESULT_ILLEGAL_STATE)));
}
} catch (RemoteException ex) {
loge("stopSatelliteTransmissionUpdates() RemoteException: " + ex);
- ex.rethrowFromSystemServer();
+ ex.rethrowAsRuntimeException();
}
}
@@ -1112,11 +1117,12 @@
cancelRemote = telephony.provisionSatelliteService(mSubId, token, provisionData,
errorCallback);
} else {
- throw new IllegalStateException("telephony service is null.");
+ executor.execute(() -> Binder.withCleanCallingIdentity(
+ () -> resultListener.accept(SATELLITE_RESULT_ILLEGAL_STATE)));
}
} catch (RemoteException ex) {
loge("provisionSatelliteService() RemoteException=" + ex);
- ex.rethrowFromSystemServer();
+ ex.rethrowAsRuntimeException();
}
if (cancellationSignal != null) {
cancellationSignal.setRemote(cancelRemote);
@@ -1138,7 +1144,6 @@
* @param resultListener Listener for the {@link SatelliteResult} result of the operation.
*
* @throws SecurityException if the caller doesn't have required permission.
- * @throws IllegalStateException if the Telephony process is not currently available.
*/
@RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION)
@FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
@@ -1161,11 +1166,12 @@
};
telephony.deprovisionSatelliteService(mSubId, token, errorCallback);
} else {
- throw new IllegalStateException("telephony service is null.");
+ executor.execute(() -> Binder.withCleanCallingIdentity(
+ () -> resultListener.accept(SATELLITE_RESULT_ILLEGAL_STATE)));
}
} catch (RemoteException ex) {
loge("deprovisionSatelliteService() RemoteException=" + ex);
- ex.rethrowFromSystemServer();
+ ex.rethrowAsRuntimeException();
}
}
@@ -1208,7 +1214,7 @@
}
} catch (RemoteException ex) {
loge("registerForSatelliteProvisionStateChanged() RemoteException: " + ex);
- ex.rethrowFromSystemServer();
+ ex.rethrowAsRuntimeException();
}
return SATELLITE_RESULT_REQUEST_FAILED;
}
@@ -1244,7 +1250,7 @@
}
} catch (RemoteException ex) {
loge("unregisterForSatelliteProvisionStateChanged() RemoteException: " + ex);
- ex.rethrowFromSystemServer();
+ ex.rethrowAsRuntimeException();
}
}
@@ -1260,7 +1266,6 @@
* will return a {@link SatelliteException} with the {@link SatelliteResult}.
*
* @throws SecurityException if the caller doesn't have required permission.
- * @throws IllegalStateException if the Telephony process is not currently available.
*/
@RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION)
@FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
@@ -1295,11 +1300,12 @@
};
telephony.requestIsSatelliteProvisioned(mSubId, receiver);
} else {
- throw new IllegalStateException("telephony service is null.");
+ executor.execute(() -> Binder.withCleanCallingIdentity(() -> callback.onError(
+ new SatelliteException(SATELLITE_RESULT_ILLEGAL_STATE))));
}
} catch (RemoteException ex) {
loge("requestIsSatelliteProvisioned() RemoteException: " + ex);
- ex.rethrowFromSystemServer();
+ ex.rethrowAsRuntimeException();
}
}
@@ -1340,7 +1346,7 @@
}
} catch (RemoteException ex) {
loge("registerForSatelliteModemStateChanged() RemoteException:" + ex);
- ex.rethrowFromSystemServer();
+ ex.rethrowAsRuntimeException();
}
return SATELLITE_RESULT_REQUEST_FAILED;
}
@@ -1376,7 +1382,7 @@
}
} catch (RemoteException ex) {
loge("unregisterForSatelliteModemStateChanged() RemoteException:" + ex);
- ex.rethrowFromSystemServer();
+ ex.rethrowAsRuntimeException();
}
}
@@ -1436,7 +1442,7 @@
}
} catch (RemoteException ex) {
loge("registerForSatelliteDatagram() RemoteException:" + ex);
- ex.rethrowFromSystemServer();
+ ex.rethrowAsRuntimeException();
}
return SATELLITE_RESULT_REQUEST_FAILED;
}
@@ -1471,7 +1477,7 @@
}
} catch (RemoteException ex) {
loge("unregisterForSatelliteDatagram() RemoteException:" + ex);
- ex.rethrowFromSystemServer();
+ ex.rethrowAsRuntimeException();
}
}
@@ -1488,7 +1494,6 @@
* @param resultListener Listener for the {@link SatelliteResult} result of the operation.
*
* @throws SecurityException if the caller doesn't have required permission.
- * @throws IllegalStateException if the Telephony process is not currently available.
*/
@RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION)
@FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
@@ -1509,11 +1514,12 @@
};
telephony.pollPendingSatelliteDatagrams(mSubId, internalCallback);
} else {
- throw new IllegalStateException("telephony service is null.");
+ executor.execute(() -> Binder.withCleanCallingIdentity(
+ () -> resultListener.accept(SATELLITE_RESULT_ILLEGAL_STATE)));
}
} catch (RemoteException ex) {
loge("pollPendingSatelliteDatagrams() RemoteException:" + ex);
- ex.rethrowFromSystemServer();
+ ex.rethrowAsRuntimeException();
}
}
@@ -1541,7 +1547,6 @@
* @param resultListener Listener for the {@link SatelliteResult} result of the operation.
*
* @throws SecurityException if the caller doesn't have required permission.
- * @throws IllegalStateException if the Telephony process is not currently available.
*/
@RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION)
@FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
@@ -1566,11 +1571,12 @@
telephony.sendSatelliteDatagram(mSubId, datagramType, datagram,
needFullScreenPointingUI, internalCallback);
} else {
- throw new IllegalStateException("telephony service is null.");
+ executor.execute(() -> Binder.withCleanCallingIdentity(
+ () -> resultListener.accept(SATELLITE_RESULT_ILLEGAL_STATE)));
}
} catch (RemoteException ex) {
loge("sendSatelliteDatagram() RemoteException:" + ex);
- ex.rethrowFromSystemServer();
+ ex.rethrowAsRuntimeException();
}
}
@@ -1587,7 +1593,6 @@
* will return a {@link SatelliteException} with the {@link SatelliteResult}.
*
* @throws SecurityException if the caller doesn't have required permission.
- * @throws IllegalStateException if the Telephony process is not currently available.
*/
@RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION)
@FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
@@ -1624,12 +1629,13 @@
telephony.requestIsSatelliteCommunicationAllowedForCurrentLocation(mSubId,
receiver);
} else {
- throw new IllegalStateException("telephony service is null.");
+ executor.execute(() -> Binder.withCleanCallingIdentity(() -> callback.onError(
+ new SatelliteException(SATELLITE_RESULT_ILLEGAL_STATE))));
}
} catch (RemoteException ex) {
loge("requestIsSatelliteCommunicationAllowedForCurrentLocation() RemoteException: "
+ ex);
- ex.rethrowFromSystemServer();
+ ex.rethrowAsRuntimeException();
}
}
@@ -1645,7 +1651,6 @@
* will return a {@link SatelliteException} with the {@link SatelliteResult}.
*
* @throws SecurityException if the caller doesn't have required permission.
- * @throws IllegalStateException if the Telephony process is not currently available.
*/
@RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION)
@FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
@@ -1681,11 +1686,12 @@
};
telephony.requestTimeForNextSatelliteVisibility(mSubId, receiver);
} else {
- throw new IllegalStateException("telephony service is null.");
+ executor.execute(() -> Binder.withCleanCallingIdentity(() -> callback.onError(
+ new SatelliteException(SATELLITE_RESULT_ILLEGAL_STATE))));
}
} catch (RemoteException ex) {
loge("requestTimeForNextSatelliteVisibility() RemoteException: " + ex);
- ex.rethrowFromSystemServer();
+ ex.rethrowAsRuntimeException();
}
}
@@ -1713,7 +1719,7 @@
}
} catch (RemoteException ex) {
loge("informDeviceAlignedToSatellite() RemoteException:" + ex);
- ex.rethrowFromSystemServer();
+ ex.rethrowAsRuntimeException();
}
}
@@ -1727,7 +1733,7 @@
* <ul>
* <li>Users want to enable it.</li>
* <li>There is no satellite communication restriction, which is added by
- * {@link #addSatelliteAttachRestrictionForCarrier(int, Executor, Consumer)}</li>
+ * {@link #addSatelliteAttachRestrictionForCarrier(int, int, Executor, Consumer)}</li>
* <li>The carrier config {@link
* android.telephony.CarrierConfigManager#KEY_SATELLITE_ATTACH_SUPPORTED_BOOL} is set to
* {@code true}.</li>
@@ -1739,7 +1745,6 @@
* @param resultListener Listener for the {@link SatelliteResult} result of the operation.
*
* @throws SecurityException if the caller doesn't have required permission.
- * @throws IllegalStateException if the Telephony process is not currently available.
* @throws IllegalArgumentException if the subscription is invalid.
*/
@RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION)
@@ -1799,7 +1804,6 @@
* @param resultListener Listener for the {@link SatelliteResult} result of the operation.
*
* @throws SecurityException if the caller doesn't have required permission.
- * @throws IllegalStateException if the Telephony process is not currently available.
* @throws IllegalArgumentException if the subscription is invalid.
*/
@RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION)
@@ -1824,11 +1828,12 @@
};
telephony.addSatelliteAttachRestrictionForCarrier(subId, reason, errorCallback);
} else {
- throw new IllegalStateException("telephony service is null.");
+ executor.execute(() -> Binder.withCleanCallingIdentity(
+ () -> resultListener.accept(SATELLITE_RESULT_ILLEGAL_STATE)));
}
} catch (RemoteException ex) {
loge("addSatelliteAttachRestrictionForCarrier() RemoteException:" + ex);
- ex.rethrowFromSystemServer();
+ ex.rethrowAsRuntimeException();
}
}
@@ -1842,7 +1847,6 @@
* @param resultListener Listener for the {@link SatelliteResult} result of the operation.
*
* @throws SecurityException if the caller doesn't have required permission.
- * @throws IllegalStateException if the Telephony process is not currently available.
* @throws IllegalArgumentException if the subscription is invalid.
*/
@RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION)
@@ -1867,17 +1871,18 @@
};
telephony.removeSatelliteAttachRestrictionForCarrier(subId, reason, errorCallback);
} else {
- throw new IllegalStateException("telephony service is null.");
+ executor.execute(() -> Binder.withCleanCallingIdentity(
+ () -> resultListener.accept(SATELLITE_RESULT_ILLEGAL_STATE)));
}
} catch (RemoteException ex) {
loge("removeSatelliteAttachRestrictionForCarrier() RemoteException:" + ex);
- ex.rethrowFromSystemServer();
+ ex.rethrowAsRuntimeException();
}
}
/**
* Get reasons for disallowing satellite attach, as requested by
- * {@link #addSatelliteAttachRestrictionForCarrier(int, Executor, Consumer)}
+ * {@link #addSatelliteAttachRestrictionForCarrier(int, int, Executor, Consumer)}
*
* @param subId The subscription ID of the carrier.
* @return Set of reasons for disallowing satellite communication.
@@ -1910,7 +1915,7 @@
}
} catch (RemoteException ex) {
loge("getSatelliteAttachRestrictionReasonsForCarrier() RemoteException: " + ex);
- ex.rethrowFromSystemServer();
+ ex.rethrowAsRuntimeException();
}
return new HashSet<>();
}
@@ -1932,11 +1937,12 @@
* The {@link NtnSignalStrength#NTN_SIGNAL_STRENGTH_NONE} will be returned if there is no
* signal strength data available.
* If the request is not successful, {@link OutcomeReceiver#onError(Throwable)} will return a
- * {@link SatelliteException} with the {@link SatelliteResult}.
+ * {@link SatelliteException} with the {@link SatelliteResult}, or return a
+ * {@link IllegalStateException} if the Telephony process is not currently available or
+ * satellite is not supported, or return a {@link RuntimeException} when remote procedure call
+ * has failed.
*
* @throws SecurityException if the caller doesn't have required permission.
- * @throws IllegalStateException if the Telephony process is not currently available or
- * satellite is not supported.
*/
@RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION)
@FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
@@ -1972,11 +1978,12 @@
};
telephony.requestNtnSignalStrength(mSubId, receiver);
} else {
- throw new IllegalStateException("Telephony service is null.");
+ executor.execute(() -> Binder.withCleanCallingIdentity(() -> callback.onError(
+ new SatelliteException(SATELLITE_RESULT_ILLEGAL_STATE))));
}
} catch (RemoteException ex) {
loge("requestNtnSignalStrength() RemoteException: " + ex);
- ex.rethrowFromSystemServer();
+ ex.rethrowAsRuntimeException();
}
}
@@ -1997,12 +2004,11 @@
*
* @throws SecurityException if the caller doesn't have required permission.
* @throws IllegalStateException if the Telephony process is not currently available.
- * @throws SatelliteException if the callback registration operation fails.
*/
@RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION)
@FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG)
public void registerForNtnSignalStrengthChanged(@NonNull @CallbackExecutor Executor executor,
- @NonNull NtnSignalStrengthCallback callback) throws SatelliteException {
+ @NonNull NtnSignalStrengthCallback callback) {
Objects.requireNonNull(executor);
Objects.requireNonNull(callback);
@@ -2024,12 +2030,9 @@
} else {
throw new IllegalStateException("Telephony service is null.");
}
- } catch (ServiceSpecificException ex) {
- logd("registerForNtnSignalStrengthChanged() registration fails: " + ex.errorCode);
- throw new SatelliteException(ex.errorCode);
} catch (RemoteException ex) {
loge("registerForNtnSignalStrengthChanged() RemoteException: " + ex);
- ex.rethrowFromSystemServer();
+ ex.rethrowAsRuntimeException();
}
}
@@ -2072,7 +2075,7 @@
}
} catch (RemoteException ex) {
loge("unregisterForNtnSignalStrengthChanged() RemoteException: " + ex);
- ex.rethrowFromSystemServer();
+ ex.rethrowAsRuntimeException();
}
}
@@ -2113,7 +2116,7 @@
}
} catch (RemoteException ex) {
loge("registerForSatelliteCapabilitiesChanged() RemoteException: " + ex);
- ex.rethrowFromSystemServer();
+ ex.rethrowAsRuntimeException();
}
return SATELLITE_RESULT_REQUEST_FAILED;
}
@@ -2149,7 +2152,7 @@
}
} catch (RemoteException ex) {
loge("unregisterForSatelliteCapabilitiesChanged() RemoteException: " + ex);
- ex.rethrowFromSystemServer();
+ ex.rethrowAsRuntimeException();
}
}
@@ -2177,7 +2180,7 @@
}
} catch (RemoteException ex) {
loge("getAllSatellitePlmnsForCarrier() RemoteException: " + ex);
- ex.rethrowFromSystemServer();
+ ex.rethrowAsRuntimeException();
}
return new ArrayList<>();
}
diff --git a/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt b/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt
index 3b217bc..566e51a 100644
--- a/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt
+++ b/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt
@@ -304,14 +304,16 @@
setVirtualMousePointerDisplayIdAndVerify(10)
localService.setPointerIconVisible(false, 10)
+ verify(native).setPointerIconVisibility(10, false)
verify(native).setPointerIconType(eq(PointerIcon.TYPE_NULL))
localService.setMousePointerAccelerationEnabled(false, 10)
- verify(native).setMousePointerAccelerationEnabled(eq(false))
+ verify(native).setMousePointerAccelerationEnabled(10, false)
service.onDisplayRemoved(10)
+ verify(native).setPointerIconVisibility(10, true)
verify(native).displayRemoved(eq(10))
verify(native).setPointerIconType(eq(PointerIcon.TYPE_NOT_SPECIFIED))
- verify(native).setMousePointerAccelerationEnabled(true)
+ verify(native).setMousePointerAccelerationEnabled(10, true)
verifyNoMoreInteractions(native)
// This call should not block because the virtual mouse pointer override was never removed.
@@ -329,25 +331,26 @@
localService.setPointerIconVisible(false, 10)
verify(native).setPointerIconType(eq(PointerIcon.TYPE_NULL))
+ verify(native).setPointerIconVisibility(10, false)
localService.setMousePointerAccelerationEnabled(false, 10)
- verify(native).setMousePointerAccelerationEnabled(eq(false))
+ verify(native).setMousePointerAccelerationEnabled(10, false)
localService.setPointerIconVisible(true, 10)
verify(native).setPointerIconType(eq(PointerIcon.TYPE_NOT_SPECIFIED))
+ verify(native).setPointerIconVisibility(10, true)
localService.setMousePointerAccelerationEnabled(true, 10)
- verify(native).setMousePointerAccelerationEnabled(eq(true))
+ verify(native).setMousePointerAccelerationEnabled(10, true)
- // Verify that setting properties on a different display is not propagated until the
- // pointer is moved to that display.
localService.setPointerIconVisible(false, 20)
+ verify(native).setPointerIconVisibility(20, false)
localService.setMousePointerAccelerationEnabled(false, 20)
+ verify(native).setMousePointerAccelerationEnabled(20, false)
verifyNoMoreInteractions(native)
clearInvocations(native)
setVirtualMousePointerDisplayIdAndVerify(20)
verify(native).setPointerIconType(eq(PointerIcon.TYPE_NULL))
- verify(native).setMousePointerAccelerationEnabled(eq(false))
}
@Test
@@ -357,12 +360,13 @@
localService.setPointerIconVisible(false, 10)
localService.setMousePointerAccelerationEnabled(false, 10)
+ verify(native).setPointerIconVisibility(10, false)
+ verify(native).setMousePointerAccelerationEnabled(10, false)
verifyNoMoreInteractions(native)
setVirtualMousePointerDisplayIdAndVerify(10)
verify(native).setPointerIconType(eq(PointerIcon.TYPE_NULL))
- verify(native).setMousePointerAccelerationEnabled(eq(false))
}
@Test
diff --git a/tests/RollbackTest/RollbackTest/src/com/android/tests/rollback/RollbackTest.java b/tests/RollbackTest/RollbackTest/src/com/android/tests/rollback/RollbackTest.java
index cbdcb88..518183f 100644
--- a/tests/RollbackTest/RollbackTest/src/com/android/tests/rollback/RollbackTest.java
+++ b/tests/RollbackTest/RollbackTest/src/com/android/tests/rollback/RollbackTest.java
@@ -30,6 +30,7 @@
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
+import android.content.pm.PackageManager;
import android.content.rollback.RollbackInfo;
import android.content.rollback.RollbackManager;
import android.os.UserManager;
@@ -146,7 +147,8 @@
assertThat(InstallUtils.getInstalledVersion(TestApp.A)).isEqualTo(1);
// Upgrade from v1 to v2, with rollbacks enabled.
- Install.single(TestApp.A2).setEnableRollback().commit();
+ Install.single(TestApp.A2).setEnableRollback().setRollbackImpactLevel(
+ PackageManager.ROLLBACK_USER_IMPACT_HIGH).commit();
assertThat(InstallUtils.getInstalledVersion(TestApp.A)).isEqualTo(2);
// The app should now be available for rollback.
@@ -154,6 +156,8 @@
assertThat(available).isNotStaged();
assertThat(available).packagesContainsExactly(
Rollback.from(TestApp.A2).to(TestApp.A1));
+ assertThat(available.getRollbackImpactLevel()).isEqualTo(
+ PackageManager.ROLLBACK_USER_IMPACT_HIGH);
// We should not have received any rollback requests yet.
// TODO: Possibly flaky if, by chance, some other app on device
@@ -264,6 +268,8 @@
RollbackInfo rollbackB = waitForAvailableRollback(TestApp.B);
assertThat(rollbackB).packagesContainsExactly(
Rollback.from(TestApp.B2).to(TestApp.B1));
+ assertThat(rollbackB.getRollbackImpactLevel()).isEqualTo(
+ PackageManager.ROLLBACK_USER_IMPACT_LOW);
// Register rollback committed receiver
RollbackBroadcastReceiver rollbackReceiver = new RollbackBroadcastReceiver();
diff --git a/tools/aapt2/cmd/Link.cpp b/tools/aapt2/cmd/Link.cpp
index 45dd02c..f3f1838 100644
--- a/tools/aapt2/cmd/Link.cpp
+++ b/tools/aapt2/cmd/Link.cpp
@@ -56,6 +56,7 @@
#include "java/JavaClassGenerator.h"
#include "java/ManifestClassGenerator.h"
#include "java/ProguardRules.h"
+#include "link/FeatureFlagsFilter.h"
#include "link/Linkers.h"
#include "link/ManifestFixer.h"
#include "link/NoDefaultResourceRemover.h"
@@ -1987,6 +1988,19 @@
context_->SetNameManglerPolicy(NameManglerPolicy{context_->GetCompilationPackage()});
context_->SetSplitNameDependencies(app_info_.split_name_dependencies);
+ FeatureFlagsFilterOptions flags_filter_options;
+ if (context_->GetMinSdkVersion() > SDK_UPSIDE_DOWN_CAKE) {
+ // For API version > U, PackageManager will dynamically read the flag values and disable
+ // manifest elements accordingly when parsing the manifest.
+ // For API version <= U, we remove disabled elements from the manifest with the filter.
+ flags_filter_options.remove_disabled_elements = false;
+ flags_filter_options.flags_must_have_value = false;
+ }
+ FeatureFlagsFilter flags_filter(options_.feature_flag_values, flags_filter_options);
+ if (!flags_filter.Consume(context_, manifest_xml.get())) {
+ return 1;
+ }
+
// Override the package ID when it is "android".
if (context_->GetCompilationPackage() == "android") {
context_->SetPackageId(kAndroidPackageId);
@@ -2531,7 +2545,7 @@
}
for (const std::string& arg : all_feature_flags_args) {
- if (ParseFeatureFlagsParameter(arg, context.GetDiagnostics(), &options_.feature_flag_values)) {
+ if (!ParseFeatureFlagsParameter(arg, context.GetDiagnostics(), &options_.feature_flag_values)) {
return 1;
}
}
diff --git a/tools/aapt2/cmd/Link.h b/tools/aapt2/cmd/Link.h
index 26713fd..dc18b1c 100644
--- a/tools/aapt2/cmd/Link.h
+++ b/tools/aapt2/cmd/Link.h
@@ -330,7 +330,11 @@
"should only be used together with the --static-lib flag.",
&options_.merge_only);
AddOptionalSwitch("-v", "Enables verbose logging.", &verbose_);
- AddOptionalFlagList("--feature-flags", "Placeholder, to be implemented.", &feature_flags_args_);
+ AddOptionalFlagList("--feature-flags",
+ "Specify the values of feature flags. The pairs in the argument\n"
+ "are separated by ',' and the name is separated from the value by '='.\n"
+ "Example: \"flag1=true,flag2=false,flag3=\" (flag3 has no given value).",
+ &feature_flags_args_);
}
int Action(const std::vector<std::string>& args) override;
diff --git a/tools/aapt2/cmd/Link_test.cpp b/tools/aapt2/cmd/Link_test.cpp
index 7096f5c..9323f3b 100644
--- a/tools/aapt2/cmd/Link_test.cpp
+++ b/tools/aapt2/cmd/Link_test.cpp
@@ -16,11 +16,10 @@
#include "Link.h"
-#include <android-base/file.h>
-
-#include "AppInfo.h"
#include "Diagnostics.h"
#include "LoadedApk.h"
+#include "android-base/file.h"
+#include "android-base/stringprintf.h"
#include "test/Test.h"
using testing::Eq;
@@ -993,4 +992,213 @@
ASSERT_FALSE(Link(link_args, &diag));
}
+static void BuildSDKWithFeatureFlagAttr(const std::string& apk_path, const std::string& java_path,
+ CommandTestFixture* fixture, android::IDiagnostics* diag) {
+ const std::string android_values =
+ R"(<resources>
+ <staging-public-group type="attr" first-id="0x01fe0063">
+ <public name="featureFlag" />
+ </staging-public-group>
+ <attr name="featureFlag" format="string" />
+ </resources>)";
+
+ SourceXML source_xml{.res_file_path = "/res/values/values.xml", .file_contents = android_values};
+ BuildSDK({source_xml}, apk_path, java_path, fixture, diag);
+}
+
+TEST_F(LinkTest, FeatureFlagDisabled_SdkAtMostUDC) {
+ StdErrDiagnostics diag;
+ const std::string android_apk = GetTestPath("android.apk");
+ const std::string android_java = GetTestPath("android-java");
+ BuildSDKWithFeatureFlagAttr(android_apk, android_java, this, &diag);
+
+ const std::string manifest_contents = android::base::StringPrintf(
+ R"(<uses-sdk android:minSdkVersion="%d" />"
+ <permission android:name="FOO" android:featureFlag="flag" />)",
+ SDK_UPSIDE_DOWN_CAKE);
+ auto app_manifest = ManifestBuilder(this)
+ .SetPackageName("com.example.app")
+ .AddContents(manifest_contents)
+ .Build();
+
+ auto app_link_args = LinkCommandBuilder(this)
+ .SetManifestFile(app_manifest)
+ .AddParameter("-I", android_apk)
+ .AddParameter("--feature-flags", "flag=false");
+
+ const std::string app_apk = GetTestPath("app.apk");
+ BuildApk({}, app_apk, std::move(app_link_args), this, &diag);
+
+ // Permission element should be removed if flag is disabled
+ auto apk = LoadedApk::LoadApkFromPath(app_apk, &diag);
+ ASSERT_THAT(apk, NotNull());
+ auto apk_manifest = apk->GetManifest();
+ ASSERT_THAT(apk_manifest, NotNull());
+ auto root = apk_manifest->root.get();
+ ASSERT_THAT(root, NotNull());
+ auto maybe_removed = root->FindChild({}, "permission");
+ ASSERT_THAT(maybe_removed, IsNull());
+}
+
+TEST_F(LinkTest, FeatureFlagEnabled_SdkAtMostUDC) {
+ StdErrDiagnostics diag;
+ const std::string android_apk = GetTestPath("android.apk");
+ const std::string android_java = GetTestPath("android-java");
+ BuildSDKWithFeatureFlagAttr(android_apk, android_java, this, &diag);
+
+ const std::string manifest_contents = android::base::StringPrintf(
+ R"(<uses-sdk android:minSdkVersion="%d" />"
+ <permission android:name="FOO" android:featureFlag="flag" />)",
+ SDK_UPSIDE_DOWN_CAKE);
+ auto app_manifest = ManifestBuilder(this)
+ .SetPackageName("com.example.app")
+ .AddContents(manifest_contents)
+ .Build();
+
+ auto app_link_args = LinkCommandBuilder(this)
+ .SetManifestFile(app_manifest)
+ .AddParameter("-I", android_apk)
+ .AddParameter("--feature-flags", "flag=true");
+
+ const std::string app_apk = GetTestPath("app.apk");
+ BuildApk({}, app_apk, std::move(app_link_args), this, &diag);
+
+ // Permission element should be kept if flag is enabled
+ auto apk = LoadedApk::LoadApkFromPath(app_apk, &diag);
+ ASSERT_THAT(apk, NotNull());
+ auto apk_manifest = apk->GetManifest();
+ ASSERT_THAT(apk_manifest, NotNull());
+ auto root = apk_manifest->root.get();
+ ASSERT_THAT(root, NotNull());
+ auto maybe_removed = root->FindChild({}, "permission");
+ ASSERT_THAT(maybe_removed, NotNull());
+}
+
+TEST_F(LinkTest, FeatureFlagWithNoValue_SdkAtMostUDC) {
+ StdErrDiagnostics diag;
+ const std::string android_apk = GetTestPath("android.apk");
+ const std::string android_java = GetTestPath("android-java");
+ BuildSDKWithFeatureFlagAttr(android_apk, android_java, this, &diag);
+
+ const std::string manifest_contents = android::base::StringPrintf(
+ R"(<uses-sdk android:minSdkVersion="%d" />"
+ <permission android:name="FOO" android:featureFlag="flag" />)",
+ SDK_UPSIDE_DOWN_CAKE);
+ auto app_manifest = ManifestBuilder(this)
+ .SetPackageName("com.example.app")
+ .AddContents(manifest_contents)
+ .Build();
+
+ auto app_link_args = LinkCommandBuilder(this)
+ .SetManifestFile(app_manifest)
+ .AddParameter("-I", android_apk)
+ .AddParameter("--feature-flags", "flag=");
+
+ // Flags must have values if <= UDC
+ const std::string app_apk = GetTestPath("app.apk");
+ ASSERT_FALSE(Link(app_link_args.Build(app_apk), &diag));
+}
+
+TEST_F(LinkTest, FeatureFlagDisabled_SdkAfterUDC) {
+ StdErrDiagnostics diag;
+ const std::string android_apk = GetTestPath("android.apk");
+ const std::string android_java = GetTestPath("android-java");
+ BuildSDKWithFeatureFlagAttr(android_apk, android_java, this, &diag);
+
+ const std::string manifest_contents = android::base::StringPrintf(
+ R"(<uses-sdk android:minSdkVersion="%d" />"
+ <permission android:name="FOO" android:featureFlag="flag" />)",
+ SDK_CUR_DEVELOPMENT);
+ auto app_manifest = ManifestBuilder(this)
+ .SetPackageName("com.example.app")
+ .AddContents(manifest_contents)
+ .Build();
+
+ auto app_link_args = LinkCommandBuilder(this)
+ .SetManifestFile(app_manifest)
+ .AddParameter("-I", android_apk)
+ .AddParameter("--feature-flags", "flag=false");
+
+ const std::string app_apk = GetTestPath("app.apk");
+ BuildApk({}, app_apk, std::move(app_link_args), this, &diag);
+
+ // Permission element should be kept if > UDC, regardless of flag value
+ auto apk = LoadedApk::LoadApkFromPath(app_apk, &diag);
+ ASSERT_THAT(apk, NotNull());
+ auto apk_manifest = apk->GetManifest();
+ ASSERT_THAT(apk_manifest, NotNull());
+ auto root = apk_manifest->root.get();
+ ASSERT_THAT(root, NotNull());
+ auto maybe_removed = root->FindChild({}, "permission");
+ ASSERT_THAT(maybe_removed, NotNull());
+}
+
+TEST_F(LinkTest, FeatureFlagEnabled_SdkAfterUDC) {
+ StdErrDiagnostics diag;
+ const std::string android_apk = GetTestPath("android.apk");
+ const std::string android_java = GetTestPath("android-java");
+ BuildSDKWithFeatureFlagAttr(android_apk, android_java, this, &diag);
+
+ const std::string manifest_contents = android::base::StringPrintf(
+ R"(<uses-sdk android:minSdkVersion="%d" />"
+ <permission android:name="FOO" android:featureFlag="flag" />)",
+ SDK_CUR_DEVELOPMENT);
+ auto app_manifest = ManifestBuilder(this)
+ .SetPackageName("com.example.app")
+ .AddContents(manifest_contents)
+ .Build();
+
+ auto app_link_args = LinkCommandBuilder(this)
+ .SetManifestFile(app_manifest)
+ .AddParameter("-I", android_apk)
+ .AddParameter("--feature-flags", "flag=true");
+
+ const std::string app_apk = GetTestPath("app.apk");
+ BuildApk({}, app_apk, std::move(app_link_args), this, &diag);
+
+ // Permission element should be kept if > UDC, regardless of flag value
+ auto apk = LoadedApk::LoadApkFromPath(app_apk, &diag);
+ ASSERT_THAT(apk, NotNull());
+ auto apk_manifest = apk->GetManifest();
+ ASSERT_THAT(apk_manifest, NotNull());
+ auto root = apk_manifest->root.get();
+ ASSERT_THAT(root, NotNull());
+ auto maybe_removed = root->FindChild({}, "permission");
+ ASSERT_THAT(maybe_removed, NotNull());
+}
+
+TEST_F(LinkTest, FeatureFlagWithNoValue_SdkAfterUDC) {
+ StdErrDiagnostics diag;
+ const std::string android_apk = GetTestPath("android.apk");
+ const std::string android_java = GetTestPath("android-java");
+ BuildSDKWithFeatureFlagAttr(android_apk, android_java, this, &diag);
+
+ const std::string manifest_contents = android::base::StringPrintf(
+ R"(<uses-sdk android:minSdkVersion="%d" />"
+ <permission android:name="FOO" android:featureFlag="flag" />)",
+ SDK_CUR_DEVELOPMENT);
+ auto app_manifest = ManifestBuilder(this)
+ .SetPackageName("com.example.app")
+ .AddContents(manifest_contents)
+ .Build();
+
+ auto app_link_args = LinkCommandBuilder(this)
+ .SetManifestFile(app_manifest)
+ .AddParameter("-I", android_apk)
+ .AddParameter("--feature-flags", "flag=");
+
+ const std::string app_apk = GetTestPath("app.apk");
+ BuildApk({}, app_apk, std::move(app_link_args), this, &diag);
+
+ // Permission element should be kept if > UDC, regardless of flag value
+ auto apk = LoadedApk::LoadApkFromPath(app_apk, &diag);
+ ASSERT_THAT(apk, NotNull());
+ auto apk_manifest = apk->GetManifest();
+ ASSERT_THAT(apk_manifest, NotNull());
+ auto root = apk_manifest->root.get();
+ ASSERT_THAT(root, NotNull());
+ auto maybe_removed = root->FindChild({}, "permission");
+ ASSERT_THAT(maybe_removed, NotNull());
+}
+
} // namespace aapt
diff --git a/tools/aapt2/java/AnnotationProcessor.cpp b/tools/aapt2/java/AnnotationProcessor.cpp
index 8c644cf..a7f6f55 100644
--- a/tools/aapt2/java/AnnotationProcessor.cpp
+++ b/tools/aapt2/java/AnnotationProcessor.cpp
@@ -64,29 +64,31 @@
{"@FlaggedApi", AnnotationRule::kFlaggedApi, "@android.annotation.FlaggedApi", true},
}};
-void AnnotationProcessor::AppendCommentLine(std::string comment) {
+void AnnotationProcessor::AppendCommentLine(std::string comment, bool add_api_annotations) {
static constexpr std::string_view sDeprecated = "@deprecated";
- // Treat deprecated specially, since we don't remove it from the source comment.
- if (comment.find(sDeprecated) != std::string::npos) {
- annotation_parameter_map_[AnnotationRule::kDeprecated] = "";
- }
+ if (add_api_annotations) {
+ // Treat deprecated specially, since we don't remove it from the source comment.
+ if (comment.find(sDeprecated) != std::string::npos) {
+ annotation_parameter_map_[AnnotationRule::kDeprecated] = "";
+ }
- for (const AnnotationRule& rule : sAnnotationRules) {
- std::string::size_type idx = comment.find(rule.doc_str.data());
- if (idx != std::string::npos) {
- // Captures all parameters associated with the specified annotation rule
- // by matching the first pair of parentheses after the rule.
- std::regex re(std::string(rule.doc_str).append(R"(\s*\((.+)\))"));
- std::smatch match_result;
- const bool is_match = std::regex_search(comment, match_result, re);
- if (is_match && rule.preserve_params) {
- annotation_parameter_map_[rule.bit_mask] = match_result[1].str();
- comment.erase(comment.begin() + match_result.position(),
- comment.begin() + match_result.position() + match_result.length());
- } else {
- annotation_parameter_map_[rule.bit_mask] = "";
- comment.erase(comment.begin() + idx, comment.begin() + idx + rule.doc_str.size());
+ for (const AnnotationRule& rule : sAnnotationRules) {
+ std::string::size_type idx = comment.find(rule.doc_str.data());
+ if (idx != std::string::npos) {
+ // Captures all parameters associated with the specified annotation rule
+ // by matching the first pair of parentheses after the rule.
+ std::regex re(std::string(rule.doc_str).append(R"(\s*\((.+)\))"));
+ std::smatch match_result;
+ const bool is_match = std::regex_search(comment, match_result, re);
+ if (is_match && rule.preserve_params) {
+ annotation_parameter_map_[rule.bit_mask] = match_result[1].str();
+ comment.erase(comment.begin() + match_result.position(),
+ comment.begin() + match_result.position() + match_result.length());
+ } else {
+ annotation_parameter_map_[rule.bit_mask] = "";
+ comment.erase(comment.begin() + idx, comment.begin() + idx + rule.doc_str.size());
+ }
}
}
}
@@ -109,12 +111,12 @@
comment_ << "\n * " << std::move(comment);
}
-void AnnotationProcessor::AppendComment(StringPiece comment) {
+void AnnotationProcessor::AppendComment(StringPiece comment, bool add_api_annotations) {
// We need to process line by line to clean-up whitespace and append prefixes.
for (StringPiece line : util::Tokenize(comment, '\n')) {
line = util::TrimWhitespace(line);
if (!line.empty()) {
- AppendCommentLine(std::string(line));
+ AppendCommentLine(std::string(line), add_api_annotations);
}
}
}
diff --git a/tools/aapt2/java/AnnotationProcessor.h b/tools/aapt2/java/AnnotationProcessor.h
index db3437e..2217ab3 100644
--- a/tools/aapt2/java/AnnotationProcessor.h
+++ b/tools/aapt2/java/AnnotationProcessor.h
@@ -60,7 +60,10 @@
// Adds more comments. Resources can have value definitions for various configurations, and
// each of the definitions may have comments that need to be processed.
- void AppendComment(android::StringPiece comment);
+ //
+ // If add_api_annotations is false, annotations found in the comment (e.g., "@SystemApi")
+ // will NOT be converted to Java annotations.
+ void AppendComment(android::StringPiece comment, bool add_api_annotations = true);
void AppendNewLine();
@@ -73,7 +76,7 @@
bool has_comments_ = false;
std::unordered_map<uint32_t, std::string> annotation_parameter_map_;
- void AppendCommentLine(std::string line);
+ void AppendCommentLine(std::string line, bool add_api_annotations);
};
} // namespace aapt
diff --git a/tools/aapt2/java/AnnotationProcessor_test.cpp b/tools/aapt2/java/AnnotationProcessor_test.cpp
index e98e96b..e5eee34 100644
--- a/tools/aapt2/java/AnnotationProcessor_test.cpp
+++ b/tools/aapt2/java/AnnotationProcessor_test.cpp
@@ -136,7 +136,28 @@
EXPECT_THAT(annotations, HasSubstr("This is a system API"));
}
-TEST(AnnotationProcessor, ExtractsFirstSentence) {
+TEST(AnnotationProcessorTest, DoNotAddApiAnnotations) {
+ AnnotationProcessor processor;
+ processor.AppendComment(
+ "@SystemApi This is a system API\n"
+ "@FlaggedApi This is a flagged API\n"
+ "@TestApi This is a test API\n"
+ "@deprecated Deprecate me\n", /*add_api_annotations=*/
+ false);
+
+ std::string annotations;
+ StringOutputStream out(&annotations);
+ Printer printer(&out);
+ processor.Print(&printer);
+ out.Flush();
+
+ EXPECT_THAT(annotations, Not(HasSubstr("@android.annotation.SystemApi")));
+ EXPECT_THAT(annotations, Not(HasSubstr("@android.annotation.FlaggedApi")));
+ EXPECT_THAT(annotations, Not(HasSubstr("@android.annotation.TestApi")));
+ EXPECT_THAT(annotations, Not(HasSubstr("@Deprecated")));
+}
+
+TEST(AnnotationProcessorTest, ExtractsFirstSentence) {
EXPECT_THAT(AnnotationProcessor::ExtractFirstSentence("This is the only sentence"),
Eq("This is the only sentence"));
EXPECT_THAT(AnnotationProcessor::ExtractFirstSentence(
diff --git a/tools/aapt2/java/JavaClassGenerator.cpp b/tools/aapt2/java/JavaClassGenerator.cpp
index 58f6564..6e73b01 100644
--- a/tools/aapt2/java/JavaClassGenerator.cpp
+++ b/tools/aapt2/java/JavaClassGenerator.cpp
@@ -180,7 +180,10 @@
<< "<td>" << std::hex << symbol.value << std::dec << "</td>"
<< "<td>" << util::TrimWhitespace(symbol.symbol.GetComment())
<< "</td></tr>";
- processor->AppendComment(line.str());
+ // add_api_annotations is false since we don't want any annotations
+ // (e.g., "@deprecated")/ found in the enum/flag values to be propagated
+ // up to the attribute.
+ processor->AppendComment(line.str(), /*add_api_annotations=*/false);
}
processor->AppendComment("</table>");
}
diff --git a/tools/aapt2/java/JavaClassGenerator_test.cpp b/tools/aapt2/java/JavaClassGenerator_test.cpp
index 40395ed..bca9f4b 100644
--- a/tools/aapt2/java/JavaClassGenerator_test.cpp
+++ b/tools/aapt2/java/JavaClassGenerator_test.cpp
@@ -324,7 +324,58 @@
EXPECT_THAT(output, HasSubstr(expected_text));
}
-TEST(JavaClassGeneratorTest, CommentsForEnumAndFlagAttributesArePresent) {}
+TEST(JavaClassGeneratorTest, CommentsForEnumAndFlagAttributesArePresent) {
+ std::unique_ptr<Attribute> flagAttr =
+ test::AttributeBuilder()
+ .SetTypeMask(android::ResTable_map::TYPE_FLAGS)
+ .SetComment("Flag attribute")
+ .AddItemWithComment("flagOne", 0x01, "Flag comment 1")
+ .AddItemWithComment("flagTwo", 0x02, "@deprecated Flag comment 2")
+ .Build();
+ std::unique_ptr<Attribute> enumAttr =
+ test::AttributeBuilder()
+ .SetTypeMask(android::ResTable_map::TYPE_ENUM)
+ .SetComment("Enum attribute")
+ .AddItemWithComment("enumOne", 0x01, "@TestApi Enum comment 1")
+ .AddItemWithComment("enumTwo", 0x02, "Enum comment 2")
+ .Build();
+
+ std::unique_ptr<ResourceTable> table = test::ResourceTableBuilder()
+ .AddValue("android:attr/one", std::move(flagAttr))
+ .AddValue("android:attr/two", std::move(enumAttr))
+ .Build();
+
+ std::unique_ptr<IAaptContext> context =
+ test::ContextBuilder()
+ .AddSymbolSource(util::make_unique<ResourceTableSymbolSource>(table.get()))
+ .SetNameManglerPolicy(NameManglerPolicy{"android"})
+ .Build();
+ JavaClassGeneratorOptions options;
+ options.use_final = false;
+ JavaClassGenerator generator(context.get(), table.get(), options);
+
+ std::string output;
+ StringOutputStream out(&output);
+ ASSERT_TRUE(generator.Generate("android", &out));
+ out.Flush();
+
+ // Special annotations from the enum/flag values should NOT generate
+ // annotations for the attribute value.
+ EXPECT_THAT(output, Not(HasSubstr("@Deprecated")));
+ EXPECT_THAT(output, Not(HasSubstr("@android.annotation.TestApi")));
+
+ EXPECT_THAT(output, HasSubstr("Flag attribute"));
+ EXPECT_THAT(output, HasSubstr("flagOne"));
+ EXPECT_THAT(output, HasSubstr("Flag comment 1"));
+ EXPECT_THAT(output, HasSubstr("flagTwo"));
+ EXPECT_THAT(output, HasSubstr("@deprecated Flag comment 2"));
+
+ EXPECT_THAT(output, HasSubstr("Enum attribute"));
+ EXPECT_THAT(output, HasSubstr("enumOne"));
+ EXPECT_THAT(output, HasSubstr("@TestApi Enum comment 1"));
+ EXPECT_THAT(output, HasSubstr("enumTwo"));
+ EXPECT_THAT(output, HasSubstr("Enum comment 2"));
+}
TEST(JavaClassGeneratorTest, CommentsForStyleablesAndNestedAttributesArePresent) {
Attribute attr;
diff --git a/tools/aapt2/test/Builders.cpp b/tools/aapt2/test/Builders.cpp
index 65f63dc..b5934e4 100644
--- a/tools/aapt2/test/Builders.cpp
+++ b/tools/aapt2/test/Builders.cpp
@@ -177,12 +177,25 @@
return *this;
}
+AttributeBuilder& AttributeBuilder::SetComment(StringPiece comment) {
+ attr_->SetComment(comment);
+ return *this;
+}
+
AttributeBuilder& AttributeBuilder::AddItem(StringPiece name, uint32_t value) {
attr_->symbols.push_back(
Attribute::Symbol{Reference(ResourceName({}, ResourceType::kId, name)), value});
return *this;
}
+AttributeBuilder& AttributeBuilder::AddItemWithComment(StringPiece name, uint32_t value,
+ StringPiece comment) {
+ Reference ref(ResourceName({}, ResourceType::kId, name));
+ ref.SetComment(comment);
+ attr_->symbols.push_back(Attribute::Symbol{ref, value});
+ return *this;
+}
+
std::unique_ptr<Attribute> AttributeBuilder::Build() {
return std::move(attr_);
}
diff --git a/tools/aapt2/test/Builders.h b/tools/aapt2/test/Builders.h
index 098535d..9ee44ba 100644
--- a/tools/aapt2/test/Builders.h
+++ b/tools/aapt2/test/Builders.h
@@ -116,7 +116,10 @@
AttributeBuilder();
AttributeBuilder& SetTypeMask(uint32_t typeMask);
AttributeBuilder& SetWeak(bool weak);
+ AttributeBuilder& SetComment(android::StringPiece comment);
AttributeBuilder& AddItem(android::StringPiece name, uint32_t value);
+ AttributeBuilder& AddItemWithComment(android::StringPiece name, uint32_t value,
+ android::StringPiece comment);
std::unique_ptr<Attribute> Build();
private:
diff --git a/tools/aapt2/util/Files.cpp b/tools/aapt2/util/Files.cpp
index 93c1b61..02e4beae 100644
--- a/tools/aapt2/util/Files.cpp
+++ b/tools/aapt2/util/Files.cpp
@@ -251,10 +251,13 @@
return false;
}
- for (StringPiece line : util::Tokenize(contents, ' ')) {
+ for (StringPiece line : util::Tokenize(contents, '\n')) {
line = util::TrimWhitespace(line);
- if (!line.empty()) {
- out_arglist->emplace_back(line);
+ for (StringPiece arg : util::Tokenize(line, ' ')) {
+ arg = util::TrimWhitespace(arg);
+ if (!arg.empty()) {
+ out_arglist->emplace_back(arg);
+ }
}
}
return true;
@@ -270,10 +273,13 @@
return false;
}
- for (StringPiece line : util::Tokenize(contents, ' ')) {
+ for (StringPiece line : util::Tokenize(contents, '\n')) {
line = util::TrimWhitespace(line);
- if (!line.empty()) {
- out_argset->emplace(line);
+ for (StringPiece arg : util::Tokenize(line, ' ')) {
+ arg = util::TrimWhitespace(arg);
+ if (!arg.empty()) {
+ out_argset->emplace(arg);
+ }
}
}
return true;
diff --git a/tools/aapt2/util/Files_test.cpp b/tools/aapt2/util/Files_test.cpp
index 6c38080..618a3e0 100644
--- a/tools/aapt2/util/Files_test.cpp
+++ b/tools/aapt2/util/Files_test.cpp
@@ -25,6 +25,9 @@
using ::android::base::StringPrintf;
+using ::testing::ElementsAre;
+using ::testing::UnorderedElementsAre;
+
namespace aapt {
namespace file {
@@ -34,9 +37,11 @@
constexpr const char sTestDirSep = '/';
#endif
-class FilesTest : public ::testing::Test {
+class FilesTest : public TestDirectoryFixture {
public:
void SetUp() override {
+ TestDirectoryFixture::SetUp();
+
std::stringstream builder;
builder << "hello" << sDirSep << "there";
expected_path_ = builder.str();
@@ -66,6 +71,42 @@
EXPECT_EQ(expected_path_, base);
}
+TEST_F(FilesTest, AppendArgsFromFile) {
+ const std::string args_file = GetTestPath("args.txt");
+ WriteFile(args_file,
+ " \n"
+ "arg1 arg2 arg3 \n"
+ " arg4 arg5");
+ std::vector<std::string> args;
+ std::string error;
+ ASSERT_TRUE(AppendArgsFromFile(args_file, &args, &error));
+ EXPECT_THAT(args, ElementsAre("arg1", "arg2", "arg3", "arg4", "arg5"));
+}
+
+TEST_F(FilesTest, AppendArgsFromFile_InvalidFile) {
+ std::vector<std::string> args;
+ std::string error;
+ ASSERT_FALSE(AppendArgsFromFile(GetTestPath("not_found.txt"), &args, &error));
+}
+
+TEST_F(FilesTest, AppendSetArgsFromFile) {
+ const std::string args_file = GetTestPath("args.txt");
+ WriteFile(args_file,
+ " \n"
+ "arg2 arg4 arg1 \n"
+ " arg5 arg3");
+ std::unordered_set<std::string> args;
+ std::string error;
+ ASSERT_TRUE(AppendSetArgsFromFile(args_file, &args, &error));
+ EXPECT_THAT(args, UnorderedElementsAre("arg1", "arg2", "arg3", "arg4", "arg5"));
+}
+
+TEST_F(FilesTest, AppendSetArgsFromFile_InvalidFile) {
+ std::unordered_set<std::string> args;
+ std::string error;
+ ASSERT_FALSE(AppendSetArgsFromFile(GetTestPath("not_found.txt"), &args, &error));
+}
+
#ifdef _WIN32
TEST_F(FilesTest, WindowsMkdirsLongPath) {
// Creating directory paths longer than the Windows maximum path length (260 charatcers) should
diff --git a/tools/hoststubgen/hoststubgen/helper-framework-runtime-src/framework/com/android/hoststubgen/nativesubstitution/SystemProperties_host.java b/tools/hoststubgen/hoststubgen/helper-framework-runtime-src/framework/com/android/hoststubgen/nativesubstitution/SystemProperties_host.java
index 1ec1d5f..2f6a361 100644
--- a/tools/hoststubgen/hoststubgen/helper-framework-runtime-src/framework/com/android/hoststubgen/nativesubstitution/SystemProperties_host.java
+++ b/tools/hoststubgen/hoststubgen/helper-framework-runtime-src/framework/com/android/hoststubgen/nativesubstitution/SystemProperties_host.java
@@ -15,42 +15,181 @@
*/
package com.android.hoststubgen.nativesubstitution;
+import android.util.SparseArray;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.util.Preconditions;
+
+import java.util.Map;
+import java.util.Objects;
+import java.util.function.Predicate;
+
public class SystemProperties_host {
+ private static final Object sLock = new Object();
+
+ /** Active system property values */
+ @GuardedBy("sLock")
+ private static Map<String, String> sValues;
+ /** Predicate tested to determine if a given key can be read. */
+ @GuardedBy("sLock")
+ private static Predicate<String> sKeyReadablePredicate;
+ /** Predicate tested to determine if a given key can be written. */
+ @GuardedBy("sLock")
+ private static Predicate<String> sKeyWritablePredicate;
+ /** Callback to trigger when values are changed */
+ @GuardedBy("sLock")
+ private static Runnable sChangeCallback;
+
+ /**
+ * Reverse mapping that provides a way back to an original key from the
+ * {@link System#identityHashCode(Object)} of {@link String#intern}.
+ */
+ @GuardedBy("sLock")
+ private static SparseArray<String> sKeyHandles = new SparseArray<>();
+
+ public static void native_init$ravenwood(Map<String, String> values,
+ Predicate<String> keyReadablePredicate, Predicate<String> keyWritablePredicate,
+ Runnable changeCallback) {
+ synchronized (sLock) {
+ sValues = Objects.requireNonNull(values);
+ sKeyReadablePredicate = Objects.requireNonNull(keyReadablePredicate);
+ sKeyWritablePredicate = Objects.requireNonNull(keyWritablePredicate);
+ sChangeCallback = Objects.requireNonNull(changeCallback);
+ sKeyHandles.clear();
+ }
+ }
+
+ public static void native_reset$ravenwood() {
+ synchronized (sLock) {
+ sValues = null;
+ sKeyReadablePredicate = null;
+ sKeyWritablePredicate = null;
+ sChangeCallback = null;
+ sKeyHandles.clear();
+ }
+ }
+
+ public static void native_set(String key, String val) {
+ synchronized (sLock) {
+ Objects.requireNonNull(key);
+ Preconditions.requireNonNullViaRavenwoodRule(sValues);
+ if (!sKeyWritablePredicate.test(key)) {
+ throw new IllegalArgumentException(
+ "Write access to system property '" + key + "' denied via RavenwoodRule");
+ }
+ if (key.startsWith("ro.") && sValues.containsKey(key)) {
+ throw new IllegalArgumentException(
+ "System property '" + key + "' already defined once; cannot redefine");
+ }
+ if ((val == null) || val.isEmpty()) {
+ sValues.remove(key);
+ } else {
+ sValues.put(key, val);
+ }
+ sChangeCallback.run();
+ }
+ }
+
public static String native_get(String key, String def) {
- throw new RuntimeException("Not implemented yet");
+ synchronized (sLock) {
+ Objects.requireNonNull(key);
+ Preconditions.requireNonNullViaRavenwoodRule(sValues);
+ if (!sKeyReadablePredicate.test(key)) {
+ throw new IllegalArgumentException(
+ "Read access to system property '" + key + "' denied via RavenwoodRule");
+ }
+ return sValues.getOrDefault(key, def);
+ }
}
+
public static int native_get_int(String key, int def) {
- throw new RuntimeException("Not implemented yet");
+ try {
+ return Integer.parseInt(native_get(key, ""));
+ } catch (NumberFormatException ignored) {
+ return def;
+ }
}
+
public static long native_get_long(String key, long def) {
- throw new RuntimeException("Not implemented yet");
+ try {
+ return Long.parseLong(native_get(key, ""));
+ } catch (NumberFormatException ignored) {
+ return def;
+ }
}
+
public static boolean native_get_boolean(String key, boolean def) {
- throw new RuntimeException("Not implemented yet");
+ return parseBoolean(native_get(key, ""), def);
}
public static long native_find(String name) {
- throw new RuntimeException("Not implemented yet");
+ synchronized (sLock) {
+ Preconditions.requireNonNullViaRavenwoodRule(sValues);
+ if (sValues.containsKey(name)) {
+ name = name.intern();
+ final int handle = System.identityHashCode(name);
+ sKeyHandles.put(handle, name);
+ return handle;
+ } else {
+ return 0;
+ }
+ }
}
+
public static String native_get(long handle) {
- throw new RuntimeException("Not implemented yet");
+ synchronized (sLock) {
+ return native_get(sKeyHandles.get((int) handle), "");
+ }
}
+
public static int native_get_int(long handle, int def) {
- throw new RuntimeException("Not implemented yet");
+ synchronized (sLock) {
+ return native_get_int(sKeyHandles.get((int) handle), def);
+ }
}
+
public static long native_get_long(long handle, long def) {
- throw new RuntimeException("Not implemented yet");
+ synchronized (sLock) {
+ return native_get_long(sKeyHandles.get((int) handle), def);
+ }
}
+
public static boolean native_get_boolean(long handle, boolean def) {
- throw new RuntimeException("Not implemented yet");
+ synchronized (sLock) {
+ return native_get_boolean(sKeyHandles.get((int) handle), def);
+ }
}
- public static void native_set(String key, String def) {
- throw new RuntimeException("Not implemented yet");
- }
+
public static void native_add_change_callback() {
- throw new RuntimeException("Not implemented yet");
+ // Ignored; callback always registered via init above
}
+
public static void native_report_sysprop_change() {
- throw new RuntimeException("Not implemented yet");
+ // Report through callback always registered via init above
+ synchronized (sLock) {
+ Preconditions.requireNonNullViaRavenwoodRule(sValues);
+ sChangeCallback.run();
+ }
+ }
+
+ private static boolean parseBoolean(String val, boolean def) {
+ // Matches system/libbase/include/android-base/parsebool.h
+ if (val == null) return def;
+ switch (val) {
+ case "1":
+ case "on":
+ case "true":
+ case "y":
+ case "yes":
+ return true;
+ case "0":
+ case "false":
+ case "n":
+ case "no":
+ case "off":
+ return false;
+ default:
+ return def;
+ }
}
}
diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/AndroidHeuristicsFilter.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/AndroidHeuristicsFilter.kt
index 8ca4732..76bac92 100644
--- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/AndroidHeuristicsFilter.kt
+++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/AndroidHeuristicsFilter.kt
@@ -24,6 +24,7 @@
private val classes: ClassNodes,
val aidlPolicy: FilterPolicyWithReason?,
val featureFlagsPolicy: FilterPolicyWithReason?,
+ val syspropsPolicy: FilterPolicyWithReason?,
fallback: OutputFilter
) : DelegatingFilter(fallback) {
override fun getPolicyForClass(className: String): FilterPolicyWithReason {
@@ -33,6 +34,9 @@
if (featureFlagsPolicy != null && classes.isFeatureFlagsClass(className)) {
return featureFlagsPolicy
}
+ if (syspropsPolicy != null && classes.isSyspropsClass(className)) {
+ return syspropsPolicy
+ }
return super.getPolicyForClass(className)
}
}
@@ -57,3 +61,13 @@
|| className.endsWith("/FeatureFlagsImpl")
|| className.endsWith("/FakeFeatureFlagsImpl");
}
+
+/**
+ * @return if a given class "seems like" a sysprops class.
+ */
+private fun ClassNodes.isSyspropsClass(className: String): Boolean {
+ // Matches template classes defined here:
+ // https://cs.android.com/android/platform/superproject/main/+/main:system/tools/sysprop/
+ return className.startsWith("android/sysprop/")
+ && className.endsWith("Properties")
+}
diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/TextFileFilterPolicyParser.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/TextFileFilterPolicyParser.kt
index d38a6e3..7fdd944 100644
--- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/TextFileFilterPolicyParser.kt
+++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/TextFileFilterPolicyParser.kt
@@ -64,6 +64,7 @@
var aidlPolicy: FilterPolicyWithReason? = null
var featureFlagsPolicy: FilterPolicyWithReason? = null
+ var syspropsPolicy: FilterPolicyWithReason? = null
try {
BufferedReader(FileReader(filename)).use { reader ->
@@ -141,6 +142,14 @@
featureFlagsPolicy =
policy.withReason("$FILTER_REASON (feature flags)")
}
+ SpecialClass.Sysprops -> {
+ if (syspropsPolicy != null) {
+ throw ParseException(
+ "Policy for sysprops already defined")
+ }
+ syspropsPolicy =
+ policy.withReason("$FILTER_REASON (sysprops)")
+ }
}
}
}
@@ -205,10 +214,10 @@
}
var ret: OutputFilter = imf
- if (aidlPolicy != null || featureFlagsPolicy != null) {
+ if (aidlPolicy != null || featureFlagsPolicy != null || syspropsPolicy != null) {
log.d("AndroidHeuristicsFilter enabled")
ret = AndroidHeuristicsFilter(
- classes, aidlPolicy, featureFlagsPolicy, imf)
+ classes, aidlPolicy, featureFlagsPolicy, syspropsPolicy, imf)
}
return ret
}
@@ -218,6 +227,7 @@
NotSpecial,
Aidl,
FeatureFlags,
+ Sysprops,
}
private fun resolveSpecialClass(className: String): SpecialClass {
@@ -227,6 +237,7 @@
when (className.lowercase()) {
":aidl" -> return SpecialClass.Aidl
":feature_flags" -> return SpecialClass.FeatureFlags
+ ":sysprops" -> return SpecialClass.Sysprops
}
throw ParseException("Invalid special class name \"$className\"")
}