diff options
875 files changed, 33468 insertions, 9498 deletions
diff --git a/AconfigFlags.bp b/AconfigFlags.bp index ce311d01cd58..f5bf437a738d 100644 --- a/AconfigFlags.bp +++ b/AconfigFlags.bp @@ -13,69 +13,143 @@ // limitations under the License. aconfig_srcjars = [ - ":android.app.usage.flags-aconfig-java{.generated_srcjars}", + // !!! KEEP THIS LIST ALPHABETICAL !!! + ":aconfig_mediacodec_flags_java_lib{.generated_srcjars}", + ":android.adaptiveauth.flags-aconfig-java{.generated_srcjars}", + ":android.app.flags-aconfig-java{.generated_srcjars}", ":android.app.smartspace.flags-aconfig-java{.generated_srcjars}", + ":android.app.usage.flags-aconfig-java{.generated_srcjars}", + ":android.appwidget.flags-aconfig-java{.generated_srcjars}", + ":android.chre.flags-aconfig-java{.generated_srcjars}", ":android.companion.flags-aconfig-java{.generated_srcjars}", + ":android.companion.virtual.flags-aconfig-java{.generated_srcjars}", + ":android.content.flags-aconfig-java{.generated_srcjars}", ":android.content.pm.flags-aconfig-java{.generated_srcjars}", ":android.content.res.flags-aconfig-java{.generated_srcjars}", + ":android.credentials.flags-aconfig-java{.generated_srcjars}", + ":android.database.sqlite-aconfig-java{.generated_srcjars}", + ":android.hardware.biometrics.flags-aconfig-java{.generated_srcjars}", ":android.hardware.flags-aconfig-java{.generated_srcjars}", ":android.hardware.radio.flags-aconfig-java{.generated_srcjars}", + ":android.hardware.usb.flags-aconfig-java{.generated_srcjars}", ":android.location.flags-aconfig-java{.generated_srcjars}", + ":android.media.tv.flags-aconfig-java{.generated_srcjars}", + ":android.multiuser.flags-aconfig-java{.generated_srcjars}", ":android.net.vcn.flags-aconfig-java{.generated_srcjars}", ":android.nfc.flags-aconfig-java{.generated_srcjars}", ":android.os.flags-aconfig-java{.generated_srcjars}", ":android.os.vibrator.flags-aconfig-java{.generated_srcjars}", + ":android.permission.flags-aconfig-java{.generated_srcjars}", + ":android.provider.flags-aconfig-java{.generated_srcjars}", ":android.security.flags-aconfig-java{.generated_srcjars}", ":android.server.app.flags-aconfig-java{.generated_srcjars}", + ":android.service.autofill.flags-aconfig-java{.generated_srcjars}", ":android.service.chooser.flags-aconfig-java{.generated_srcjars}", + ":android.service.controls.flags-aconfig-java{.generated_srcjars}", ":android.service.dreams.flags-aconfig-java{.generated_srcjars}", ":android.service.notification.flags-aconfig-java{.generated_srcjars}", - ":android.view.flags-aconfig-java{.generated_srcjars}", + ":android.service.voice.flags-aconfig-java{.generated_srcjars}", + ":android.speech.flags-aconfig-java{.generated_srcjars}", + ":android.tracing.flags-aconfig-java{.generated_srcjars}", ":android.view.accessibility.flags-aconfig-java{.generated_srcjars}", + ":android.view.contentcapture.flags-aconfig-java{.generated_srcjars}", + ":android.view.contentprotection.flags-aconfig-java{.generated_srcjars}", + ":android.view.flags-aconfig-java{.generated_srcjars}", + ":android.view.inputmethod.flags-aconfig-java{.generated_srcjars}", + ":android.webkit.flags-aconfig-java{.generated_srcjars}", + ":android.widget.flags-aconfig-java{.generated_srcjars}", ":audio-framework-aconfig", ":camera_platform_flags_core_java_lib{.generated_srcjars}", - ":com.android.window.flags.window-aconfig-java{.generated_srcjars}", - ":android.hardware.biometrics.flags-aconfig-java{.generated_srcjars}", ":com.android.hardware.input-aconfig-java{.generated_srcjars}", ":com.android.input.flags-aconfig-java{.generated_srcjars}", - ":com.android.text.flags-aconfig-java{.generated_srcjars}", - ":framework-jobscheduler-job.flags-aconfig-java{.generated_srcjars}", - ":telecom_flags_core_java_lib{.generated_srcjars}", - ":telephony_flags_core_java_lib{.generated_srcjars}", - ":android.companion.virtual.flags-aconfig-java{.generated_srcjars}", - ":android.view.inputmethod.flags-aconfig-java{.generated_srcjars}", - ":android.widget.flags-aconfig-java{.generated_srcjars}", - ":com.android.media.flags.bettertogether-aconfig-java{.generated_srcjars}", - ":sdk_sandbox_flags_lib{.generated_srcjars}", - ":android.permission.flags-aconfig-java{.generated_srcjars}", - ":android.database.sqlite-aconfig-java{.generated_srcjars}", - ":hwui_flags_java_lib{.generated_srcjars}", - ":framework_graphics_flags_java_lib{.generated_srcjars}", - ":display_flags_lib{.generated_srcjars}", ":com.android.internal.foldables.flags-aconfig-java{.generated_srcjars}", - ":android.multiuser.flags-aconfig-java{.generated_srcjars}", - ":android.app.flags-aconfig-java{.generated_srcjars}", - ":android.credentials.flags-aconfig-java{.generated_srcjars}", - ":android.view.contentprotection.flags-aconfig-java{.generated_srcjars}", - ":com.android.server.flags.services-aconfig-java{.generated_srcjars}", - ":android.service.controls.flags-aconfig-java{.generated_srcjars}", - ":android.service.voice.flags-aconfig-java{.generated_srcjars}", - ":android.media.tv.flags-aconfig-java{.generated_srcjars}", - ":android.service.autofill.flags-aconfig-java{.generated_srcjars}", + ":com.android.media.flags.bettertogether-aconfig-java{.generated_srcjars}", ":com.android.net.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}", ":device_policy_aconfig_flags_lib{.generated_srcjars}", - ":surfaceflinger_flags_java_lib{.generated_srcjars}", - ":android.view.contentcapture.flags-aconfig-java{.generated_srcjars}", - ":android.hardware.usb.flags-aconfig-java{.generated_srcjars}", - ":android.tracing.flags-aconfig-java{.generated_srcjars}", - ":android.appwidget.flags-aconfig-java{.generated_srcjars}", - ":android.webkit.flags-aconfig-java{.generated_srcjars}", - ":android.provider.flags-aconfig-java{.generated_srcjars}", - ":android.chre.flags-aconfig-java{.generated_srcjars}", - ":android.speech.flags-aconfig-java{.generated_srcjars}", + ":display_flags_lib{.generated_srcjars}", + ":framework-jobscheduler-job.flags-aconfig-java{.generated_srcjars}", + ":framework_graphics_flags_java_lib{.generated_srcjars}", + ":hwui_flags_java_lib{.generated_srcjars}", ":power_flags_lib{.generated_srcjars}", + ":sdk_sandbox_flags_lib{.generated_srcjars}", + ":surfaceflinger_flags_java_lib{.generated_srcjars}", + ":telecom_flags_core_java_lib{.generated_srcjars}", + ":telephony_flags_core_java_lib{.generated_srcjars}", + // !!! KEEP THIS LIST ALPHABETICAL !!! ] +stubs_defaults { + name: "framework-minus-apex-aconfig-declarations", + aconfig_declarations: [ + "android.app.flags-aconfig", + "android.app.smartspace.flags-aconfig", + "android.app.usage.flags-aconfig", + "android.appwidget.flags-aconfig", + "android.companion.flags-aconfig", + "android.companion.virtual.flags-aconfig", + "android.content.pm.flags-aconfig", + "android.content.res.flags-aconfig", + "android.credentials.flags-aconfig", + "android.database.sqlite-aconfig", + "android.hardware.biometrics.flags-aconfig", + "android.hardware.flags-aconfig", + "android.hardware.radio.flags-aconfig", + "android.hardware.usb.flags-aconfig", + "android.location.flags-aconfig", + "android.media.audio-aconfig", + "android.media.audiopolicy-aconfig", + "android.media.midi-aconfig", + "android.media.tv.flags-aconfig", + "android.multiuser.flags-aconfig", + "android.net.vcn.flags-aconfig", + "android.nfc.flags-aconfig", + "android.os.flags-aconfig", + "android.os.vibrator.flags-aconfig", + "android.permission.flags-aconfig", + "android.provider.flags-aconfig", + "android.security.flags-aconfig", + "android.server.app.flags-aconfig", + "android.service.autofill.flags-aconfig", + "android.service.chooser.flags-aconfig", + "android.service.controls.flags-aconfig", + "android.service.dreams.flags-aconfig", + "android.service.notification.flags-aconfig", + "android.service.voice.flags-aconfig", + "android.speech.flags-aconfig", + "android.tracing.flags-aconfig", + "android.view.accessibility.flags-aconfig", + "android.view.contentcapture.flags-aconfig", + "android.view.contentprotection.flags-aconfig", + "android.view.flags-aconfig", + "android.view.inputmethod.flags-aconfig", + "android.webkit.flags-aconfig", + "android.widget.flags-aconfig", + "camera_platform_flags", + "chre_flags", + "com.android.hardware.input.input-aconfig", + "com.android.input.flags-aconfig", + "com.android.media.flags.bettertogether-aconfig", + "com.android.net.flags-aconfig", + "com.android.server.flags.services-aconfig", + "com.android.text.flags-aconfig", + "com.android.window.flags.window-aconfig", + "device_policy_aconfig_flags", + "display_flags", + "fold_lock_setting_flags", + "framework-jobscheduler-job.flags-aconfig", + "framework_graphics_flags", + "hwui_flags", + "power_flags", + "sdk_sandbox_flags", + "surfaceflinger_flags", + "telecom_flags", + "telephony_flags", + ], +} + filegroup { name: "framework-minus-apex-aconfig-srcjars", srcs: aconfig_srcjars, @@ -455,6 +529,13 @@ java_aconfig_library { defaults: ["framework-minus-apex-aconfig-java-defaults"], } +java_aconfig_library { + name: "com.android.media.flags.bettertogether-aconfig-java-host", + aconfig_declarations: "com.android.media.flags.bettertogether-aconfig", + host_supported: true, + defaults: ["framework-minus-apex-aconfig-java-defaults"], +} + // Media TV aconfig_declarations { name: "android.media.tv.flags-aconfig", @@ -940,3 +1021,29 @@ java_aconfig_library { aconfig_declarations: "power_flags", defaults: ["framework-minus-apex-aconfig-java-defaults"], } + +// Content +aconfig_declarations { + name: "android.content.flags-aconfig", + package: "android.content.flags", + srcs: ["core/java/android/content/flags/flags.aconfig"], +} + +java_aconfig_library { + name: "android.content.flags-aconfig-java", + aconfig_declarations: "android.content.flags-aconfig", + defaults: ["framework-minus-apex-aconfig-java-defaults"], +} + +// Adaptive Auth +aconfig_declarations { + name: "android.adaptiveauth.flags-aconfig", + package: "android.adaptiveauth", + srcs: ["core/java/android/adaptiveauth/*.aconfig"], +} + +java_aconfig_library { + name: "android.adaptiveauth.flags-aconfig-java", + aconfig_declarations: "android.adaptiveauth.flags-aconfig", + defaults: ["framework-minus-apex-aconfig-java-defaults"], +} diff --git a/Android.bp b/Android.bp index a3e39018e147..9c56733650cb 100644 --- a/Android.bp +++ b/Android.bp @@ -149,6 +149,7 @@ filegroup { ":framework-javastream-protos", ":statslog-framework-java-gen", // FrameworkStatsLog.java ":audio_policy_configuration_V7_0", + ":perfetto_trace_javastream_protos", ], } @@ -215,7 +216,6 @@ java_library { "apex_aidl_interface-java", "packagemanager_aidl-java", "framework-protos", - "libtombstone_proto_java", "updatable-driver-protos", "ota_metadata_proto_java", "android.hidl.base-V1.0-java", diff --git a/ProtoLibraries.bp b/ProtoLibraries.bp index d03bbd249b00..e7adf203334e 100644 --- a/ProtoLibraries.bp +++ b/ProtoLibraries.bp @@ -34,6 +34,7 @@ gensrcs { ":ipconnectivity-proto-src", ":libstats_atom_enum_protos", ":libstats_atom_message_protos", + ":libtombstone_proto-src", "core/proto/**/*.proto", "libs/incident/**/*.proto", ], diff --git a/Ravenwood.bp b/Ravenwood.bp index f330ad14ea57..0877bcedb609 100644 --- a/Ravenwood.bp +++ b/Ravenwood.bp @@ -75,16 +75,36 @@ java_genrule { ], } +java_library { + name: "mockito-ravenwood-prebuilt", + installable: false, + static_libs: [ + "mockito-robolectric-prebuilt", + ], +} + +java_library { + name: "inline-mockito-ravenwood-prebuilt", + installable: false, + static_libs: [ + "inline-mockito-robolectric-prebuilt", + ], +} + android_ravenwood_libgroup { name: "ravenwood-runtime", libs: [ "framework-minus-apex.ravenwood", "hoststubgen-helper-runtime.ravenwood", "hoststubgen-helper-framework-runtime.ravenwood", + "core-libart-for-host", + "all-updatable-modules-system-stubs", "junit", "truth", "ravenwood-junit-impl", "android.test.mock.ravenwood", + "mockito-ravenwood-prebuilt", + "inline-mockito-ravenwood-prebuilt", ], } @@ -94,5 +114,7 @@ android_ravenwood_libgroup { "junit", "truth", "ravenwood-junit", + "mockito-ravenwood-prebuilt", + "inline-mockito-ravenwood-prebuilt", ], } diff --git a/STABILITY_OWNERS b/STABILITY_OWNERS deleted file mode 100644 index a7ecb4dfdd44..000000000000 --- a/STABILITY_OWNERS +++ /dev/null @@ -1,2 +0,0 @@ -gaillard@google.com - diff --git a/TEST_MAPPING b/TEST_MAPPING index ecfd86c584e0..c904eb46d88e 100644 --- a/TEST_MAPPING +++ b/TEST_MAPPING @@ -142,8 +142,7 @@ "name": "CtsUtilTestCasesRavenwood", "host": true, "file_patterns": [ - "*Ravenwood*", - "*ravenwood*" + "[Rr]avenwood" ] } ], diff --git a/apex/jobscheduler/framework/java/com/android/server/DeviceIdleInternal.java b/apex/jobscheduler/framework/java/com/android/server/DeviceIdleInternal.java index caf7e7f4a4ed..1fc888b06ffd 100644 --- a/apex/jobscheduler/framework/java/com/android/server/DeviceIdleInternal.java +++ b/apex/jobscheduler/framework/java/com/android/server/DeviceIdleInternal.java @@ -16,6 +16,7 @@ package com.android.server; +import android.annotation.NonNull; import android.annotation.Nullable; import android.os.PowerExemptionManager; import android.os.PowerExemptionManager.ReasonCode; @@ -77,6 +78,9 @@ public interface DeviceIdleInternal { int[] getPowerSaveTempWhitelistAppIds(); + @NonNull + String[] getFullPowerWhitelistExceptIdle(); + /** * Listener to be notified when DeviceIdleController determines that the device has moved or is * stationary. diff --git a/apex/jobscheduler/service/java/com/android/server/DeviceIdleController.java b/apex/jobscheduler/service/java/com/android/server/DeviceIdleController.java index 6383ed873e59..31214cbb7066 100644 --- a/apex/jobscheduler/service/java/com/android/server/DeviceIdleController.java +++ b/apex/jobscheduler/service/java/com/android/server/DeviceIdleController.java @@ -2374,6 +2374,11 @@ public class DeviceIdleController extends SystemService return DeviceIdleController.this.isAppOnWhitelistInternal(appid); } + @Override + public String[] getFullPowerWhitelistExceptIdle() { + return DeviceIdleController.this.getFullPowerWhitelistInternalUnchecked(); + } + /** * Returns the array of app ids whitelisted by user. Take care not to * modify this, as it is a reference to the original copy. But the reference @@ -3100,10 +3105,14 @@ public class DeviceIdleController extends SystemService } private String[] getFullPowerWhitelistInternal(final int callingUid, final int callingUserId) { - final String[] apps; + return ArrayUtils.filter(getFullPowerWhitelistInternalUnchecked(), String[]::new, + (pkg) -> !mPackageManagerInternal.filterAppAccess(pkg, callingUid, callingUserId)); + } + + private String[] getFullPowerWhitelistInternalUnchecked() { synchronized (this) { int size = mPowerSaveWhitelistApps.size() + mPowerSaveWhitelistUserApps.size(); - apps = new String[size]; + final String[] apps = new String[size]; int cur = 0; for (int i = 0; i < mPowerSaveWhitelistApps.size(); i++) { apps[cur] = mPowerSaveWhitelistApps.keyAt(i); @@ -3113,9 +3122,8 @@ public class DeviceIdleController extends SystemService apps[cur] = mPowerSaveWhitelistUserApps.keyAt(i); cur++; } + return apps; } - return ArrayUtils.filter(apps, String[]::new, - (pkg) -> !mPackageManagerInternal.filterAppAccess(pkg, callingUid, callingUserId)); } public boolean isPowerSaveWhitelistExceptIdleAppInternal(String packageName) { diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/BackgroundJobsController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/BackgroundJobsController.java index e3ba50dc635b..e3ac780abf09 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/controllers/BackgroundJobsController.java +++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/BackgroundJobsController.java @@ -318,6 +318,10 @@ public final class BackgroundJobsController extends StateController { try { final boolean isStopped = mPackageManagerInternal.isPackageStopped(packageName, uid); + if (DEBUG) { + Slog.d(TAG, + "Pulled stopped state of " + packageName + " (" + uid + "): " + isStopped); + } mPackageStoppedState.add(uid, packageName, isStopped); return isStopped; } catch (PackageManager.NameNotFoundException e) { 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 0e67b9ac944f..44afbe6aff51 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 @@ -30,11 +30,15 @@ import android.annotation.ElapsedRealtimeLong; import android.annotation.NonNull; import android.annotation.Nullable; import android.app.job.JobInfo; +import android.content.BroadcastReceiver; import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; import android.content.pm.PackageManager; import android.os.Handler; import android.os.Looper; import android.os.Message; +import android.os.PowerManager; import android.os.UserHandle; import android.provider.DeviceConfig; import android.util.ArraySet; @@ -48,6 +52,8 @@ import android.util.TimeUtils; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.server.AppSchedulingModuleThread; +import com.android.server.DeviceIdleInternal; +import com.android.server.LocalServices; import com.android.server.job.JobSchedulerService; import com.android.server.utils.AlarmQueue; @@ -127,6 +133,19 @@ public final class FlexibilityController extends StateController { @GuardedBy("mLock") private final SparseLongArray mLastSeenConstraintTimesElapsed = new SparseLongArray(); + private DeviceIdleInternal mDeviceIdleInternal; + private final ArraySet<String> mPowerAllowlistedApps = new ArraySet<>(); + + private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + switch (intent.getAction()) { + case PowerManager.ACTION_POWER_SAVE_WHITELIST_CHANGED: + mHandler.post(FlexibilityController.this::updatePowerAllowlistCache); + break; + } + } + }; @VisibleForTesting @GuardedBy("mLock") final FlexibilityTracker mFlexibilityTracker; @@ -180,8 +199,16 @@ public final class FlexibilityController extends StateController { } }; - private static final int MSG_UPDATE_JOBS = 0; - private static final int MSG_UPDATE_JOB = 1; + private static final int MSG_CHECK_ALL_JOBS = 0; + /** Check the jobs in {@link #mJobsToCheck} */ + private static final int MSG_CHECK_JOBS = 1; + /** Check the jobs of packages in {@link #mPackagesToCheck} */ + private static final int MSG_CHECK_PACKAGES = 2; + + @GuardedBy("mLock") + private final ArraySet<JobStatus> mJobsToCheck = new ArraySet<>(); + @GuardedBy("mLock") + private final ArraySet<String> mPackagesToCheck = new ArraySet<>(); public FlexibilityController( JobSchedulerService service, PrefetchController prefetchController) { @@ -204,6 +231,16 @@ public final class FlexibilityController extends StateController { mPercentToDropConstraints = mFcConfig.DEFAULT_PERCENT_TO_DROP_FLEXIBLE_CONSTRAINTS; mPrefetchController = prefetchController; + + if (mFlexibilityEnabled) { + registerBroadcastReceiver(); + } + } + + @Override + public void onSystemServicesReady() { + mDeviceIdleInternal = LocalServices.getService(DeviceIdleInternal.class); + mHandler.post(FlexibilityController.this::updatePowerAllowlistCache); } @Override @@ -241,6 +278,7 @@ public final class FlexibilityController extends StateController { mFlexibilityAlarmQueue.removeAlarmForKey(js); mFlexibilityTracker.remove(js); } + mJobsToCheck.remove(js); } @Override @@ -266,7 +304,14 @@ public final class FlexibilityController extends StateController { @GuardedBy("mLock") boolean isFlexibilitySatisfiedLocked(JobStatus js) { return !mFlexibilityEnabled + // Exclude all jobs of the TOP app || mService.getUidBias(js.getSourceUid()) == JobInfo.BIAS_TOP_APP + // Only exclude DEFAULT+ priority jobs for BFGS+ apps + || (mService.getUidBias(js.getSourceUid()) >= JobInfo.BIAS_BOUND_FOREGROUND_SERVICE + && js.getEffectivePriority() >= JobInfo.PRIORITY_DEFAULT) + // For apps in the power allowlist, automatically exclude DEFAULT+ priority jobs. + || (js.getEffectivePriority() >= JobInfo.PRIORITY_DEFAULT + && mPowerAllowlistedApps.contains(js.getSourcePackageName())) || hasEnoughSatisfiedConstraintsLocked(js) || mService.isCurrentlyRunningLocked(js); } @@ -371,7 +416,7 @@ public final class FlexibilityController extends StateController { // Push the job update to the handler to avoid blocking other controllers and // potentially batch back-to-back controller state updates together. - mHandler.obtainMessage(MSG_UPDATE_JOBS).sendToTarget(); + mHandler.obtainMessage(MSG_CHECK_ALL_JOBS).sendToTarget(); } } } @@ -485,7 +530,9 @@ public final class FlexibilityController extends StateController { @Override @GuardedBy("mLock") public void onUidBiasChangedLocked(int uid, int prevBias, int newBias) { - if (prevBias != JobInfo.BIAS_TOP_APP && newBias != JobInfo.BIAS_TOP_APP) { + if (prevBias < JobInfo.BIAS_BOUND_FOREGROUND_SERVICE + && newBias < JobInfo.BIAS_BOUND_FOREGROUND_SERVICE) { + // All changes are below BFGS. There's no significant change to care about. return; } final long nowElapsed = sElapsedRealtimeClock.millis(); @@ -557,6 +604,39 @@ public final class FlexibilityController extends StateController { mFcConfig.processConstantLocked(properties, key); } + private void registerBroadcastReceiver() { + IntentFilter filter = new IntentFilter(PowerManager.ACTION_POWER_SAVE_WHITELIST_CHANGED); + mContext.registerReceiver(mBroadcastReceiver, filter); + } + + private void unregisterBroadcastReceiver() { + mContext.unregisterReceiver(mBroadcastReceiver); + } + + private void updatePowerAllowlistCache() { + if (mDeviceIdleInternal == null) { + return; + } + + // Don't call out to DeviceIdleController with the lock held. + final String[] allowlistedPkgs = mDeviceIdleInternal.getFullPowerWhitelistExceptIdle(); + final ArraySet<String> changedPkgs = new ArraySet<>(); + synchronized (mLock) { + changedPkgs.addAll(mPowerAllowlistedApps); + mPowerAllowlistedApps.clear(); + for (final String pkgName : allowlistedPkgs) { + mPowerAllowlistedApps.add(pkgName); + if (changedPkgs.contains(pkgName)) { + changedPkgs.remove(pkgName); + } else { + changedPkgs.add(pkgName); + } + } + mPackagesToCheck.addAll(changedPkgs); + mHandler.sendEmptyMessage(MSG_CHECK_PACKAGES); + } + } + @VisibleForTesting class FlexibilityTracker { final ArrayList<ArraySet<JobStatus>> mTrackedJobs; @@ -710,7 +790,8 @@ public final class FlexibilityController extends StateController { } mFlexibilityTracker.setNumDroppedFlexibleConstraints(js, js.getNumAppliedFlexibleConstraints()); - mHandler.obtainMessage(MSG_UPDATE_JOB, js).sendToTarget(); + mJobsToCheck.add(js); + mHandler.sendEmptyMessage(MSG_CHECK_JOBS); return; } if (nextTimeElapsed == NO_LIFECYCLE_END) { @@ -761,10 +842,12 @@ public final class FlexibilityController extends StateController { @Override public void handleMessage(Message msg) { switch (msg.what) { - case MSG_UPDATE_JOBS: - removeMessages(MSG_UPDATE_JOBS); + case MSG_CHECK_ALL_JOBS: + removeMessages(MSG_CHECK_ALL_JOBS); synchronized (mLock) { + mJobsToCheck.clear(); + mPackagesToCheck.clear(); final long nowElapsed = sElapsedRealtimeClock.millis(); final ArraySet<JobStatus> changedJobs = new ArraySet<>(); @@ -790,19 +873,50 @@ public final class FlexibilityController extends StateController { } break; - case MSG_UPDATE_JOB: + case MSG_CHECK_JOBS: synchronized (mLock) { - final JobStatus js = (JobStatus) msg.obj; - if (DEBUG) { - Slog.d("blah", "Checking on " + js.toShortString()); + final long nowElapsed = sElapsedRealtimeClock.millis(); + ArraySet<JobStatus> changedJobs = new ArraySet<>(); + + for (int i = mJobsToCheck.size() - 1; i >= 0; --i) { + final JobStatus js = mJobsToCheck.valueAt(i); + if (DEBUG) { + Slog.d(TAG, "Checking on " + js.toShortString()); + } + if (js.setFlexibilityConstraintSatisfied( + nowElapsed, isFlexibilitySatisfiedLocked(js))) { + changedJobs.add(js); + } + } + + mJobsToCheck.clear(); + if (changedJobs.size() > 0) { + mStateChangedListener.onControllerStateChanged(changedJobs); } + } + break; + + case MSG_CHECK_PACKAGES: + synchronized (mLock) { final long nowElapsed = sElapsedRealtimeClock.millis(); - if (js.setFlexibilityConstraintSatisfied( - nowElapsed, isFlexibilitySatisfiedLocked(js))) { - // TODO(141645789): add method that will take a single job - ArraySet<JobStatus> changedJob = new ArraySet<>(); - changedJob.add(js); - mStateChangedListener.onControllerStateChanged(changedJob); + final ArraySet<JobStatus> changedJobs = new ArraySet<>(); + + mService.getJobStore().forEachJob( + (js) -> mPackagesToCheck.contains(js.getSourcePackageName()) + || mPackagesToCheck.contains(js.getCallingPackageName()), + (js) -> { + if (DEBUG) { + Slog.d(TAG, "Checking on " + js.toShortString()); + } + if (js.setFlexibilityConstraintSatisfied( + nowElapsed, isFlexibilitySatisfiedLocked(js))) { + changedJobs.add(js); + } + }); + + mPackagesToCheck.clear(); + if (changedJobs.size() > 0) { + mStateChangedListener.onControllerStateChanged(changedJobs); } } break; @@ -837,7 +951,7 @@ public final class FlexibilityController extends StateController { @VisibleForTesting static final long DEFAULT_DEADLINE_PROXIMITY_LIMIT_MS = 15 * MINUTE_IN_MILLIS; @VisibleForTesting - static final long DEFAULT_FALLBACK_FLEXIBILITY_DEADLINE_MS = 72 * HOUR_IN_MILLIS; + static final long DEFAULT_FALLBACK_FLEXIBILITY_DEADLINE_MS = 24 * HOUR_IN_MILLIS; private static final long DEFAULT_MIN_TIME_BETWEEN_FLEXIBILITY_ALARMS_MS = MINUTE_IN_MILLIS; @VisibleForTesting final int[] DEFAULT_PERCENT_TO_DROP_FLEXIBLE_CONSTRAINTS = {50, 60, 70, 80}; @@ -882,10 +996,12 @@ public final class FlexibilityController extends StateController { mFlexibilityEnabled = true; mPrefetchController .registerPrefetchChangedListener(mPrefetchChangedListener); + registerBroadcastReceiver(); } else { mFlexibilityEnabled = false; mPrefetchController .unRegisterPrefetchChangedListener(mPrefetchChangedListener); + unregisterBroadcastReceiver(); } } break; @@ -985,7 +1101,14 @@ public final class FlexibilityController extends StateController { pw.println(":"); pw.increaseIndent(); - pw.print(KEY_APPLIED_CONSTRAINTS, APPLIED_CONSTRAINTS).println(); + pw.print(KEY_APPLIED_CONSTRAINTS, APPLIED_CONSTRAINTS); + pw.print("("); + if (APPLIED_CONSTRAINTS != 0) { + JobStatus.dumpConstraints(pw, APPLIED_CONSTRAINTS); + } else { + pw.print("nothing"); + } + pw.println(")"); pw.print(KEY_DEADLINE_PROXIMITY_LIMIT, DEADLINE_PROXIMITY_LIMIT_MS).println(); pw.print(KEY_FALLBACK_FLEXIBILITY_DEADLINE, FALLBACK_FLEXIBILITY_DEADLINE_MS).println(); pw.print(KEY_MIN_TIME_BETWEEN_FLEXIBILITY_ALARMS_MS, @@ -1044,6 +1167,10 @@ public final class FlexibilityController extends StateController { pw.decreaseIndent(); pw.println(); + pw.print("Power allowlisted packages: "); + pw.println(mPowerAllowlistedApps); + + pw.println(); mFlexibilityTracker.dump(pw, predicate); pw.println(); mFlexibilityAlarmQueue.dump(pw); diff --git a/api/Android.bp b/api/Android.bp index 1686943d08ca..b3b18b66e097 100644 --- a/api/Android.bp +++ b/api/Android.bp @@ -60,8 +60,40 @@ metalava_cmd = "$(location metalava)" metalava_cmd += " -J--add-opens=java.base/java.util=ALL-UNNAMED " metalava_cmd += " --quiet " +soong_config_module_type { + name: "enable_crashrecovery_module", + module_type: "combined_apis_defaults", + config_namespace: "ANDROID", + bool_variables: ["release_crashrecovery_module"], + properties: [ + "bootclasspath", + "system_server_classpath", + ], +} + +soong_config_bool_variable { + name: "release_crashrecovery_module", +} + +enable_crashrecovery_module { + name: "crashrecovery_module_defaults", + soong_config_variables: { + release_crashrecovery_module: { + bootclasspath: [ + "framework-crashrecovery", + ], + system_server_classpath: [ + "service-crashrecovery", + ], + }, + }, +} + combined_apis { name: "frameworks-base-api", + defaults: [ + "crashrecovery_module_defaults", + ], bootclasspath: [ "android.net.ipsec.ike", "art.module.public.api", @@ -269,6 +301,7 @@ packages_to_document = [ // classpath (or sources) somehow. stubs_defaults { name: "android-non-updatable-stubs-defaults", + defaults: ["framework-minus-apex-aconfig-declarations"], srcs: [":android-non-updatable-stub-sources"], sdk_version: "none", system_modules: "none", diff --git a/api/StubLibraries.bp b/api/StubLibraries.bp index 74344cd4b5a5..59c01284ab27 100644 --- a/api/StubLibraries.bp +++ b/api/StubLibraries.bp @@ -239,6 +239,10 @@ java_defaults { name: "android-non-updatable_from_source_defaults", libs: ["stub-annotations"], static_libs: ["framework-res-package-jar"], // Export package of framework-res +} + +java_defaults { + name: "android-non-updatable_exportable_from_source_defaults", dist: { targets: ["sdk"], tag: ".jar", @@ -265,6 +269,14 @@ java_library { } java_library { + name: "android-non-updatable.stubs.exportable", + defaults: ["android-non-updatable_defaults"], + static_libs: [ + "android-non-updatable.stubs.exportable.from-source", + ], +} + +java_library { name: "android-non-updatable.stubs.system", defaults: ["android-non-updatable_defaults"], static_libs: [ @@ -283,6 +295,14 @@ java_library { } java_library { + name: "android-non-updatable.stubs.exportable.system", + defaults: ["android-non-updatable_defaults"], + static_libs: [ + "android-non-updatable.stubs.exportable.system.from-source", + ], +} + +java_library { name: "android-non-updatable.stubs.module_lib", defaults: ["android-non-updatable_defaults"], static_libs: [ @@ -301,6 +321,14 @@ java_library { } java_library { + name: "android-non-updatable.stubs.exportable.module_lib", + defaults: ["android-non-updatable_defaults"], + static_libs: [ + "android-non-updatable.stubs.exportable.module_lib.from-source", + ], +} + +java_library { name: "android-non-updatable.stubs.test", defaults: ["android-non-updatable_defaults"], static_libs: [ @@ -319,6 +347,14 @@ java_library { } java_library { + name: "android-non-updatable.stubs.exportable.test", + defaults: ["android-non-updatable_defaults"], + static_libs: [ + "android-non-updatable.stubs.exportable.test.from-source", + ], +} + +java_library { name: "android-non-updatable.stubs.from-source", defaults: [ "android-non-updatable_defaults", @@ -326,6 +362,17 @@ java_library { ], srcs: [":api-stubs-docs-non-updatable"], libs: ["all-modules-public-stubs"], +} + +java_library { + name: "android-non-updatable.stubs.exportable.from-source", + defaults: [ + "android-non-updatable_defaults", + "android-non-updatable_from_source_defaults", + "android-non-updatable_exportable_from_source_defaults", + ], + srcs: [":api-stubs-docs-non-updatable{.exportable}"], + libs: ["all-modules-public-stubs"], dist: { dir: "apistubs/android/public", }, @@ -339,6 +386,17 @@ java_library { ], srcs: [":system-api-stubs-docs-non-updatable"], libs: ["all-modules-system-stubs"], +} + +java_library { + name: "android-non-updatable.stubs.exportable.system.from-source", + defaults: [ + "android-non-updatable_defaults", + "android-non-updatable_from_source_defaults", + "android-non-updatable_exportable_from_source_defaults", + ], + srcs: [":system-api-stubs-docs-non-updatable{.exportable}"], + libs: ["all-modules-system-stubs"], dist: { dir: "apistubs/android/system", }, @@ -352,6 +410,17 @@ java_library { ], srcs: [":module-lib-api-stubs-docs-non-updatable"], libs: non_updatable_api_deps_on_modules, +} + +java_library { + name: "android-non-updatable.stubs.exportable.module_lib.from-source", + defaults: [ + "android-non-updatable_defaults", + "android-non-updatable_from_source_defaults", + "android-non-updatable_exportable_from_source_defaults", + ], + srcs: [":module-lib-api-stubs-docs-non-updatable{.exportable}"], + libs: non_updatable_api_deps_on_modules, dist: { dir: "apistubs/android/module-lib", }, @@ -365,6 +434,17 @@ java_library { ], srcs: [":test-api-stubs-docs-non-updatable"], libs: ["all-modules-system-stubs"], +} + +java_library { + name: "android-non-updatable.stubs.exportable.test.from-source", + defaults: [ + "android-non-updatable_defaults", + "android-non-updatable_from_source_defaults", + "android-non-updatable_exportable_from_source_defaults", + ], + srcs: [":test-api-stubs-docs-non-updatable{.exportable}"], + libs: ["all-modules-system-stubs"], dist: { dir: "apistubs/android/test", }, @@ -462,6 +542,16 @@ java_library { } java_library { + name: "android_stubs_current_exportable.from-source", + static_libs: [ + "all-modules-public-stubs-exportable", + "android-non-updatable.stubs.exportable", + "private-stub-annotations-jar", + ], + defaults: ["android.jar_defaults"], +} + +java_library { name: "android_system_stubs_current.from-source", static_libs: [ "all-modules-system-stubs", @@ -470,6 +560,19 @@ java_library { ], defaults: [ "android.jar_defaults", + ], + visibility: ["//frameworks/base/services"], +} + +java_library { + name: "android_system_stubs_current_exportable.from-source", + static_libs: [ + "all-modules-system-stubs-exportable", + "android-non-updatable.stubs.exportable.system", + "private-stub-annotations-jar", + ], + defaults: [ + "android.jar_defaults", "android_stubs_dists_default", ], dist: { @@ -498,6 +601,23 @@ java_library { ], defaults: [ "android.jar_defaults", + ], + visibility: ["//frameworks/base/services"], +} + +java_library { + name: "android_test_stubs_current_exportable.from-source", + static_libs: [ + // Updatable modules do not have test APIs, but we want to include their SystemApis, like we + // include the SystemApi of framework-non-updatable-sources. + "all-updatable-modules-system-stubs-exportable", + // Non-updatable modules on the other hand can have test APIs, so include their test-stubs. + "all-non-updatable-modules-test-stubs-exportable", + "android-non-updatable.stubs.exportable.test", + "private-stub-annotations-jar", + ], + defaults: [ + "android.jar_defaults", "android_stubs_dists_default", ], dist: { @@ -505,6 +625,7 @@ java_library { }, } +// This module does not need to be copied to dist java_library { name: "android_test_frameworks_core_stubs_current.from-source", static_libs: [ @@ -513,24 +634,34 @@ java_library { ], defaults: [ "android.jar_defaults", - "android_stubs_dists_default", ], - dist: { - dir: "apistubs/android/test-core", - }, + visibility: ["//frameworks/base/services"], } java_library { name: "android_module_lib_stubs_current.from-source", defaults: [ "android.jar_defaults", - "android_stubs_dists_default", ], static_libs: [ "android-non-updatable.stubs.module_lib", "art.module.public.api.stubs.module_lib", "i18n.module.public.api.stubs", ], + visibility: ["//frameworks/base/services"], +} + +java_library { + name: "android_module_lib_stubs_current_exportable.from-source", + defaults: [ + "android.jar_defaults", + "android_stubs_dists_default", + ], + static_libs: [ + "android-non-updatable.stubs.exportable.module_lib", + "art.module.public.api.stubs.exportable.module_lib", + "i18n.module.public.api.stubs.exportable", + ], dist: { dir: "apistubs/android/module-lib", }, @@ -540,13 +671,26 @@ java_library { name: "android_system_server_stubs_current.from-source", defaults: [ "android.jar_defaults", - "android_stubs_dists_default", ], srcs: [":services-non-updatable-stubs"], installable: false, static_libs: [ "android_module_lib_stubs_current.from-source", ], + visibility: ["//frameworks/base/services"], +} + +java_library { + name: "android_system_server_stubs_current_exportable.from-source", + defaults: [ + "android.jar_defaults", + "android_stubs_dists_default", + ], + srcs: [":services-non-updatable-stubs{.exportable}"], + installable: false, + static_libs: [ + "android_module_lib_stubs_current_exportable.from-source", + ], dist: { dir: "apistubs/android/system-server", }, diff --git a/api/api.go b/api/api.go index b975c55c5af9..fa2be21db09f 100644 --- a/api/api.go +++ b/api/api.go @@ -204,6 +204,15 @@ func createMergedPublicStubs(ctx android.LoadHookContext, modules []string) { ctx.CreateModule(java.LibraryFactory, &props) } +func createMergedPublicExportableStubs(ctx android.LoadHookContext, modules []string) { + props := libraryProps{} + props.Name = proptools.StringPtr("all-modules-public-stubs-exportable") + props.Static_libs = transformArray(modules, "", ".stubs.exportable") + props.Sdk_version = proptools.StringPtr("module_current") + props.Visibility = []string{"//frameworks/base"} + ctx.CreateModule(java.LibraryFactory, &props) +} + func createMergedSystemStubs(ctx android.LoadHookContext, modules []string) { // First create the all-updatable-modules-system-stubs { @@ -228,6 +237,30 @@ func createMergedSystemStubs(ctx android.LoadHookContext, modules []string) { } } +func createMergedSystemExportableStubs(ctx android.LoadHookContext, modules []string) { + // First create the all-updatable-modules-system-stubs + { + updatable_modules := removeAll(modules, non_updatable_modules) + props := libraryProps{} + props.Name = proptools.StringPtr("all-updatable-modules-system-stubs-exportable") + props.Static_libs = transformArray(updatable_modules, "", ".stubs.exportable.system") + props.Sdk_version = proptools.StringPtr("module_current") + props.Visibility = []string{"//frameworks/base"} + ctx.CreateModule(java.LibraryFactory, &props) + } + // Now merge all-updatable-modules-system-stubs and stubs from non-updatable modules + // into all-modules-system-stubs. + { + props := libraryProps{} + props.Name = proptools.StringPtr("all-modules-system-stubs-exportable") + props.Static_libs = transformArray(non_updatable_modules, "", ".stubs.exportable.system") + props.Static_libs = append(props.Static_libs, "all-updatable-modules-system-stubs-exportable") + props.Sdk_version = proptools.StringPtr("module_current") + props.Visibility = []string{"//frameworks/base"} + ctx.CreateModule(java.LibraryFactory, &props) + } +} + func createMergedTestStubsForNonUpdatableModules(ctx android.LoadHookContext) { props := libraryProps{} props.Name = proptools.StringPtr("all-non-updatable-modules-test-stubs") @@ -237,6 +270,15 @@ func createMergedTestStubsForNonUpdatableModules(ctx android.LoadHookContext) { ctx.CreateModule(java.LibraryFactory, &props) } +func createMergedTestExportableStubsForNonUpdatableModules(ctx android.LoadHookContext) { + props := libraryProps{} + props.Name = proptools.StringPtr("all-non-updatable-modules-test-stubs-exportable") + props.Static_libs = transformArray(non_updatable_modules, "", ".stubs.exportable.test") + props.Sdk_version = proptools.StringPtr("module_current") + props.Visibility = []string{"//frameworks/base"} + ctx.CreateModule(java.LibraryFactory, &props) +} + func createMergedFrameworkImpl(ctx android.LoadHookContext, modules []string) { // This module is for the "framework-all" module, which should not include the core libraries. modules = removeAll(modules, core_libraries_modules) @@ -267,6 +309,19 @@ func createMergedFrameworkImpl(ctx android.LoadHookContext, modules []string) { } } +func createMergedFrameworkModuleLibExportableStubs(ctx android.LoadHookContext, modules []string) { + // The user of this module compiles against the "core" SDK and against non-updatable modules, + // so remove to avoid dupes. + modules = removeAll(modules, core_libraries_modules) + modules = removeAll(modules, non_updatable_modules) + props := libraryProps{} + props.Name = proptools.StringPtr("framework-updatable-stubs-module_libs_api-exportable") + props.Static_libs = transformArray(modules, "", ".stubs.exportable.module_lib") + props.Sdk_version = proptools.StringPtr("module_current") + props.Visibility = []string{"//frameworks/base"} + ctx.CreateModule(java.LibraryFactory, &props) +} + func createMergedFrameworkModuleLibStubs(ctx android.LoadHookContext, modules []string) { // The user of this module compiles against the "core" SDK and against non-updatable modules, // so remove to avoid dupes. @@ -382,6 +437,27 @@ func createFullApiLibraries(ctx android.LoadHookContext) { } } +func createFullExportableApiLibraries(ctx android.LoadHookContext) { + javaLibraryNames := []string{ + "android_stubs_current_exportable", + "android_system_stubs_current_exportable", + "android_test_stubs_current_exportable", + "android_module_lib_stubs_current_exportable", + "android_system_server_stubs_current_exportable", + } + + for _, libraryName := range javaLibraryNames { + props := libraryProps{} + props.Name = proptools.StringPtr(libraryName) + staticLib := libraryName + ".from-source" + props.Static_libs = []string{staticLib} + props.Defaults = []string{"android.jar_defaults"} + props.Visibility = []string{"//visibility:public"} + + ctx.CreateModule(java.LibraryFactory, &props) + } +} + func (a *CombinedApis) createInternalModules(ctx android.LoadHookContext) { bootclasspath := a.properties.Bootclasspath system_server_classpath := a.properties.System_server_classpath @@ -397,6 +473,11 @@ func (a *CombinedApis) createInternalModules(ctx android.LoadHookContext) { createMergedFrameworkModuleLibStubs(ctx, bootclasspath) createMergedFrameworkImpl(ctx, bootclasspath) + createMergedPublicExportableStubs(ctx, bootclasspath) + createMergedSystemExportableStubs(ctx, bootclasspath) + createMergedTestExportableStubsForNonUpdatableModules(ctx) + createMergedFrameworkModuleLibExportableStubs(ctx, bootclasspath) + createMergedAnnotationsFilegroups(ctx, bootclasspath, system_server_classpath) createPublicStubsSourceFilegroup(ctx, bootclasspath) @@ -404,6 +485,8 @@ func (a *CombinedApis) createInternalModules(ctx android.LoadHookContext) { createApiContributionDefaults(ctx, bootclasspath) createFullApiLibraries(ctx) + + createFullExportableApiLibraries(ctx) } func combinedApisModuleFactory() android.Module { diff --git a/boot/Android.bp b/boot/Android.bp index 8a3d35e2d0eb..228d060bf9cf 100644 --- a/boot/Android.bp +++ b/boot/Android.bp @@ -26,9 +26,11 @@ package { soong_config_module_type { name: "custom_platform_bootclasspath", module_type: "platform_bootclasspath", - config_namespace: "AUTO", + config_namespace: "bootclasspath", bool_variables: [ "car_bootclasspath_fragment", + "nfc_apex_bootclasspath_fragment", + "release_crashrecovery_module", ], properties: [ "fragments", @@ -155,6 +157,24 @@ custom_platform_bootclasspath { }, ], }, + nfc_apex_bootclasspath_fragment: { + fragments: [ + // only used if NFC mainline is enabled. + { + apex: "com.android.nfcservices", + module: "com.android.nfcservices-bootclasspath-fragment", + }, + ], + }, + release_crashrecovery_module: { + fragments: [ + // only used when crashrecovery is enabled + { + apex: "com.android.crashrecovery", + module: "com.android.crashrecovery-bootclasspath-fragment", + }, + ], + }, }, // Additional information needed by hidden api processing. diff --git a/boot/hiddenapi/hiddenapi-max-target-o.txt b/boot/hiddenapi/hiddenapi-max-target-o.txt index 3c16915cf71f..2d417ce752e1 100644 --- a/boot/hiddenapi/hiddenapi-max-target-o.txt +++ b/boot/hiddenapi/hiddenapi-max-target-o.txt @@ -33834,649 +33834,6 @@ Landroid/net/WifiLinkQualityInfo;->setRssi(I)V Landroid/net/WifiLinkQualityInfo;->setTxBad(J)V Landroid/net/WifiLinkQualityInfo;->setTxGood(J)V Landroid/net/WifiLinkQualityInfo;->setType(I)V -Landroid/nfc/ApduList;-><init>()V -Landroid/nfc/ApduList;-><init>(Landroid/os/Parcel;)V -Landroid/nfc/ApduList;->add([B)V -Landroid/nfc/ApduList;->commands:Ljava/util/ArrayList; -Landroid/nfc/ApduList;->CREATOR:Landroid/os/Parcelable$Creator; -Landroid/nfc/ApduList;->get()Ljava/util/List; -Landroid/nfc/BeamShareData;-><init>(Landroid/nfc/NdefMessage;[Landroid/net/Uri;Landroid/os/UserHandle;I)V -Landroid/nfc/BeamShareData;->CREATOR:Landroid/os/Parcelable$Creator; -Landroid/nfc/BeamShareData;->flags:I -Landroid/nfc/BeamShareData;->ndefMessage:Landroid/nfc/NdefMessage; -Landroid/nfc/BeamShareData;->uris:[Landroid/net/Uri; -Landroid/nfc/BeamShareData;->userHandle:Landroid/os/UserHandle; -Landroid/nfc/cardemulation/AidGroup;-><init>(Ljava/util/List;Ljava/lang/String;)V -Landroid/nfc/cardemulation/AidGroup;->isValidCategory(Ljava/lang/String;)Z -Landroid/nfc/cardemulation/AidGroup;->MAX_NUM_AIDS:I -Landroid/nfc/cardemulation/AidGroup;->TAG:Ljava/lang/String; -Landroid/nfc/cardemulation/ApduServiceInfo;->dump(Ljava/io/FileDescriptor;Ljava/io/PrintWriter;[Ljava/lang/String;)V -Landroid/nfc/cardemulation/ApduServiceInfo;->getAidGroups()Ljava/util/ArrayList; -Landroid/nfc/cardemulation/ApduServiceInfo;->getAids()Ljava/util/List; -Landroid/nfc/cardemulation/ApduServiceInfo;->getCategoryForAid(Ljava/lang/String;)Ljava/lang/String; -Landroid/nfc/cardemulation/ApduServiceInfo;->getComponent()Landroid/content/ComponentName; -Landroid/nfc/cardemulation/ApduServiceInfo;->getDynamicAidGroupForCategory(Ljava/lang/String;)Landroid/nfc/cardemulation/AidGroup; -Landroid/nfc/cardemulation/ApduServiceInfo;->getPrefixAids()Ljava/util/List; -Landroid/nfc/cardemulation/ApduServiceInfo;->getSubsetAids()Ljava/util/List; -Landroid/nfc/cardemulation/ApduServiceInfo;->hasCategory(Ljava/lang/String;)Z -Landroid/nfc/cardemulation/ApduServiceInfo;->loadAppLabel(Landroid/content/pm/PackageManager;)Ljava/lang/CharSequence; -Landroid/nfc/cardemulation/ApduServiceInfo;->loadIcon(Landroid/content/pm/PackageManager;)Landroid/graphics/drawable/Drawable; -Landroid/nfc/cardemulation/ApduServiceInfo;->loadLabel(Landroid/content/pm/PackageManager;)Ljava/lang/CharSequence; -Landroid/nfc/cardemulation/ApduServiceInfo;->mBannerResourceId:I -Landroid/nfc/cardemulation/ApduServiceInfo;->mDescription:Ljava/lang/String; -Landroid/nfc/cardemulation/ApduServiceInfo;->mOnHost:Z -Landroid/nfc/cardemulation/ApduServiceInfo;->mRequiresDeviceUnlock:Z -Landroid/nfc/cardemulation/ApduServiceInfo;->mSettingsActivityName:Ljava/lang/String; -Landroid/nfc/cardemulation/ApduServiceInfo;->mUid:I -Landroid/nfc/cardemulation/ApduServiceInfo;->removeDynamicAidGroupForCategory(Ljava/lang/String;)Z -Landroid/nfc/cardemulation/ApduServiceInfo;->setOrReplaceDynamicAidGroup(Landroid/nfc/cardemulation/AidGroup;)V -Landroid/nfc/cardemulation/ApduServiceInfo;->TAG:Ljava/lang/String; -Landroid/nfc/cardemulation/CardEmulation;-><init>(Landroid/content/Context;Landroid/nfc/INfcCardEmulation;)V -Landroid/nfc/cardemulation/CardEmulation;->getServices(Ljava/lang/String;)Ljava/util/List; -Landroid/nfc/cardemulation/CardEmulation;->isValidAid(Ljava/lang/String;)Z -Landroid/nfc/cardemulation/CardEmulation;->mContext:Landroid/content/Context; -Landroid/nfc/cardemulation/CardEmulation;->recoverService()V -Landroid/nfc/cardemulation/CardEmulation;->sCardEmus:Ljava/util/HashMap; -Landroid/nfc/cardemulation/CardEmulation;->setDefaultForNextTap(Landroid/content/ComponentName;)Z -Landroid/nfc/cardemulation/CardEmulation;->setDefaultServiceForCategory(Landroid/content/ComponentName;Ljava/lang/String;)Z -Landroid/nfc/cardemulation/CardEmulation;->sIsInitialized:Z -Landroid/nfc/cardemulation/CardEmulation;->sService:Landroid/nfc/INfcCardEmulation; -Landroid/nfc/cardemulation/CardEmulation;->TAG:Ljava/lang/String; -Landroid/nfc/cardemulation/HostApduService;->KEY_DATA:Ljava/lang/String; -Landroid/nfc/cardemulation/HostApduService;->mMessenger:Landroid/os/Messenger; -Landroid/nfc/cardemulation/HostApduService;->mNfcService:Landroid/os/Messenger; -Landroid/nfc/cardemulation/HostApduService;->MSG_COMMAND_APDU:I -Landroid/nfc/cardemulation/HostApduService;->MSG_DEACTIVATED:I -Landroid/nfc/cardemulation/HostApduService;->MSG_RESPONSE_APDU:I -Landroid/nfc/cardemulation/HostApduService;->MSG_UNHANDLED:I -Landroid/nfc/cardemulation/HostApduService;->TAG:Ljava/lang/String; -Landroid/nfc/cardemulation/HostNfcFService;->KEY_DATA:Ljava/lang/String; -Landroid/nfc/cardemulation/HostNfcFService;->KEY_MESSENGER:Ljava/lang/String; -Landroid/nfc/cardemulation/HostNfcFService;->mMessenger:Landroid/os/Messenger; -Landroid/nfc/cardemulation/HostNfcFService;->mNfcService:Landroid/os/Messenger; -Landroid/nfc/cardemulation/HostNfcFService;->MSG_COMMAND_PACKET:I -Landroid/nfc/cardemulation/HostNfcFService;->MSG_DEACTIVATED:I -Landroid/nfc/cardemulation/HostNfcFService;->MSG_RESPONSE_PACKET:I -Landroid/nfc/cardemulation/HostNfcFService;->TAG:Ljava/lang/String; -Landroid/nfc/cardemulation/NfcFCardEmulation;-><init>(Landroid/content/Context;Landroid/nfc/INfcFCardEmulation;)V -Landroid/nfc/cardemulation/NfcFCardEmulation;->getMaxNumOfRegisterableSystemCodes()I -Landroid/nfc/cardemulation/NfcFCardEmulation;->getNfcFServices()Ljava/util/List; -Landroid/nfc/cardemulation/NfcFCardEmulation;->isValidNfcid2(Ljava/lang/String;)Z -Landroid/nfc/cardemulation/NfcFCardEmulation;->isValidSystemCode(Ljava/lang/String;)Z -Landroid/nfc/cardemulation/NfcFCardEmulation;->mContext:Landroid/content/Context; -Landroid/nfc/cardemulation/NfcFCardEmulation;->recoverService()V -Landroid/nfc/cardemulation/NfcFCardEmulation;->sCardEmus:Ljava/util/HashMap; -Landroid/nfc/cardemulation/NfcFCardEmulation;->sIsInitialized:Z -Landroid/nfc/cardemulation/NfcFCardEmulation;->sService:Landroid/nfc/INfcFCardEmulation; -Landroid/nfc/cardemulation/NfcFCardEmulation;->TAG:Ljava/lang/String; -Landroid/nfc/cardemulation/NfcFServiceInfo;-><init>(Landroid/content/pm/PackageManager;Landroid/content/pm/ResolveInfo;)V -Landroid/nfc/cardemulation/NfcFServiceInfo;-><init>(Landroid/content/pm/ResolveInfo;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILjava/lang/String;)V -Landroid/nfc/cardemulation/NfcFServiceInfo;->CREATOR:Landroid/os/Parcelable$Creator; -Landroid/nfc/cardemulation/NfcFServiceInfo;->DEFAULT_T3T_PMM:Ljava/lang/String; -Landroid/nfc/cardemulation/NfcFServiceInfo;->dump(Ljava/io/FileDescriptor;Ljava/io/PrintWriter;[Ljava/lang/String;)V -Landroid/nfc/cardemulation/NfcFServiceInfo;->getComponent()Landroid/content/ComponentName; -Landroid/nfc/cardemulation/NfcFServiceInfo;->getDescription()Ljava/lang/String; -Landroid/nfc/cardemulation/NfcFServiceInfo;->getNfcid2()Ljava/lang/String; -Landroid/nfc/cardemulation/NfcFServiceInfo;->getSystemCode()Ljava/lang/String; -Landroid/nfc/cardemulation/NfcFServiceInfo;->getT3tPmm()Ljava/lang/String; -Landroid/nfc/cardemulation/NfcFServiceInfo;->getUid()I -Landroid/nfc/cardemulation/NfcFServiceInfo;->loadIcon(Landroid/content/pm/PackageManager;)Landroid/graphics/drawable/Drawable; -Landroid/nfc/cardemulation/NfcFServiceInfo;->loadLabel(Landroid/content/pm/PackageManager;)Ljava/lang/CharSequence; -Landroid/nfc/cardemulation/NfcFServiceInfo;->mDescription:Ljava/lang/String; -Landroid/nfc/cardemulation/NfcFServiceInfo;->mDynamicNfcid2:Ljava/lang/String; -Landroid/nfc/cardemulation/NfcFServiceInfo;->mDynamicSystemCode:Ljava/lang/String; -Landroid/nfc/cardemulation/NfcFServiceInfo;->mNfcid2:Ljava/lang/String; -Landroid/nfc/cardemulation/NfcFServiceInfo;->mService:Landroid/content/pm/ResolveInfo; -Landroid/nfc/cardemulation/NfcFServiceInfo;->mSystemCode:Ljava/lang/String; -Landroid/nfc/cardemulation/NfcFServiceInfo;->mT3tPmm:Ljava/lang/String; -Landroid/nfc/cardemulation/NfcFServiceInfo;->mUid:I -Landroid/nfc/cardemulation/NfcFServiceInfo;->setOrReplaceDynamicNfcid2(Ljava/lang/String;)V -Landroid/nfc/cardemulation/NfcFServiceInfo;->setOrReplaceDynamicSystemCode(Ljava/lang/String;)V -Landroid/nfc/cardemulation/NfcFServiceInfo;->TAG:Ljava/lang/String; -Landroid/nfc/ErrorCodes;-><init>()V -Landroid/nfc/ErrorCodes;->asString(I)Ljava/lang/String; -Landroid/nfc/ErrorCodes;->ERROR_BUFFER_TO_SMALL:I -Landroid/nfc/ErrorCodes;->ERROR_BUSY:I -Landroid/nfc/ErrorCodes;->ERROR_CANCELLED:I -Landroid/nfc/ErrorCodes;->ERROR_CONNECT:I -Landroid/nfc/ErrorCodes;->ERROR_DISCONNECT:I -Landroid/nfc/ErrorCodes;->ERROR_INSUFFICIENT_RESOURCES:I -Landroid/nfc/ErrorCodes;->ERROR_INVALID_PARAM:I -Landroid/nfc/ErrorCodes;->ERROR_IO:I -Landroid/nfc/ErrorCodes;->ERROR_NFC_ON:I -Landroid/nfc/ErrorCodes;->ERROR_NOT_INITIALIZED:I -Landroid/nfc/ErrorCodes;->ERROR_NOT_SUPPORTED:I -Landroid/nfc/ErrorCodes;->ERROR_NO_SE_CONNECTED:I -Landroid/nfc/ErrorCodes;->ERROR_READ:I -Landroid/nfc/ErrorCodes;->ERROR_SAP_USED:I -Landroid/nfc/ErrorCodes;->ERROR_SERVICE_NAME_USED:I -Landroid/nfc/ErrorCodes;->ERROR_SE_ALREADY_SELECTED:I -Landroid/nfc/ErrorCodes;->ERROR_SE_CONNECTED:I -Landroid/nfc/ErrorCodes;->ERROR_SOCKET_CREATION:I -Landroid/nfc/ErrorCodes;->ERROR_SOCKET_NOT_CONNECTED:I -Landroid/nfc/ErrorCodes;->ERROR_SOCKET_OPTIONS:I -Landroid/nfc/ErrorCodes;->ERROR_TIMEOUT:I -Landroid/nfc/ErrorCodes;->ERROR_WRITE:I -Landroid/nfc/ErrorCodes;->SUCCESS:I -Landroid/nfc/IAppCallback$Stub$Proxy;-><init>(Landroid/os/IBinder;)V -Landroid/nfc/IAppCallback$Stub$Proxy;->createBeamShareData(B)Landroid/nfc/BeamShareData; -Landroid/nfc/IAppCallback$Stub$Proxy;->getInterfaceDescriptor()Ljava/lang/String; -Landroid/nfc/IAppCallback$Stub$Proxy;->mRemote:Landroid/os/IBinder; -Landroid/nfc/IAppCallback$Stub$Proxy;->onNdefPushComplete(B)V -Landroid/nfc/IAppCallback$Stub$Proxy;->onTagDiscovered(Landroid/nfc/Tag;)V -Landroid/nfc/IAppCallback$Stub;-><init>()V -Landroid/nfc/IAppCallback$Stub;->asInterface(Landroid/os/IBinder;)Landroid/nfc/IAppCallback; -Landroid/nfc/IAppCallback$Stub;->DESCRIPTOR:Ljava/lang/String; -Landroid/nfc/IAppCallback$Stub;->TRANSACTION_createBeamShareData:I -Landroid/nfc/IAppCallback$Stub;->TRANSACTION_onNdefPushComplete:I -Landroid/nfc/IAppCallback$Stub;->TRANSACTION_onTagDiscovered:I -Landroid/nfc/IAppCallback;->createBeamShareData(B)Landroid/nfc/BeamShareData; -Landroid/nfc/IAppCallback;->onNdefPushComplete(B)V -Landroid/nfc/IAppCallback;->onTagDiscovered(Landroid/nfc/Tag;)V -Landroid/nfc/INfcAdapter$Stub$Proxy;-><init>(Landroid/os/IBinder;)V -Landroid/nfc/INfcAdapter$Stub$Proxy;->addNfcUnlockHandler(Landroid/nfc/INfcUnlockHandler;[I)V -Landroid/nfc/INfcAdapter$Stub$Proxy;->disable(Z)Z -Landroid/nfc/INfcAdapter$Stub$Proxy;->disableNdefPush()Z -Landroid/nfc/INfcAdapter$Stub$Proxy;->dispatch(Landroid/nfc/Tag;)V -Landroid/nfc/INfcAdapter$Stub$Proxy;->enable()Z -Landroid/nfc/INfcAdapter$Stub$Proxy;->enableNdefPush()Z -Landroid/nfc/INfcAdapter$Stub$Proxy;->getInterfaceDescriptor()Ljava/lang/String; -Landroid/nfc/INfcAdapter$Stub$Proxy;->getNfcAdapterExtrasInterface(Ljava/lang/String;)Landroid/nfc/INfcAdapterExtras; -Landroid/nfc/INfcAdapter$Stub$Proxy;->getNfcCardEmulationInterface()Landroid/nfc/INfcCardEmulation; -Landroid/nfc/INfcAdapter$Stub$Proxy;->getNfcDtaInterface(Ljava/lang/String;)Landroid/nfc/INfcDta; -Landroid/nfc/INfcAdapter$Stub$Proxy;->getNfcFCardEmulationInterface()Landroid/nfc/INfcFCardEmulation; -Landroid/nfc/INfcAdapter$Stub$Proxy;->getNfcTagInterface()Landroid/nfc/INfcTag; -Landroid/nfc/INfcAdapter$Stub$Proxy;->getState()I -Landroid/nfc/INfcAdapter$Stub$Proxy;->ignore(IILandroid/nfc/ITagRemovedCallback;)Z -Landroid/nfc/INfcAdapter$Stub$Proxy;->invokeBeam()V -Landroid/nfc/INfcAdapter$Stub$Proxy;->invokeBeamInternal(Landroid/nfc/BeamShareData;)V -Landroid/nfc/INfcAdapter$Stub$Proxy;->isNdefPushEnabled()Z -Landroid/nfc/INfcAdapter$Stub$Proxy;->mRemote:Landroid/os/IBinder; -Landroid/nfc/INfcAdapter$Stub$Proxy;->pausePolling(I)V -Landroid/nfc/INfcAdapter$Stub$Proxy;->removeNfcUnlockHandler(Landroid/nfc/INfcUnlockHandler;)V -Landroid/nfc/INfcAdapter$Stub$Proxy;->resumePolling()V -Landroid/nfc/INfcAdapter$Stub$Proxy;->setAppCallback(Landroid/nfc/IAppCallback;)V -Landroid/nfc/INfcAdapter$Stub$Proxy;->setForegroundDispatch(Landroid/app/PendingIntent;[Landroid/content/IntentFilter;Landroid/nfc/TechListParcel;)V -Landroid/nfc/INfcAdapter$Stub$Proxy;->setP2pModes(II)V -Landroid/nfc/INfcAdapter$Stub$Proxy;->setReaderMode(Landroid/os/IBinder;Landroid/nfc/IAppCallback;ILandroid/os/Bundle;)V -Landroid/nfc/INfcAdapter$Stub$Proxy;->verifyNfcPermission()V -Landroid/nfc/INfcAdapter$Stub;-><init>()V -Landroid/nfc/INfcAdapter$Stub;->asInterface(Landroid/os/IBinder;)Landroid/nfc/INfcAdapter; -Landroid/nfc/INfcAdapter$Stub;->DESCRIPTOR:Ljava/lang/String; -Landroid/nfc/INfcAdapter$Stub;->TRANSACTION_addNfcUnlockHandler:I -Landroid/nfc/INfcAdapter$Stub;->TRANSACTION_disable:I -Landroid/nfc/INfcAdapter$Stub;->TRANSACTION_disableNdefPush:I -Landroid/nfc/INfcAdapter$Stub;->TRANSACTION_dispatch:I -Landroid/nfc/INfcAdapter$Stub;->TRANSACTION_enableNdefPush:I -Landroid/nfc/INfcAdapter$Stub;->TRANSACTION_getNfcAdapterExtrasInterface:I -Landroid/nfc/INfcAdapter$Stub;->TRANSACTION_getNfcCardEmulationInterface:I -Landroid/nfc/INfcAdapter$Stub;->TRANSACTION_getNfcDtaInterface:I -Landroid/nfc/INfcAdapter$Stub;->TRANSACTION_getNfcFCardEmulationInterface:I -Landroid/nfc/INfcAdapter$Stub;->TRANSACTION_getNfcTagInterface:I -Landroid/nfc/INfcAdapter$Stub;->TRANSACTION_getState:I -Landroid/nfc/INfcAdapter$Stub;->TRANSACTION_ignore:I -Landroid/nfc/INfcAdapter$Stub;->TRANSACTION_invokeBeam:I -Landroid/nfc/INfcAdapter$Stub;->TRANSACTION_invokeBeamInternal:I -Landroid/nfc/INfcAdapter$Stub;->TRANSACTION_isNdefPushEnabled:I -Landroid/nfc/INfcAdapter$Stub;->TRANSACTION_pausePolling:I -Landroid/nfc/INfcAdapter$Stub;->TRANSACTION_removeNfcUnlockHandler:I -Landroid/nfc/INfcAdapter$Stub;->TRANSACTION_resumePolling:I -Landroid/nfc/INfcAdapter$Stub;->TRANSACTION_setAppCallback:I -Landroid/nfc/INfcAdapter$Stub;->TRANSACTION_setForegroundDispatch:I -Landroid/nfc/INfcAdapter$Stub;->TRANSACTION_setP2pModes:I -Landroid/nfc/INfcAdapter$Stub;->TRANSACTION_setReaderMode:I -Landroid/nfc/INfcAdapter$Stub;->TRANSACTION_verifyNfcPermission:I -Landroid/nfc/INfcAdapter;->addNfcUnlockHandler(Landroid/nfc/INfcUnlockHandler;[I)V -Landroid/nfc/INfcAdapter;->disable(Z)Z -Landroid/nfc/INfcAdapter;->disableNdefPush()Z -Landroid/nfc/INfcAdapter;->dispatch(Landroid/nfc/Tag;)V -Landroid/nfc/INfcAdapter;->enable()Z -Landroid/nfc/INfcAdapter;->enableNdefPush()Z -Landroid/nfc/INfcAdapter;->getNfcAdapterExtrasInterface(Ljava/lang/String;)Landroid/nfc/INfcAdapterExtras; -Landroid/nfc/INfcAdapter;->getNfcCardEmulationInterface()Landroid/nfc/INfcCardEmulation; -Landroid/nfc/INfcAdapter;->getNfcDtaInterface(Ljava/lang/String;)Landroid/nfc/INfcDta; -Landroid/nfc/INfcAdapter;->getNfcFCardEmulationInterface()Landroid/nfc/INfcFCardEmulation; -Landroid/nfc/INfcAdapter;->getNfcTagInterface()Landroid/nfc/INfcTag; -Landroid/nfc/INfcAdapter;->getState()I -Landroid/nfc/INfcAdapter;->ignore(IILandroid/nfc/ITagRemovedCallback;)Z -Landroid/nfc/INfcAdapter;->invokeBeam()V -Landroid/nfc/INfcAdapter;->invokeBeamInternal(Landroid/nfc/BeamShareData;)V -Landroid/nfc/INfcAdapter;->isNdefPushEnabled()Z -Landroid/nfc/INfcAdapter;->pausePolling(I)V -Landroid/nfc/INfcAdapter;->removeNfcUnlockHandler(Landroid/nfc/INfcUnlockHandler;)V -Landroid/nfc/INfcAdapter;->resumePolling()V -Landroid/nfc/INfcAdapter;->setAppCallback(Landroid/nfc/IAppCallback;)V -Landroid/nfc/INfcAdapter;->setForegroundDispatch(Landroid/app/PendingIntent;[Landroid/content/IntentFilter;Landroid/nfc/TechListParcel;)V -Landroid/nfc/INfcAdapter;->setP2pModes(II)V -Landroid/nfc/INfcAdapter;->setReaderMode(Landroid/os/IBinder;Landroid/nfc/IAppCallback;ILandroid/os/Bundle;)V -Landroid/nfc/INfcAdapter;->verifyNfcPermission()V -Landroid/nfc/INfcAdapterExtras$Stub$Proxy;-><init>(Landroid/os/IBinder;)V -Landroid/nfc/INfcAdapterExtras$Stub$Proxy;->authenticate(Ljava/lang/String;[B)V -Landroid/nfc/INfcAdapterExtras$Stub$Proxy;->close(Ljava/lang/String;Landroid/os/IBinder;)Landroid/os/Bundle; -Landroid/nfc/INfcAdapterExtras$Stub$Proxy;->getCardEmulationRoute(Ljava/lang/String;)I -Landroid/nfc/INfcAdapterExtras$Stub$Proxy;->getDriverName(Ljava/lang/String;)Ljava/lang/String; -Landroid/nfc/INfcAdapterExtras$Stub$Proxy;->getInterfaceDescriptor()Ljava/lang/String; -Landroid/nfc/INfcAdapterExtras$Stub$Proxy;->mRemote:Landroid/os/IBinder; -Landroid/nfc/INfcAdapterExtras$Stub$Proxy;->open(Ljava/lang/String;Landroid/os/IBinder;)Landroid/os/Bundle; -Landroid/nfc/INfcAdapterExtras$Stub$Proxy;->setCardEmulationRoute(Ljava/lang/String;I)V -Landroid/nfc/INfcAdapterExtras$Stub$Proxy;->transceive(Ljava/lang/String;[B)Landroid/os/Bundle; -Landroid/nfc/INfcAdapterExtras$Stub;-><init>()V -Landroid/nfc/INfcAdapterExtras$Stub;->asInterface(Landroid/os/IBinder;)Landroid/nfc/INfcAdapterExtras; -Landroid/nfc/INfcAdapterExtras$Stub;->DESCRIPTOR:Ljava/lang/String; -Landroid/nfc/INfcAdapterExtras$Stub;->TRANSACTION_authenticate:I -Landroid/nfc/INfcAdapterExtras$Stub;->TRANSACTION_close:I -Landroid/nfc/INfcAdapterExtras$Stub;->TRANSACTION_getCardEmulationRoute:I -Landroid/nfc/INfcAdapterExtras$Stub;->TRANSACTION_getDriverName:I -Landroid/nfc/INfcAdapterExtras$Stub;->TRANSACTION_open:I -Landroid/nfc/INfcAdapterExtras$Stub;->TRANSACTION_setCardEmulationRoute:I -Landroid/nfc/INfcAdapterExtras$Stub;->TRANSACTION_transceive:I -Landroid/nfc/INfcCardEmulation$Stub$Proxy;-><init>(Landroid/os/IBinder;)V -Landroid/nfc/INfcCardEmulation$Stub$Proxy;->getAidGroupForService(ILandroid/content/ComponentName;Ljava/lang/String;)Landroid/nfc/cardemulation/AidGroup; -Landroid/nfc/INfcCardEmulation$Stub$Proxy;->getInterfaceDescriptor()Ljava/lang/String; -Landroid/nfc/INfcCardEmulation$Stub$Proxy;->getServices(ILjava/lang/String;)Ljava/util/List; -Landroid/nfc/INfcCardEmulation$Stub$Proxy;->isDefaultServiceForAid(ILandroid/content/ComponentName;Ljava/lang/String;)Z -Landroid/nfc/INfcCardEmulation$Stub$Proxy;->isDefaultServiceForCategory(ILandroid/content/ComponentName;Ljava/lang/String;)Z -Landroid/nfc/INfcCardEmulation$Stub$Proxy;->mRemote:Landroid/os/IBinder; -Landroid/nfc/INfcCardEmulation$Stub$Proxy;->registerAidGroupForService(ILandroid/content/ComponentName;Landroid/nfc/cardemulation/AidGroup;)Z -Landroid/nfc/INfcCardEmulation$Stub$Proxy;->removeAidGroupForService(ILandroid/content/ComponentName;Ljava/lang/String;)Z -Landroid/nfc/INfcCardEmulation$Stub$Proxy;->setDefaultForNextTap(ILandroid/content/ComponentName;)Z -Landroid/nfc/INfcCardEmulation$Stub$Proxy;->setDefaultServiceForCategory(ILandroid/content/ComponentName;Ljava/lang/String;)Z -Landroid/nfc/INfcCardEmulation$Stub$Proxy;->setPreferredService(Landroid/content/ComponentName;)Z -Landroid/nfc/INfcCardEmulation$Stub$Proxy;->supportsAidPrefixRegistration()Z -Landroid/nfc/INfcCardEmulation$Stub$Proxy;->unsetPreferredService()Z -Landroid/nfc/INfcCardEmulation$Stub;-><init>()V -Landroid/nfc/INfcCardEmulation$Stub;->asInterface(Landroid/os/IBinder;)Landroid/nfc/INfcCardEmulation; -Landroid/nfc/INfcCardEmulation$Stub;->DESCRIPTOR:Ljava/lang/String; -Landroid/nfc/INfcCardEmulation$Stub;->TRANSACTION_getAidGroupForService:I -Landroid/nfc/INfcCardEmulation$Stub;->TRANSACTION_getServices:I -Landroid/nfc/INfcCardEmulation$Stub;->TRANSACTION_isDefaultServiceForAid:I -Landroid/nfc/INfcCardEmulation$Stub;->TRANSACTION_isDefaultServiceForCategory:I -Landroid/nfc/INfcCardEmulation$Stub;->TRANSACTION_registerAidGroupForService:I -Landroid/nfc/INfcCardEmulation$Stub;->TRANSACTION_removeAidGroupForService:I -Landroid/nfc/INfcCardEmulation$Stub;->TRANSACTION_setDefaultForNextTap:I -Landroid/nfc/INfcCardEmulation$Stub;->TRANSACTION_setDefaultServiceForCategory:I -Landroid/nfc/INfcCardEmulation$Stub;->TRANSACTION_setPreferredService:I -Landroid/nfc/INfcCardEmulation$Stub;->TRANSACTION_supportsAidPrefixRegistration:I -Landroid/nfc/INfcCardEmulation$Stub;->TRANSACTION_unsetPreferredService:I -Landroid/nfc/INfcCardEmulation;->getAidGroupForService(ILandroid/content/ComponentName;Ljava/lang/String;)Landroid/nfc/cardemulation/AidGroup; -Landroid/nfc/INfcCardEmulation;->getServices(ILjava/lang/String;)Ljava/util/List; -Landroid/nfc/INfcCardEmulation;->isDefaultServiceForAid(ILandroid/content/ComponentName;Ljava/lang/String;)Z -Landroid/nfc/INfcCardEmulation;->isDefaultServiceForCategory(ILandroid/content/ComponentName;Ljava/lang/String;)Z -Landroid/nfc/INfcCardEmulation;->registerAidGroupForService(ILandroid/content/ComponentName;Landroid/nfc/cardemulation/AidGroup;)Z -Landroid/nfc/INfcCardEmulation;->removeAidGroupForService(ILandroid/content/ComponentName;Ljava/lang/String;)Z -Landroid/nfc/INfcCardEmulation;->setDefaultForNextTap(ILandroid/content/ComponentName;)Z -Landroid/nfc/INfcCardEmulation;->setDefaultServiceForCategory(ILandroid/content/ComponentName;Ljava/lang/String;)Z -Landroid/nfc/INfcCardEmulation;->setPreferredService(Landroid/content/ComponentName;)Z -Landroid/nfc/INfcCardEmulation;->supportsAidPrefixRegistration()Z -Landroid/nfc/INfcCardEmulation;->unsetPreferredService()Z -Landroid/nfc/INfcDta$Stub$Proxy;-><init>(Landroid/os/IBinder;)V -Landroid/nfc/INfcDta$Stub$Proxy;->disableClient()V -Landroid/nfc/INfcDta$Stub$Proxy;->disableDta()V -Landroid/nfc/INfcDta$Stub$Proxy;->disableServer()V -Landroid/nfc/INfcDta$Stub$Proxy;->enableClient(Ljava/lang/String;III)Z -Landroid/nfc/INfcDta$Stub$Proxy;->enableDta()V -Landroid/nfc/INfcDta$Stub$Proxy;->enableServer(Ljava/lang/String;IIII)Z -Landroid/nfc/INfcDta$Stub$Proxy;->getInterfaceDescriptor()Ljava/lang/String; -Landroid/nfc/INfcDta$Stub$Proxy;->mRemote:Landroid/os/IBinder; -Landroid/nfc/INfcDta$Stub$Proxy;->registerMessageService(Ljava/lang/String;)Z -Landroid/nfc/INfcDta$Stub;-><init>()V -Landroid/nfc/INfcDta$Stub;->asInterface(Landroid/os/IBinder;)Landroid/nfc/INfcDta; -Landroid/nfc/INfcDta$Stub;->DESCRIPTOR:Ljava/lang/String; -Landroid/nfc/INfcDta$Stub;->TRANSACTION_disableClient:I -Landroid/nfc/INfcDta$Stub;->TRANSACTION_disableDta:I -Landroid/nfc/INfcDta$Stub;->TRANSACTION_disableServer:I -Landroid/nfc/INfcDta$Stub;->TRANSACTION_enableClient:I -Landroid/nfc/INfcDta$Stub;->TRANSACTION_enableDta:I -Landroid/nfc/INfcDta$Stub;->TRANSACTION_enableServer:I -Landroid/nfc/INfcDta$Stub;->TRANSACTION_registerMessageService:I -Landroid/nfc/INfcDta;->disableClient()V -Landroid/nfc/INfcDta;->disableDta()V -Landroid/nfc/INfcDta;->disableServer()V -Landroid/nfc/INfcDta;->enableClient(Ljava/lang/String;III)Z -Landroid/nfc/INfcDta;->enableDta()V -Landroid/nfc/INfcDta;->enableServer(Ljava/lang/String;IIII)Z -Landroid/nfc/INfcDta;->registerMessageService(Ljava/lang/String;)Z -Landroid/nfc/INfcFCardEmulation$Stub$Proxy;-><init>(Landroid/os/IBinder;)V -Landroid/nfc/INfcFCardEmulation$Stub$Proxy;->disableNfcFForegroundService()Z -Landroid/nfc/INfcFCardEmulation$Stub$Proxy;->enableNfcFForegroundService(Landroid/content/ComponentName;)Z -Landroid/nfc/INfcFCardEmulation$Stub$Proxy;->getInterfaceDescriptor()Ljava/lang/String; -Landroid/nfc/INfcFCardEmulation$Stub$Proxy;->getMaxNumOfRegisterableSystemCodes()I -Landroid/nfc/INfcFCardEmulation$Stub$Proxy;->getNfcFServices(I)Ljava/util/List; -Landroid/nfc/INfcFCardEmulation$Stub$Proxy;->getNfcid2ForService(ILandroid/content/ComponentName;)Ljava/lang/String; -Landroid/nfc/INfcFCardEmulation$Stub$Proxy;->getSystemCodeForService(ILandroid/content/ComponentName;)Ljava/lang/String; -Landroid/nfc/INfcFCardEmulation$Stub$Proxy;->mRemote:Landroid/os/IBinder; -Landroid/nfc/INfcFCardEmulation$Stub$Proxy;->registerSystemCodeForService(ILandroid/content/ComponentName;Ljava/lang/String;)Z -Landroid/nfc/INfcFCardEmulation$Stub$Proxy;->removeSystemCodeForService(ILandroid/content/ComponentName;)Z -Landroid/nfc/INfcFCardEmulation$Stub$Proxy;->setNfcid2ForService(ILandroid/content/ComponentName;Ljava/lang/String;)Z -Landroid/nfc/INfcFCardEmulation$Stub;-><init>()V -Landroid/nfc/INfcFCardEmulation$Stub;->asInterface(Landroid/os/IBinder;)Landroid/nfc/INfcFCardEmulation; -Landroid/nfc/INfcFCardEmulation$Stub;->DESCRIPTOR:Ljava/lang/String; -Landroid/nfc/INfcFCardEmulation$Stub;->TRANSACTION_disableNfcFForegroundService:I -Landroid/nfc/INfcFCardEmulation$Stub;->TRANSACTION_enableNfcFForegroundService:I -Landroid/nfc/INfcFCardEmulation$Stub;->TRANSACTION_getMaxNumOfRegisterableSystemCodes:I -Landroid/nfc/INfcFCardEmulation$Stub;->TRANSACTION_getNfcFServices:I -Landroid/nfc/INfcFCardEmulation$Stub;->TRANSACTION_getNfcid2ForService:I -Landroid/nfc/INfcFCardEmulation$Stub;->TRANSACTION_getSystemCodeForService:I -Landroid/nfc/INfcFCardEmulation$Stub;->TRANSACTION_registerSystemCodeForService:I -Landroid/nfc/INfcFCardEmulation$Stub;->TRANSACTION_removeSystemCodeForService:I -Landroid/nfc/INfcFCardEmulation$Stub;->TRANSACTION_setNfcid2ForService:I -Landroid/nfc/INfcFCardEmulation;->disableNfcFForegroundService()Z -Landroid/nfc/INfcFCardEmulation;->enableNfcFForegroundService(Landroid/content/ComponentName;)Z -Landroid/nfc/INfcFCardEmulation;->getMaxNumOfRegisterableSystemCodes()I -Landroid/nfc/INfcFCardEmulation;->getNfcFServices(I)Ljava/util/List; -Landroid/nfc/INfcFCardEmulation;->getNfcid2ForService(ILandroid/content/ComponentName;)Ljava/lang/String; -Landroid/nfc/INfcFCardEmulation;->getSystemCodeForService(ILandroid/content/ComponentName;)Ljava/lang/String; -Landroid/nfc/INfcFCardEmulation;->registerSystemCodeForService(ILandroid/content/ComponentName;Ljava/lang/String;)Z -Landroid/nfc/INfcFCardEmulation;->removeSystemCodeForService(ILandroid/content/ComponentName;)Z -Landroid/nfc/INfcFCardEmulation;->setNfcid2ForService(ILandroid/content/ComponentName;Ljava/lang/String;)Z -Landroid/nfc/INfcTag$Stub$Proxy;-><init>(Landroid/os/IBinder;)V -Landroid/nfc/INfcTag$Stub$Proxy;->canMakeReadOnly(I)Z -Landroid/nfc/INfcTag$Stub$Proxy;->connect(II)I -Landroid/nfc/INfcTag$Stub$Proxy;->formatNdef(I[B)I -Landroid/nfc/INfcTag$Stub$Proxy;->getExtendedLengthApdusSupported()Z -Landroid/nfc/INfcTag$Stub$Proxy;->getInterfaceDescriptor()Ljava/lang/String; -Landroid/nfc/INfcTag$Stub$Proxy;->getMaxTransceiveLength(I)I -Landroid/nfc/INfcTag$Stub$Proxy;->getTechList(I)[I -Landroid/nfc/INfcTag$Stub$Proxy;->getTimeout(I)I -Landroid/nfc/INfcTag$Stub$Proxy;->isNdef(I)Z -Landroid/nfc/INfcTag$Stub$Proxy;->isPresent(I)Z -Landroid/nfc/INfcTag$Stub$Proxy;->mRemote:Landroid/os/IBinder; -Landroid/nfc/INfcTag$Stub$Proxy;->ndefIsWritable(I)Z -Landroid/nfc/INfcTag$Stub$Proxy;->ndefMakeReadOnly(I)I -Landroid/nfc/INfcTag$Stub$Proxy;->ndefRead(I)Landroid/nfc/NdefMessage; -Landroid/nfc/INfcTag$Stub$Proxy;->ndefWrite(ILandroid/nfc/NdefMessage;)I -Landroid/nfc/INfcTag$Stub$Proxy;->reconnect(I)I -Landroid/nfc/INfcTag$Stub$Proxy;->rediscover(I)Landroid/nfc/Tag; -Landroid/nfc/INfcTag$Stub$Proxy;->resetTimeouts()V -Landroid/nfc/INfcTag$Stub$Proxy;->setTimeout(II)I -Landroid/nfc/INfcTag$Stub$Proxy;->transceive(I[BZ)Landroid/nfc/TransceiveResult; -Landroid/nfc/INfcTag$Stub;-><init>()V -Landroid/nfc/INfcTag$Stub;->asInterface(Landroid/os/IBinder;)Landroid/nfc/INfcTag; -Landroid/nfc/INfcTag$Stub;->DESCRIPTOR:Ljava/lang/String; -Landroid/nfc/INfcTag$Stub;->TRANSACTION_canMakeReadOnly:I -Landroid/nfc/INfcTag$Stub;->TRANSACTION_connect:I -Landroid/nfc/INfcTag$Stub;->TRANSACTION_formatNdef:I -Landroid/nfc/INfcTag$Stub;->TRANSACTION_getExtendedLengthApdusSupported:I -Landroid/nfc/INfcTag$Stub;->TRANSACTION_getMaxTransceiveLength:I -Landroid/nfc/INfcTag$Stub;->TRANSACTION_getTechList:I -Landroid/nfc/INfcTag$Stub;->TRANSACTION_getTimeout:I -Landroid/nfc/INfcTag$Stub;->TRANSACTION_isNdef:I -Landroid/nfc/INfcTag$Stub;->TRANSACTION_isPresent:I -Landroid/nfc/INfcTag$Stub;->TRANSACTION_ndefIsWritable:I -Landroid/nfc/INfcTag$Stub;->TRANSACTION_ndefMakeReadOnly:I -Landroid/nfc/INfcTag$Stub;->TRANSACTION_ndefRead:I -Landroid/nfc/INfcTag$Stub;->TRANSACTION_ndefWrite:I -Landroid/nfc/INfcTag$Stub;->TRANSACTION_reconnect:I -Landroid/nfc/INfcTag$Stub;->TRANSACTION_rediscover:I -Landroid/nfc/INfcTag$Stub;->TRANSACTION_resetTimeouts:I -Landroid/nfc/INfcTag$Stub;->TRANSACTION_setTimeout:I -Landroid/nfc/INfcTag$Stub;->TRANSACTION_transceive:I -Landroid/nfc/INfcTag;->canMakeReadOnly(I)Z -Landroid/nfc/INfcTag;->connect(II)I -Landroid/nfc/INfcTag;->formatNdef(I[B)I -Landroid/nfc/INfcTag;->getExtendedLengthApdusSupported()Z -Landroid/nfc/INfcTag;->getMaxTransceiveLength(I)I -Landroid/nfc/INfcTag;->getTechList(I)[I -Landroid/nfc/INfcTag;->getTimeout(I)I -Landroid/nfc/INfcTag;->isNdef(I)Z -Landroid/nfc/INfcTag;->isPresent(I)Z -Landroid/nfc/INfcTag;->ndefIsWritable(I)Z -Landroid/nfc/INfcTag;->ndefMakeReadOnly(I)I -Landroid/nfc/INfcTag;->ndefRead(I)Landroid/nfc/NdefMessage; -Landroid/nfc/INfcTag;->ndefWrite(ILandroid/nfc/NdefMessage;)I -Landroid/nfc/INfcTag;->reconnect(I)I -Landroid/nfc/INfcTag;->rediscover(I)Landroid/nfc/Tag; -Landroid/nfc/INfcTag;->resetTimeouts()V -Landroid/nfc/INfcTag;->setTimeout(II)I -Landroid/nfc/INfcTag;->transceive(I[BZ)Landroid/nfc/TransceiveResult; -Landroid/nfc/INfcUnlockHandler$Stub$Proxy;-><init>(Landroid/os/IBinder;)V -Landroid/nfc/INfcUnlockHandler$Stub$Proxy;->getInterfaceDescriptor()Ljava/lang/String; -Landroid/nfc/INfcUnlockHandler$Stub$Proxy;->mRemote:Landroid/os/IBinder; -Landroid/nfc/INfcUnlockHandler$Stub$Proxy;->onUnlockAttempted(Landroid/nfc/Tag;)Z -Landroid/nfc/INfcUnlockHandler$Stub;-><init>()V -Landroid/nfc/INfcUnlockHandler$Stub;->asInterface(Landroid/os/IBinder;)Landroid/nfc/INfcUnlockHandler; -Landroid/nfc/INfcUnlockHandler$Stub;->DESCRIPTOR:Ljava/lang/String; -Landroid/nfc/INfcUnlockHandler$Stub;->TRANSACTION_onUnlockAttempted:I -Landroid/nfc/INfcUnlockHandler;->onUnlockAttempted(Landroid/nfc/Tag;)Z -Landroid/nfc/ITagRemovedCallback$Stub$Proxy;-><init>(Landroid/os/IBinder;)V -Landroid/nfc/ITagRemovedCallback$Stub$Proxy;->getInterfaceDescriptor()Ljava/lang/String; -Landroid/nfc/ITagRemovedCallback$Stub$Proxy;->mRemote:Landroid/os/IBinder; -Landroid/nfc/ITagRemovedCallback$Stub$Proxy;->onTagRemoved()V -Landroid/nfc/ITagRemovedCallback$Stub;-><init>()V -Landroid/nfc/ITagRemovedCallback$Stub;->asInterface(Landroid/os/IBinder;)Landroid/nfc/ITagRemovedCallback; -Landroid/nfc/ITagRemovedCallback$Stub;->DESCRIPTOR:Ljava/lang/String; -Landroid/nfc/ITagRemovedCallback$Stub;->TRANSACTION_onTagRemoved:I -Landroid/nfc/ITagRemovedCallback;->onTagRemoved()V -Landroid/nfc/NdefMessage;->mRecords:[Landroid/nfc/NdefRecord; -Landroid/nfc/NdefRecord;->bytesToString([B)Ljava/lang/StringBuilder; -Landroid/nfc/NdefRecord;->EMPTY_BYTE_ARRAY:[B -Landroid/nfc/NdefRecord;->ensureSanePayloadSize(J)V -Landroid/nfc/NdefRecord;->FLAG_CF:B -Landroid/nfc/NdefRecord;->FLAG_IL:B -Landroid/nfc/NdefRecord;->FLAG_MB:B -Landroid/nfc/NdefRecord;->FLAG_ME:B -Landroid/nfc/NdefRecord;->FLAG_SR:B -Landroid/nfc/NdefRecord;->getByteLength()I -Landroid/nfc/NdefRecord;->MAX_PAYLOAD_SIZE:I -Landroid/nfc/NdefRecord;->mPayload:[B -Landroid/nfc/NdefRecord;->mTnf:S -Landroid/nfc/NdefRecord;->mType:[B -Landroid/nfc/NdefRecord;->parse(Ljava/nio/ByteBuffer;Z)[Landroid/nfc/NdefRecord; -Landroid/nfc/NdefRecord;->parseWktUri()Landroid/net/Uri; -Landroid/nfc/NdefRecord;->RTD_ANDROID_APP:[B -Landroid/nfc/NdefRecord;->TNF_RESERVED:S -Landroid/nfc/NdefRecord;->toUri(Z)Landroid/net/Uri; -Landroid/nfc/NdefRecord;->URI_PREFIX_MAP:[Ljava/lang/String; -Landroid/nfc/NdefRecord;->validateTnf(S[B[B[B)Ljava/lang/String; -Landroid/nfc/NdefRecord;->writeToByteBuffer(Ljava/nio/ByteBuffer;ZZ)V -Landroid/nfc/NfcActivityManager$NfcActivityState;->activity:Landroid/app/Activity; -Landroid/nfc/NfcActivityManager$NfcActivityState;->destroy()V -Landroid/nfc/NfcActivityManager$NfcActivityState;->flags:I -Landroid/nfc/NfcActivityManager$NfcActivityState;->ndefMessage:Landroid/nfc/NdefMessage; -Landroid/nfc/NfcActivityManager$NfcActivityState;->ndefMessageCallback:Landroid/nfc/NfcAdapter$CreateNdefMessageCallback; -Landroid/nfc/NfcActivityManager$NfcActivityState;->onNdefPushCompleteCallback:Landroid/nfc/NfcAdapter$OnNdefPushCompleteCallback; -Landroid/nfc/NfcActivityManager$NfcActivityState;->readerCallback:Landroid/nfc/NfcAdapter$ReaderCallback; -Landroid/nfc/NfcActivityManager$NfcActivityState;->readerModeExtras:Landroid/os/Bundle; -Landroid/nfc/NfcActivityManager$NfcActivityState;->readerModeFlags:I -Landroid/nfc/NfcActivityManager$NfcActivityState;->resumed:Z -Landroid/nfc/NfcActivityManager$NfcActivityState;->token:Landroid/os/Binder; -Landroid/nfc/NfcActivityManager$NfcActivityState;->uriCallback:Landroid/nfc/NfcAdapter$CreateBeamUrisCallback; -Landroid/nfc/NfcActivityManager$NfcActivityState;->uris:[Landroid/net/Uri; -Landroid/nfc/NfcActivityManager$NfcApplicationState;->app:Landroid/app/Application; -Landroid/nfc/NfcActivityManager$NfcApplicationState;->refCount:I -Landroid/nfc/NfcActivityManager$NfcApplicationState;->register()V -Landroid/nfc/NfcActivityManager$NfcApplicationState;->unregister()V -Landroid/nfc/NfcActivityManager;-><init>(Landroid/nfc/NfcAdapter;)V -Landroid/nfc/NfcActivityManager;->createBeamShareData(B)Landroid/nfc/BeamShareData; -Landroid/nfc/NfcActivityManager;->DBG:Ljava/lang/Boolean; -Landroid/nfc/NfcActivityManager;->destroyActivityState(Landroid/app/Activity;)V -Landroid/nfc/NfcActivityManager;->disableReaderMode(Landroid/app/Activity;)V -Landroid/nfc/NfcActivityManager;->enableReaderMode(Landroid/app/Activity;Landroid/nfc/NfcAdapter$ReaderCallback;ILandroid/os/Bundle;)V -Landroid/nfc/NfcActivityManager;->findActivityState(Landroid/app/Activity;)Landroid/nfc/NfcActivityManager$NfcActivityState; -Landroid/nfc/NfcActivityManager;->findAppState(Landroid/app/Application;)Landroid/nfc/NfcActivityManager$NfcApplicationState; -Landroid/nfc/NfcActivityManager;->findResumedActivityState()Landroid/nfc/NfcActivityManager$NfcActivityState; -Landroid/nfc/NfcActivityManager;->getActivityState(Landroid/app/Activity;)Landroid/nfc/NfcActivityManager$NfcActivityState; -Landroid/nfc/NfcActivityManager;->mActivities:Ljava/util/List; -Landroid/nfc/NfcActivityManager;->mApps:Ljava/util/List; -Landroid/nfc/NfcActivityManager;->onNdefPushComplete(B)V -Landroid/nfc/NfcActivityManager;->onTagDiscovered(Landroid/nfc/Tag;)V -Landroid/nfc/NfcActivityManager;->registerApplication(Landroid/app/Application;)V -Landroid/nfc/NfcActivityManager;->requestNfcServiceCallback()V -Landroid/nfc/NfcActivityManager;->setNdefPushContentUri(Landroid/app/Activity;[Landroid/net/Uri;)V -Landroid/nfc/NfcActivityManager;->setNdefPushContentUriCallback(Landroid/app/Activity;Landroid/nfc/NfcAdapter$CreateBeamUrisCallback;)V -Landroid/nfc/NfcActivityManager;->setNdefPushMessage(Landroid/app/Activity;Landroid/nfc/NdefMessage;I)V -Landroid/nfc/NfcActivityManager;->setNdefPushMessageCallback(Landroid/app/Activity;Landroid/nfc/NfcAdapter$CreateNdefMessageCallback;I)V -Landroid/nfc/NfcActivityManager;->setOnNdefPushCompleteCallback(Landroid/app/Activity;Landroid/nfc/NfcAdapter$OnNdefPushCompleteCallback;)V -Landroid/nfc/NfcActivityManager;->setReaderMode(Landroid/os/Binder;ILandroid/os/Bundle;)V -Landroid/nfc/NfcActivityManager;->TAG:Ljava/lang/String; -Landroid/nfc/NfcActivityManager;->unregisterApplication(Landroid/app/Application;)V -Landroid/nfc/NfcActivityManager;->verifyNfcPermission()V -Landroid/nfc/NfcAdapter;-><init>(Landroid/content/Context;)V -Landroid/nfc/NfcAdapter;->ACTION_HANDOVER_TRANSFER_DONE:Ljava/lang/String; -Landroid/nfc/NfcAdapter;->ACTION_HANDOVER_TRANSFER_STARTED:Ljava/lang/String; -Landroid/nfc/NfcAdapter;->ACTION_TAG_LEFT_FIELD:Ljava/lang/String; -Landroid/nfc/NfcAdapter;->disableForegroundDispatchInternal(Landroid/app/Activity;Z)V -Landroid/nfc/NfcAdapter;->dispatch(Landroid/nfc/Tag;)V -Landroid/nfc/NfcAdapter;->enforceResumed(Landroid/app/Activity;)V -Landroid/nfc/NfcAdapter;->EXTRA_HANDOVER_TRANSFER_STATUS:Ljava/lang/String; -Landroid/nfc/NfcAdapter;->EXTRA_HANDOVER_TRANSFER_URI:Ljava/lang/String; -Landroid/nfc/NfcAdapter;->getCardEmulationService()Landroid/nfc/INfcCardEmulation; -Landroid/nfc/NfcAdapter;->getNfcDtaInterface()Landroid/nfc/INfcDta; -Landroid/nfc/NfcAdapter;->getNfcFCardEmulationService()Landroid/nfc/INfcFCardEmulation; -Landroid/nfc/NfcAdapter;->getSdkVersion()I -Landroid/nfc/NfcAdapter;->getServiceInterface()Landroid/nfc/INfcAdapter; -Landroid/nfc/NfcAdapter;->getTagService()Landroid/nfc/INfcTag; -Landroid/nfc/NfcAdapter;->HANDOVER_TRANSFER_STATUS_FAILURE:I -Landroid/nfc/NfcAdapter;->HANDOVER_TRANSFER_STATUS_SUCCESS:I -Landroid/nfc/NfcAdapter;->hasNfcFeature()Z -Landroid/nfc/NfcAdapter;->hasNfcHceFeature()Z -Landroid/nfc/NfcAdapter;->invokeBeam(Landroid/nfc/BeamShareData;)Z -Landroid/nfc/NfcAdapter;->mContext:Landroid/content/Context; -Landroid/nfc/NfcAdapter;->mForegroundDispatchListener:Landroid/app/OnActivityPausedListener; -Landroid/nfc/NfcAdapter;->mLock:Ljava/lang/Object; -Landroid/nfc/NfcAdapter;->mNfcActivityManager:Landroid/nfc/NfcActivityManager; -Landroid/nfc/NfcAdapter;->mNfcUnlockHandlers:Ljava/util/HashMap; -Landroid/nfc/NfcAdapter;->mTagRemovedListener:Landroid/nfc/ITagRemovedCallback; -Landroid/nfc/NfcAdapter;->pausePolling(I)V -Landroid/nfc/NfcAdapter;->resumePolling()V -Landroid/nfc/NfcAdapter;->sCardEmulationService:Landroid/nfc/INfcCardEmulation; -Landroid/nfc/NfcAdapter;->setP2pModes(II)V -Landroid/nfc/NfcAdapter;->sHasNfcFeature:Z -Landroid/nfc/NfcAdapter;->sIsInitialized:Z -Landroid/nfc/NfcAdapter;->sNfcAdapters:Ljava/util/HashMap; -Landroid/nfc/NfcAdapter;->sNfcFCardEmulationService:Landroid/nfc/INfcFCardEmulation; -Landroid/nfc/NfcAdapter;->sNullContextNfcAdapter:Landroid/nfc/NfcAdapter; -Landroid/nfc/NfcAdapter;->sTagService:Landroid/nfc/INfcTag; -Landroid/nfc/NfcAdapter;->TAG:Ljava/lang/String; -Landroid/nfc/NfcEvent;-><init>(Landroid/nfc/NfcAdapter;B)V -Landroid/nfc/NfcManager;->mAdapter:Landroid/nfc/NfcAdapter; -Landroid/nfc/Tag;-><init>([B[I[Landroid/os/Bundle;ILandroid/nfc/INfcTag;)V -Landroid/nfc/Tag;->createMockTag([B[I[Landroid/os/Bundle;)Landroid/nfc/Tag; -Landroid/nfc/Tag;->generateTechStringList([I)[Ljava/lang/String; -Landroid/nfc/Tag;->getConnectedTechnology()I -Landroid/nfc/Tag;->getTechCodeList()[I -Landroid/nfc/Tag;->getTechCodesFromStrings([Ljava/lang/String;)[I -Landroid/nfc/Tag;->getTechExtras(I)Landroid/os/Bundle; -Landroid/nfc/Tag;->getTechStringToCodeMap()Ljava/util/HashMap; -Landroid/nfc/Tag;->hasTech(I)Z -Landroid/nfc/Tag;->mConnectedTechnology:I -Landroid/nfc/Tag;->mServiceHandle:I -Landroid/nfc/Tag;->mTagService:Landroid/nfc/INfcTag; -Landroid/nfc/Tag;->mTechExtras:[Landroid/os/Bundle; -Landroid/nfc/Tag;->mTechList:[I -Landroid/nfc/Tag;->mTechStringList:[Ljava/lang/String; -Landroid/nfc/Tag;->readBytesWithNull(Landroid/os/Parcel;)[B -Landroid/nfc/Tag;->rediscover()Landroid/nfc/Tag; -Landroid/nfc/Tag;->setConnectedTechnology(I)V -Landroid/nfc/Tag;->setTechnologyDisconnected()V -Landroid/nfc/Tag;->writeBytesWithNull(Landroid/os/Parcel;[B)V -Landroid/nfc/tech/BasicTagTechnology;-><init>(Landroid/nfc/Tag;I)V -Landroid/nfc/tech/BasicTagTechnology;->checkConnected()V -Landroid/nfc/tech/BasicTagTechnology;->getMaxTransceiveLengthInternal()I -Landroid/nfc/tech/BasicTagTechnology;->mIsConnected:Z -Landroid/nfc/tech/BasicTagTechnology;->mSelectedTechnology:I -Landroid/nfc/tech/BasicTagTechnology;->mTag:Landroid/nfc/Tag; -Landroid/nfc/tech/BasicTagTechnology;->reconnect()V -Landroid/nfc/tech/BasicTagTechnology;->TAG:Ljava/lang/String; -Landroid/nfc/tech/BasicTagTechnology;->transceive([BZ)[B -Landroid/nfc/tech/IsoDep;-><init>(Landroid/nfc/Tag;)V -Landroid/nfc/tech/IsoDep;->EXTRA_HIST_BYTES:Ljava/lang/String; -Landroid/nfc/tech/IsoDep;->EXTRA_HI_LAYER_RESP:Ljava/lang/String; -Landroid/nfc/tech/IsoDep;->mHiLayerResponse:[B -Landroid/nfc/tech/IsoDep;->mHistBytes:[B -Landroid/nfc/tech/IsoDep;->TAG:Ljava/lang/String; -Landroid/nfc/tech/MifareClassic;-><init>(Landroid/nfc/Tag;)V -Landroid/nfc/tech/MifareClassic;->authenticate(I[BZ)Z -Landroid/nfc/tech/MifareClassic;->isEmulated()Z -Landroid/nfc/tech/MifareClassic;->MAX_BLOCK_COUNT:I -Landroid/nfc/tech/MifareClassic;->MAX_SECTOR_COUNT:I -Landroid/nfc/tech/MifareClassic;->mIsEmulated:Z -Landroid/nfc/tech/MifareClassic;->mSize:I -Landroid/nfc/tech/MifareClassic;->mType:I -Landroid/nfc/tech/MifareClassic;->TAG:Ljava/lang/String; -Landroid/nfc/tech/MifareClassic;->validateBlock(I)V -Landroid/nfc/tech/MifareClassic;->validateSector(I)V -Landroid/nfc/tech/MifareClassic;->validateValueOperand(I)V -Landroid/nfc/tech/MifareUltralight;-><init>(Landroid/nfc/Tag;)V -Landroid/nfc/tech/MifareUltralight;->EXTRA_IS_UL_C:Ljava/lang/String; -Landroid/nfc/tech/MifareUltralight;->MAX_PAGE_COUNT:I -Landroid/nfc/tech/MifareUltralight;->mType:I -Landroid/nfc/tech/MifareUltralight;->NXP_MANUFACTURER_ID:I -Landroid/nfc/tech/MifareUltralight;->TAG:Ljava/lang/String; -Landroid/nfc/tech/MifareUltralight;->validatePageIndex(I)V -Landroid/nfc/tech/Ndef;-><init>(Landroid/nfc/Tag;)V -Landroid/nfc/tech/Ndef;->EXTRA_NDEF_CARDSTATE:Ljava/lang/String; -Landroid/nfc/tech/Ndef;->EXTRA_NDEF_MAXLENGTH:Ljava/lang/String; -Landroid/nfc/tech/Ndef;->EXTRA_NDEF_MSG:Ljava/lang/String; -Landroid/nfc/tech/Ndef;->EXTRA_NDEF_TYPE:Ljava/lang/String; -Landroid/nfc/tech/Ndef;->ICODE_SLI:Ljava/lang/String; -Landroid/nfc/tech/Ndef;->mCardState:I -Landroid/nfc/tech/Ndef;->mMaxNdefSize:I -Landroid/nfc/tech/Ndef;->mNdefMsg:Landroid/nfc/NdefMessage; -Landroid/nfc/tech/Ndef;->mNdefType:I -Landroid/nfc/tech/Ndef;->NDEF_MODE_READ_ONLY:I -Landroid/nfc/tech/Ndef;->NDEF_MODE_READ_WRITE:I -Landroid/nfc/tech/Ndef;->NDEF_MODE_UNKNOWN:I -Landroid/nfc/tech/Ndef;->TAG:Ljava/lang/String; -Landroid/nfc/tech/Ndef;->TYPE_1:I -Landroid/nfc/tech/Ndef;->TYPE_2:I -Landroid/nfc/tech/Ndef;->TYPE_3:I -Landroid/nfc/tech/Ndef;->TYPE_4:I -Landroid/nfc/tech/Ndef;->TYPE_ICODE_SLI:I -Landroid/nfc/tech/Ndef;->TYPE_MIFARE_CLASSIC:I -Landroid/nfc/tech/Ndef;->TYPE_OTHER:I -Landroid/nfc/tech/Ndef;->UNKNOWN:Ljava/lang/String; -Landroid/nfc/tech/NdefFormatable;-><init>(Landroid/nfc/Tag;)V -Landroid/nfc/tech/NdefFormatable;->format(Landroid/nfc/NdefMessage;Z)V -Landroid/nfc/tech/NdefFormatable;->TAG:Ljava/lang/String; -Landroid/nfc/tech/NfcA;-><init>(Landroid/nfc/Tag;)V -Landroid/nfc/tech/NfcA;->EXTRA_ATQA:Ljava/lang/String; -Landroid/nfc/tech/NfcA;->EXTRA_SAK:Ljava/lang/String; -Landroid/nfc/tech/NfcA;->mAtqa:[B -Landroid/nfc/tech/NfcA;->mSak:S -Landroid/nfc/tech/NfcA;->TAG:Ljava/lang/String; -Landroid/nfc/tech/NfcB;-><init>(Landroid/nfc/Tag;)V -Landroid/nfc/tech/NfcB;->EXTRA_APPDATA:Ljava/lang/String; -Landroid/nfc/tech/NfcB;->EXTRA_PROTINFO:Ljava/lang/String; -Landroid/nfc/tech/NfcB;->mAppData:[B -Landroid/nfc/tech/NfcB;->mProtInfo:[B -Landroid/nfc/tech/NfcBarcode;-><init>(Landroid/nfc/Tag;)V -Landroid/nfc/tech/NfcBarcode;->EXTRA_BARCODE_TYPE:Ljava/lang/String; -Landroid/nfc/tech/NfcBarcode;->mType:I -Landroid/nfc/tech/NfcF;-><init>(Landroid/nfc/Tag;)V -Landroid/nfc/tech/NfcF;->EXTRA_PMM:Ljava/lang/String; -Landroid/nfc/tech/NfcF;->EXTRA_SC:Ljava/lang/String; -Landroid/nfc/tech/NfcF;->mManufacturer:[B -Landroid/nfc/tech/NfcF;->mSystemCode:[B -Landroid/nfc/tech/NfcF;->TAG:Ljava/lang/String; -Landroid/nfc/tech/NfcV;-><init>(Landroid/nfc/Tag;)V -Landroid/nfc/tech/NfcV;->EXTRA_DSFID:Ljava/lang/String; -Landroid/nfc/tech/NfcV;->EXTRA_RESP_FLAGS:Ljava/lang/String; -Landroid/nfc/tech/NfcV;->mDsfId:B -Landroid/nfc/tech/NfcV;->mRespFlags:B -Landroid/nfc/tech/TagTechnology;->ISO_DEP:I -Landroid/nfc/tech/TagTechnology;->MIFARE_CLASSIC:I -Landroid/nfc/tech/TagTechnology;->MIFARE_ULTRALIGHT:I -Landroid/nfc/tech/TagTechnology;->NDEF:I -Landroid/nfc/tech/TagTechnology;->NDEF_FORMATABLE:I -Landroid/nfc/tech/TagTechnology;->NFC_A:I -Landroid/nfc/tech/TagTechnology;->NFC_B:I -Landroid/nfc/tech/TagTechnology;->NFC_BARCODE:I -Landroid/nfc/tech/TagTechnology;->NFC_F:I -Landroid/nfc/tech/TagTechnology;->NFC_V:I -Landroid/nfc/tech/TagTechnology;->reconnect()V -Landroid/nfc/TechListParcel;->CREATOR:Landroid/os/Parcelable$Creator; -Landroid/nfc/TechListParcel;->getTechLists()[[Ljava/lang/String; -Landroid/nfc/TechListParcel;->mTechLists:[[Ljava/lang/String; -Landroid/nfc/TransceiveResult;-><init>(I[B)V -Landroid/nfc/TransceiveResult;->CREATOR:Landroid/os/Parcelable$Creator; -Landroid/nfc/TransceiveResult;->getResponseOrThrow()[B -Landroid/nfc/TransceiveResult;->mResponseData:[B -Landroid/nfc/TransceiveResult;->mResult:I -Landroid/nfc/TransceiveResult;->RESULT_EXCEEDED_LENGTH:I -Landroid/nfc/TransceiveResult;->RESULT_FAILURE:I -Landroid/nfc/TransceiveResult;->RESULT_SUCCESS:I -Landroid/nfc/TransceiveResult;->RESULT_TAGLOST:I Landroid/opengl/EGL14;->eglCreatePbufferFromClientBuffer(Landroid/opengl/EGLDisplay;IJLandroid/opengl/EGLConfig;[II)Landroid/opengl/EGLSurface; Landroid/opengl/EGL14;->_eglCreateWindowSurface(Landroid/opengl/EGLDisplay;Landroid/opengl/EGLConfig;Ljava/lang/Object;[II)Landroid/opengl/EGLSurface; Landroid/opengl/EGL14;->_eglCreateWindowSurfaceTexture(Landroid/opengl/EGLDisplay;Landroid/opengl/EGLConfig;Ljava/lang/Object;[II)Landroid/opengl/EGLSurface; diff --git a/boot/hiddenapi/hiddenapi-max-target-r-loprio.txt b/boot/hiddenapi/hiddenapi-max-target-r-loprio.txt index f5184e7963d7..4df1dcaed136 100644 --- a/boot/hiddenapi/hiddenapi-max-target-r-loprio.txt +++ b/boot/hiddenapi/hiddenapi-max-target-r-loprio.txt @@ -19,7 +19,6 @@ Landroid/Manifest$permission;->READ_FRAME_BUFFER:Ljava/lang/String; Landroid/media/IVolumeController$Stub;->asInterface(Landroid/os/IBinder;)Landroid/media/IVolumeController; Landroid/net/INetworkPolicyListener$Stub;-><init>()V Landroid/net/sip/ISipSession$Stub;->asInterface(Landroid/os/IBinder;)Landroid/net/sip/ISipSession; -Landroid/nfc/INfcAdapter$Stub;->TRANSACTION_enable:I Landroid/os/IPowerManager$Stub;->TRANSACTION_acquireWakeLock:I Landroid/os/IPowerManager$Stub;->TRANSACTION_goToSleep:I Landroid/service/euicc/IEuiccService$Stub;-><init>()V diff --git a/cmds/idmap2/libidmap2/FabricatedOverlay.cpp b/cmds/idmap2/libidmap2/FabricatedOverlay.cpp index 16bb896e939c..55ea15d0cdf1 100644 --- a/cmds/idmap2/libidmap2/FabricatedOverlay.cpp +++ b/cmds/idmap2/libidmap2/FabricatedOverlay.cpp @@ -468,9 +468,9 @@ Result<OverlayData> FabContainer::GetOverlayData(const OverlayManifestInfo& info entry.name().c_str()); const auto& res_value = entry.res_value(); result.pairs.emplace_back(OverlayData::Value{ - name, TargetValueWithConfig{.config = entry.configuration(), .value = TargetValue{ + name, TargetValueWithConfig{.value = TargetValue{ .data_type = static_cast<uint8_t>(res_value.data_type()), - .data_value = res_value.data_value()}}}); + .data_value = res_value.data_value()}, .config = entry.configuration()}}); } } } diff --git a/cmds/idmap2/libidmap2/ResourceContainer.cpp b/cmds/idmap2/libidmap2/ResourceContainer.cpp index 7869fbdb8cea..3c0e118bbfe7 100644 --- a/cmds/idmap2/libidmap2/ResourceContainer.cpp +++ b/cmds/idmap2/libidmap2/ResourceContainer.cpp @@ -227,9 +227,9 @@ Result<OverlayData> CreateResourceMapping(ResourceId id, const ZipAssetsProvider } else { overlay_data.pairs.emplace_back( OverlayData::Value{*target_resource, TargetValueWithConfig{ - .config = std::string(), .value = TargetValue{.data_type = overlay_resource->dataType, - .data_value = overlay_resource->data}}}); + .data_value = overlay_resource->data}, + .config = std::string()}}); } } @@ -268,10 +268,11 @@ struct ResState { std::unique_ptr<AssetManager2> am; ZipAssetsProvider* zip_assets; - static Result<ResState> Initialize(std::unique_ptr<ZipAssetsProvider> zip) { + static Result<ResState> Initialize(std::unique_ptr<ZipAssetsProvider> zip, + package_property_t flags) { ResState state; state.zip_assets = zip.get(); - if ((state.apk_assets = ApkAssets::Load(std::move(zip))) == nullptr) { + if ((state.apk_assets = ApkAssets::Load(std::move(zip), flags)) == nullptr) { return Error("failed to load apk asset"); } @@ -284,7 +285,7 @@ struct ResState { } state.am = std::make_unique<AssetManager2>(); - if (!state.am->SetApkAssets({state.apk_assets})) { + if (!state.am->SetApkAssets({state.apk_assets}, false)) { return Error("failed to create asset manager"); } @@ -343,8 +344,8 @@ Result<const ResState*> ApkResourceContainer::GetState() const { return state; } - auto state = - ResState::Initialize(std::move(std::get<std::unique_ptr<ZipAssetsProvider>>(state_))); + auto state = ResState::Initialize(std::move(std::get<std::unique_ptr<ZipAssetsProvider>>(state_)), + PROPERTY_OPTIMIZE_NAME_LOOKUPS); if (!state) { return state.GetError(); } diff --git a/core/api/current.txt b/core/api/current.txt index 46c8f8208883..f55de5ae2d26 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -1602,7 +1602,6 @@ package android { 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 @@ -4625,9 +4624,9 @@ package android.app { public class ActivityManager { method public int addAppTask(@NonNull android.app.Activity, @NonNull android.content.Intent, @Nullable android.app.ActivityManager.TaskDescription, @NonNull android.graphics.Bitmap); + method @FlaggedApi("android.app.app_start_info") public void addApplicationStartInfoCompletionListener(@NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<android.app.ApplicationStartInfo>); method @FlaggedApi("android.app.app_start_info") public void addStartInfoTimestamp(@IntRange(from=android.app.ApplicationStartInfo.START_TIMESTAMP_RESERVED_RANGE_DEVELOPER_START, to=android.app.ApplicationStartInfo.START_TIMESTAMP_RESERVED_RANGE_DEVELOPER) int, long); method public void appNotResponding(@NonNull String); - method @FlaggedApi("android.app.app_start_info") public void clearApplicationStartInfoCompletionListener(); method public boolean clearApplicationUserData(); method public void clearWatchHeapLimit(); method @RequiresPermission(android.Manifest.permission.DUMP) public void dumpPackageState(java.io.FileDescriptor, String); @@ -4661,8 +4660,8 @@ package android.app { method @RequiresPermission(android.Manifest.permission.KILL_BACKGROUND_PROCESSES) public void killBackgroundProcesses(String); method @RequiresPermission(android.Manifest.permission.REORDER_TASKS) public void moveTaskToFront(int, int); method @RequiresPermission(android.Manifest.permission.REORDER_TASKS) public void moveTaskToFront(int, int, android.os.Bundle); + method @FlaggedApi("android.app.app_start_info") public void removeApplicationStartInfoCompletionListener(@NonNull java.util.function.Consumer<android.app.ApplicationStartInfo>); method @Deprecated public void restartPackage(String); - method @FlaggedApi("android.app.app_start_info") public void setApplicationStartInfoCompletionListener(@NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<android.app.ApplicationStartInfo>); method public void setProcessStateSummary(@Nullable byte[]); method public static void setVrThread(int); method public void setWatchHeapLimit(long); @@ -5318,7 +5317,6 @@ package android.app { ctor @Deprecated public AutomaticZenRule(String, android.content.ComponentName, android.net.Uri, int, boolean); ctor public AutomaticZenRule(@NonNull String, @Nullable android.content.ComponentName, @Nullable android.content.ComponentName, @NonNull android.net.Uri, @Nullable android.service.notification.ZenPolicy, int, boolean); ctor public AutomaticZenRule(android.os.Parcel); - method @FlaggedApi("android.app.modes_api") public boolean canUpdate(); method public int describeContents(); method public android.net.Uri getConditionId(); method @Nullable public android.content.ComponentName getConfigurationActivity(); @@ -10387,6 +10385,7 @@ package android.content { method @CheckResult(suggest="#enforceCallingPermission(String,String)") public abstract int checkCallingPermission(@NonNull String); method @CheckResult(suggest="#enforceCallingUriPermission(Uri,int,String)") public abstract int checkCallingUriPermission(android.net.Uri, int); method @NonNull public int[] checkCallingUriPermissions(@NonNull java.util.List<android.net.Uri>, int); + method @FlaggedApi("android.security.content_uri_permission_apis") public int checkContentUriPermissionFull(@NonNull android.net.Uri, int, int, int); method @CheckResult(suggest="#enforcePermission(String,int,int,String)") public abstract int checkPermission(@NonNull String, int, int); method public abstract int checkSelfPermission(@NonNull String); method @CheckResult(suggest="#enforceUriPermission(Uri,int,int,String)") public abstract int checkUriPermission(android.net.Uri, int, int, int); @@ -10545,6 +10544,7 @@ package android.content { field public static final int BIND_INCLUDE_CAPABILITIES = 4096; // 0x1000 field public static final int BIND_NOT_FOREGROUND = 4; // 0x4 field public static final int BIND_NOT_PERCEPTIBLE = 256; // 0x100 + field @FlaggedApi("android.content.flags.enable_bind_package_isolated_process") public static final int BIND_PACKAGE_ISOLATED_PROCESS = 16384; // 0x4000 field public static final int BIND_SHARED_ISOLATED_PROCESS = 8192; // 0x2000 field public static final int BIND_WAIVE_PRIORITY = 32; // 0x20 field public static final String BIOMETRIC_SERVICE = "biometric"; @@ -12445,7 +12445,7 @@ package android.content.pm { 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 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, int) 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; method @RequiresPermission(anyOf={android.Manifest.permission.DELETE_PACKAGES, android.Manifest.permission.REQUEST_DELETE_PACKAGES}) public void uninstall(@NonNull String, @NonNull android.content.IntentSender); method @RequiresPermission(anyOf={android.Manifest.permission.DELETE_PACKAGES, android.Manifest.permission.REQUEST_DELETE_PACKAGES}) public void uninstall(@NonNull android.content.pm.VersionedPackage, @NonNull android.content.IntentSender); @@ -12872,7 +12872,6 @@ package android.content.pm { field public static final int COMPONENT_ENABLED_STATE_DISABLED_USER = 3; // 0x3 field public static final int COMPONENT_ENABLED_STATE_ENABLED = 1; // 0x1 field @FlaggedApi("android.content.pm.archiving") public static final int DELETE_ARCHIVE = 16; // 0x10 - field @FlaggedApi("android.content.pm.archiving") public static final int DELETE_SHOW_DIALOG = 32; // 0x20 field public static final int DONT_KILL_APP = 1; // 0x1 field public static final String EXTRA_VERIFICATION_ID = "android.content.pm.extra.VERIFICATION_ID"; field public static final String EXTRA_VERIFICATION_RESULT = "android.content.pm.extra.VERIFICATION_RESULT"; @@ -13675,11 +13674,8 @@ package android.content.res { @FlaggedApi("android.content.res.font_scale_converter_public") public interface FontScaleConverter { method public float convertDpToSp(float); method public float convertSpToDp(float); - } - - @FlaggedApi("android.content.res.font_scale_converter_public") public class FontScaleConverterFactory { - method @FlaggedApi("android.content.res.font_scale_converter_public") @AnyThread @Nullable public static android.content.res.FontScaleConverter forScale(float); - method @FlaggedApi("android.content.res.font_scale_converter_public") @AnyThread public static boolean isNonLinearFontScalingActive(float); + method @AnyThread @Nullable public static android.content.res.FontScaleConverter forScale(float); + method @AnyThread public static boolean isNonLinearFontScalingActive(float); } public class ObbInfo implements android.os.Parcelable { @@ -16399,6 +16395,8 @@ package android.graphics { field public static final int START_HYPHEN_EDIT_NO_EDIT = 0; // 0x0 field public static final int STRIKE_THRU_TEXT_FLAG = 16; // 0x10 field public static final int SUBPIXEL_TEXT_FLAG = 128; // 0x80 + field @FlaggedApi("com.android.text.flags.inter_character_justification") public static final int TEXT_RUN_FLAG_LEFT_EDGE = 8192; // 0x2000 + field @FlaggedApi("com.android.text.flags.inter_character_justification") public static final int TEXT_RUN_FLAG_RIGHT_EDGE = 16384; // 0x4000 field public static final int UNDERLINE_TEXT_FLAG = 8; // 0x8 } @@ -18682,6 +18680,8 @@ package android.hardware.biometrics { method @Nullable public int getAllowedAuthenticators(); method @FlaggedApi("android.hardware.biometrics.custom_biometric_prompt") @Nullable public android.hardware.biometrics.PromptContentView getContentView(); method @Nullable public CharSequence getDescription(); + method @FlaggedApi("android.hardware.biometrics.custom_biometric_prompt") @Nullable @RequiresPermission("android.permission.MANAGE_BIOMETRIC_DIALOG") public android.graphics.Bitmap getLogoBitmap(); + method @FlaggedApi("android.hardware.biometrics.custom_biometric_prompt") @DrawableRes @RequiresPermission("android.permission.MANAGE_BIOMETRIC_DIALOG") public int getLogoRes(); method @Nullable public CharSequence getNegativeButtonText(); method @Nullable public CharSequence getSubtitle(); method @NonNull public CharSequence getTitle(); @@ -18731,6 +18731,8 @@ package android.hardware.biometrics { method @FlaggedApi("android.hardware.biometrics.custom_biometric_prompt") @NonNull public android.hardware.biometrics.BiometricPrompt.Builder setContentView(@NonNull android.hardware.biometrics.PromptContentView); method @NonNull public android.hardware.biometrics.BiometricPrompt.Builder setDescription(@NonNull CharSequence); method @Deprecated @NonNull public android.hardware.biometrics.BiometricPrompt.Builder setDeviceCredentialAllowed(boolean); + method @FlaggedApi("android.hardware.biometrics.custom_biometric_prompt") @NonNull @RequiresPermission("android.permission.MANAGE_BIOMETRIC_DIALOG") public android.hardware.biometrics.BiometricPrompt.Builder setLogo(@DrawableRes int); + method @FlaggedApi("android.hardware.biometrics.custom_biometric_prompt") @NonNull @RequiresPermission("android.permission.MANAGE_BIOMETRIC_DIALOG") public android.hardware.biometrics.BiometricPrompt.Builder setLogo(@NonNull android.graphics.Bitmap); method @NonNull public android.hardware.biometrics.BiometricPrompt.Builder setNegativeButton(@NonNull CharSequence, @NonNull java.util.concurrent.Executor, @NonNull android.content.DialogInterface.OnClickListener); method @NonNull public android.hardware.biometrics.BiometricPrompt.Builder setSubtitle(@NonNull CharSequence); method @NonNull public android.hardware.biometrics.BiometricPrompt.Builder setTitle(@NonNull CharSequence); @@ -18752,21 +18754,21 @@ package android.hardware.biometrics { method @Nullable public java.security.Signature getSignature(); } - @FlaggedApi("android.hardware.biometrics.custom_biometric_prompt") public interface PromptContentListItem { + @FlaggedApi("android.hardware.biometrics.custom_biometric_prompt") public interface PromptContentItem { } - @FlaggedApi("android.hardware.biometrics.custom_biometric_prompt") public final class PromptContentListItemBulletedText implements android.os.Parcelable android.hardware.biometrics.PromptContentListItem { - ctor public PromptContentListItemBulletedText(@NonNull CharSequence); + @FlaggedApi("android.hardware.biometrics.custom_biometric_prompt") public final class PromptContentItemBulletedText implements android.os.Parcelable android.hardware.biometrics.PromptContentItem { + ctor public PromptContentItemBulletedText(@NonNull CharSequence); 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.PromptContentListItemBulletedText> CREATOR; + field @NonNull public static final android.os.Parcelable.Creator<android.hardware.biometrics.PromptContentItemBulletedText> CREATOR; } - @FlaggedApi("android.hardware.biometrics.custom_biometric_prompt") public final class PromptContentListItemPlainText implements android.os.Parcelable android.hardware.biometrics.PromptContentListItem { - ctor public PromptContentListItemPlainText(@NonNull CharSequence); + @FlaggedApi("android.hardware.biometrics.custom_biometric_prompt") public final class PromptContentItemPlainText implements android.os.Parcelable android.hardware.biometrics.PromptContentItem { + ctor public PromptContentItemPlainText(@NonNull CharSequence); 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.PromptContentListItemPlainText> CREATOR; + field @NonNull public static final android.os.Parcelable.Creator<android.hardware.biometrics.PromptContentItemPlainText> CREATOR; } @FlaggedApi("android.hardware.biometrics.custom_biometric_prompt") public interface PromptContentView { @@ -18775,7 +18777,7 @@ package android.hardware.biometrics { @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 @NonNull public java.util.List<android.hardware.biometrics.PromptContentListItem> getListItems(); + method @NonNull public java.util.List<android.hardware.biometrics.PromptContentItem> getListItems(); method public static int getMaxEachItemCharacterNumber(); method public static int getMaxItemCount(); method public void writeToParcel(@NonNull android.os.Parcel, int); @@ -18784,7 +18786,8 @@ package android.hardware.biometrics { public static final class PromptVerticalListContentView.Builder { ctor public PromptVerticalListContentView.Builder(); - method @NonNull public android.hardware.biometrics.PromptVerticalListContentView.Builder addListItem(@NonNull android.hardware.biometrics.PromptContentListItem); + 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); } @@ -23340,6 +23343,7 @@ package android.media { field public static final String KEY_HDR10_PLUS_INFO = "hdr10-plus-info"; field public static final String KEY_HDR_STATIC_INFO = "hdr-static-info"; field public static final String KEY_HEIGHT = "height"; + field @FlaggedApi("com.android.media.codec.flags.codec_importance") public static final String KEY_IMPORTANCE = "importance"; field public static final String KEY_INTRA_REFRESH_PERIOD = "intra-refresh-period"; field public static final String KEY_IS_ADTS = "is-adts"; field public static final String KEY_IS_AUTOSELECT = "is-autoselect"; @@ -24302,7 +24306,7 @@ package android.media { method public void release(); method public void selectRoute(@NonNull android.media.MediaRoute2Info); method public void setVolume(int); - method @FlaggedApi("com.android.media.flags.enable_built_in_speaker_route_suitability_statuses") public boolean wasTransferRequestedBySelf(); + method @FlaggedApi("com.android.media.flags.enable_built_in_speaker_route_suitability_statuses") public boolean wasTransferInitiatedBySelf(); } public abstract static class MediaRouter2.TransferCallback { @@ -26668,12 +26672,16 @@ package android.media.tv { public static final class TvContract.Channels implements android.media.tv.TvContract.BaseTvColumns { method @Nullable public static String getVideoResolution(String); + field @FlaggedApi("android.media.tv.flags.broadcast_visibility_types") public static final int BROADCAST_VISIBILITY_TYPE_INVISIBLE = 2; // 0x2 + field @FlaggedApi("android.media.tv.flags.broadcast_visibility_types") public static final int BROADCAST_VISIBILITY_TYPE_NUMERIC_SELECTABLE_ONLY = 1; // 0x1 + field @FlaggedApi("android.media.tv.flags.broadcast_visibility_types") public static final int BROADCAST_VISIBILITY_TYPE_VISIBLE = 0; // 0x0 field public static final String COLUMN_APP_LINK_COLOR = "app_link_color"; field public static final String COLUMN_APP_LINK_ICON_URI = "app_link_icon_uri"; field public static final String COLUMN_APP_LINK_INTENT_URI = "app_link_intent_uri"; field public static final String COLUMN_APP_LINK_POSTER_ART_URI = "app_link_poster_art_uri"; field public static final String COLUMN_APP_LINK_TEXT = "app_link_text"; field public static final String COLUMN_BROADCAST_GENRE = "broadcast_genre"; + field @FlaggedApi("android.media.tv.flags.broadcast_visibility_types") public static final String COLUMN_BROADCAST_VISIBILITY_TYPE = "broadcast_visibility_type"; field public static final String COLUMN_BROWSABLE = "browsable"; field public static final String COLUMN_CHANNEL_LIST_ID = "channel_list_id"; field public static final String COLUMN_DESCRIPTION = "description"; @@ -31844,6 +31852,7 @@ package android.os { method public long computeChargeTimeRemaining(); method public int getIntProperty(int); method public long getLongProperty(int); + method @FlaggedApi("android.os.battery_part_status_api") @Nullable public String getStringProperty(int); method public boolean isCharging(); field public static final String ACTION_CHARGING = "android.os.action.CHARGING"; field public static final String ACTION_DISCHARGING = "android.os.action.DISCHARGING"; @@ -40511,7 +40520,6 @@ package android.service.notification { public final class ZenPolicy implements android.os.Parcelable { method public int describeContents(); - method @FlaggedApi("android.app.modes_api") public int getAllowedChannels(); method public int getPriorityCallSenders(); method public int getPriorityCategoryAlarms(); method public int getPriorityCategoryCalls(); @@ -40522,6 +40530,7 @@ package android.service.notification { method public int getPriorityCategoryReminders(); method public int getPriorityCategoryRepeatCallers(); method public int getPriorityCategorySystem(); + method @FlaggedApi("android.app.modes_api") public int getPriorityChannels(); method public int getPriorityConversationSenders(); method public int getPriorityMessageSenders(); method public int getVisualEffectAmbient(); @@ -40532,9 +40541,6 @@ package android.service.notification { method public int getVisualEffectPeek(); method public int getVisualEffectStatusBar(); method public void writeToParcel(android.os.Parcel, int); - field @FlaggedApi("android.app.modes_api") public static final int CHANNEL_TYPE_NONE = 2; // 0x2 - field @FlaggedApi("android.app.modes_api") public static final int CHANNEL_TYPE_PRIORITY = 1; // 0x1 - field @FlaggedApi("android.app.modes_api") public static final int CHANNEL_TYPE_UNSET = 0; // 0x0 field public static final int CONVERSATION_SENDERS_ANYONE = 1; // 0x1 field public static final int CONVERSATION_SENDERS_IMPORTANT = 2; // 0x2 field public static final int CONVERSATION_SENDERS_NONE = 3; // 0x3 @@ -40555,11 +40561,11 @@ package android.service.notification { method @NonNull public android.service.notification.ZenPolicy.Builder allowAlarms(boolean); method @NonNull public android.service.notification.ZenPolicy.Builder allowAllSounds(); method @NonNull public android.service.notification.ZenPolicy.Builder allowCalls(int); - method @FlaggedApi("android.app.modes_api") @NonNull public android.service.notification.ZenPolicy.Builder allowChannels(int); method @NonNull public android.service.notification.ZenPolicy.Builder allowConversations(int); method @NonNull public android.service.notification.ZenPolicy.Builder allowEvents(boolean); method @NonNull public android.service.notification.ZenPolicy.Builder allowMedia(boolean); method @NonNull public android.service.notification.ZenPolicy.Builder allowMessages(int); + method @FlaggedApi("android.app.modes_api") @NonNull public android.service.notification.ZenPolicy.Builder allowPriorityChannels(boolean); method @NonNull public android.service.notification.ZenPolicy.Builder allowReminders(boolean); method @NonNull public android.service.notification.ZenPolicy.Builder allowRepeatCallers(boolean); method @NonNull public android.service.notification.ZenPolicy.Builder allowSystem(boolean); @@ -41655,6 +41661,7 @@ package android.telecom { 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>); } @@ -43073,6 +43080,8 @@ package android.telephony { field public static final String KEY_RTT_UPGRADE_SUPPORTED_FOR_DOWNGRADED_VT_CALL_BOOL = "rtt_upgrade_supported_for_downgraded_vt_call"; field @FlaggedApi("com.android.internal.telephony.flags.carrier_enabled_satellite_flag") public static final String KEY_SATELLITE_ATTACH_SUPPORTED_BOOL = "satellite_attach_supported_bool"; field @FlaggedApi("com.android.internal.telephony.flags.carrier_enabled_satellite_flag") public static final String KEY_SATELLITE_CONNECTION_HYSTERESIS_SEC_INT = "satellite_connection_hysteresis_sec_int"; + field @FlaggedApi("com.android.internal.telephony.flags.carrier_enabled_satellite_flag") public static final String KEY_SATELLITE_ENTITLEMENT_STATUS_REFRESH_DAYS_INT = "satellite_entitlement_status_refresh_days_int"; + field @FlaggedApi("com.android.internal.telephony.flags.carrier_enabled_satellite_flag") public static final String KEY_SATELLITE_ENTITLEMENT_SUPPORTED_BOOL = "satellite_entitlement_supported_bool"; field public static final String KEY_SHOW_4G_FOR_3G_DATA_ICON_BOOL = "show_4g_for_3g_data_icon_bool"; field public static final String KEY_SHOW_4G_FOR_LTE_DATA_ICON_BOOL = "show_4g_for_lte_data_icon_bool"; field public static final String KEY_SHOW_APN_SETTING_CDMA_BOOL = "show_apn_setting_cdma_bool"; diff --git a/core/api/system-current.txt b/core/api/system-current.txt index bd4ecf298d77..b0920e15ff92 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -133,6 +133,7 @@ package android { field public static final String FORCE_STOP_PACKAGES = "android.permission.FORCE_STOP_PACKAGES"; field public static final String GET_APP_METADATA = "android.permission.GET_APP_METADATA"; field public static final String GET_APP_OPS_STATS = "android.permission.GET_APP_OPS_STATS"; + field @FlaggedApi("android.app.bic_client") public static final String GET_BACKGROUND_INSTALLED_PACKAGES = "android.permission.GET_BACKGROUND_INSTALLED_PACKAGES"; field @FlaggedApi("android.app.get_binding_uid_importance") public static final String GET_BINDING_UID_IMPORTANCE = "android.permission.GET_BINDING_UID_IMPORTANCE"; field public static final String GET_HISTORICAL_APP_OPS_STATS = "android.permission.GET_HISTORICAL_APP_OPS_STATS"; field public static final String GET_PROCESS_STATE_AND_OOM_SCORE = "android.permission.GET_PROCESS_STATE_AND_OOM_SCORE"; @@ -294,6 +295,7 @@ package android { field public static final String READ_RUNTIME_PROFILES = "android.permission.READ_RUNTIME_PROFILES"; field public static final String READ_SAFETY_CENTER_STATUS = "android.permission.READ_SAFETY_CENTER_STATUS"; field public static final String READ_SEARCH_INDEXABLES = "android.permission.READ_SEARCH_INDEXABLES"; + field @FlaggedApi("android.app.system_terms_of_address_enabled") public static final String READ_SYSTEM_GRAMMATICAL_GENDER = "android.permission.READ_SYSTEM_GRAMMATICAL_GENDER"; field public static final String READ_SYSTEM_UPDATE_INFO = "android.permission.READ_SYSTEM_UPDATE_INFO"; field public static final String READ_WALLPAPER_INTERNAL = "android.permission.READ_WALLPAPER_INTERNAL"; field public static final String READ_WIFI_CREDENTIAL = "android.permission.READ_WIFI_CREDENTIAL"; @@ -622,6 +624,7 @@ package android.app { field public static final String OPSTR_ACCEPT_HANDOVER = "android:accept_handover"; field public static final String OPSTR_ACCESS_ACCESSIBILITY = "android:access_accessibility"; field public static final String OPSTR_ACCESS_NOTIFICATIONS = "android:access_notifications"; + field @FlaggedApi("android.permission.flags.enhanced_confirmation_mode_apis_enabled") public static final String OPSTR_ACCESS_RESTRICTED_SETTINGS = "android:access_restricted_settings"; field public static final String OPSTR_ACTIVATE_PLATFORM_VPN = "android:activate_platform_vpn"; field public static final String OPSTR_ACTIVATE_VPN = "android:activate_vpn"; field public static final String OPSTR_ASSIST_SCREENSHOT = "android:assist_screenshot"; @@ -1614,6 +1617,7 @@ package android.app.ambientcontext { field public static final int LEVEL_MEDIUM_HIGH = 4; // 0x4 field public static final int LEVEL_MEDIUM_LOW = 2; // 0x2 field public static final int LEVEL_UNKNOWN = 0; // 0x0 + field @FlaggedApi("android.app.ambient_heart_rate") public static final int RATE_PER_MINUTE_UNKNOWN = -1; // 0xffffffff } public static final class AmbientContextEvent.Builder { @@ -3221,6 +3225,7 @@ package android.companion.virtual { method @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void close(); method @NonNull public android.content.Context createContext(); method @NonNull @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public android.companion.virtual.audio.VirtualAudioDevice createVirtualAudioDevice(@NonNull android.hardware.display.VirtualDisplay, @Nullable java.util.concurrent.Executor, @Nullable android.companion.virtual.audio.VirtualAudioDevice.AudioConfigurationChangeCallback); + method @FlaggedApi("android.companion.virtual.flags.virtual_camera") @NonNull @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public android.companion.virtual.camera.VirtualCamera createVirtualCamera(@NonNull android.companion.virtual.camera.VirtualCameraConfig); method @Deprecated @Nullable public android.hardware.display.VirtualDisplay createVirtualDisplay(@IntRange(from=1) int, @IntRange(from=1) int, @IntRange(from=1) int, @Nullable android.view.Surface, int, @Nullable java.util.concurrent.Executor, @Nullable android.hardware.display.VirtualDisplay.Callback); method @Nullable public android.hardware.display.VirtualDisplay createVirtualDisplay(@NonNull android.hardware.display.VirtualDisplayConfig, @Nullable java.util.concurrent.Executor, @Nullable android.hardware.display.VirtualDisplay.Callback); method @NonNull @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public android.hardware.input.VirtualDpad createVirtualDpad(@NonNull android.hardware.input.VirtualDpadConfig); @@ -3229,6 +3234,7 @@ package android.companion.virtual { method @NonNull @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public android.hardware.input.VirtualMouse createVirtualMouse(@NonNull android.hardware.input.VirtualMouseConfig); method @Deprecated @NonNull @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public android.hardware.input.VirtualMouse createVirtualMouse(@NonNull android.hardware.display.VirtualDisplay, @NonNull String, int, int); method @NonNull @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public android.hardware.input.VirtualNavigationTouchpad createVirtualNavigationTouchpad(@NonNull android.hardware.input.VirtualNavigationTouchpadConfig); + method @FlaggedApi("android.companion.virtual.flags.virtual_stylus") @NonNull @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public android.hardware.input.VirtualStylus createVirtualStylus(@NonNull android.hardware.input.VirtualStylusConfig); method @NonNull @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public android.hardware.input.VirtualTouchscreen createVirtualTouchscreen(@NonNull android.hardware.input.VirtualTouchscreenConfig); method @Deprecated @NonNull @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public android.hardware.input.VirtualTouchscreen createVirtualTouchscreen(@NonNull android.hardware.display.VirtualDisplay, @NonNull String, int, int); method public int getDeviceId(); @@ -3274,6 +3280,7 @@ package android.companion.virtual { field @Deprecated public static final int NAVIGATION_POLICY_DEFAULT_BLOCKED = 1; // 0x1 field @FlaggedApi("android.companion.virtual.flags.dynamic_policy") public static final int POLICY_TYPE_ACTIVITY = 3; // 0x3 field public static final int POLICY_TYPE_AUDIO = 1; // 0x1 + field @FlaggedApi("android.companion.virtual.flags.virtual_camera") public static final int POLICY_TYPE_CAMERA = 5; // 0x5 field @FlaggedApi("android.companion.virtual.flags.cross_device_clipboard") public static final int POLICY_TYPE_CLIPBOARD = 4; // 0x4 field public static final int POLICY_TYPE_RECENTS = 2; // 0x2 field public static final int POLICY_TYPE_SENSORS = 0; // 0x0 @@ -3356,30 +3363,38 @@ package android.companion.virtual.camera { @FlaggedApi("android.companion.virtual.flags.virtual_camera") public interface VirtualCameraCallback { method public default void onProcessCaptureRequest(int, long); method public void onStreamClosed(int); - method public void onStreamConfigured(int, @NonNull android.view.Surface, @NonNull android.companion.virtual.camera.VirtualCameraStreamConfig); + method public void onStreamConfigured(int, @NonNull android.view.Surface, @IntRange(from=1) int, @IntRange(from=1) int, int); } @FlaggedApi("android.companion.virtual.flags.virtual_camera") public final class VirtualCameraConfig implements android.os.Parcelable { method public int describeContents(); + method public int getLensFacing(); method @NonNull public String getName(); + method public int getSensorOrientation(); method @NonNull public java.util.Set<android.companion.virtual.camera.VirtualCameraStreamConfig> getStreamConfigs(); method public void writeToParcel(@NonNull android.os.Parcel, int); field @NonNull public static final android.os.Parcelable.Creator<android.companion.virtual.camera.VirtualCameraConfig> CREATOR; + field public static final int SENSOR_ORIENTATION_0 = 0; // 0x0 + field public static final int SENSOR_ORIENTATION_180 = 180; // 0xb4 + field public static final int SENSOR_ORIENTATION_270 = 270; // 0x10e + field public static final int SENSOR_ORIENTATION_90 = 90; // 0x5a } @FlaggedApi("android.companion.virtual.flags.virtual_camera") public static final class VirtualCameraConfig.Builder { ctor public VirtualCameraConfig.Builder(); - method @NonNull public android.companion.virtual.camera.VirtualCameraConfig.Builder addStreamConfig(int, int, int); + method @NonNull public android.companion.virtual.camera.VirtualCameraConfig.Builder addStreamConfig(@IntRange(from=1) int, @IntRange(from=1) int, int, @IntRange(from=1) int); method @NonNull public android.companion.virtual.camera.VirtualCameraConfig build(); + method @NonNull public android.companion.virtual.camera.VirtualCameraConfig.Builder setLensFacing(int); method @NonNull public android.companion.virtual.camera.VirtualCameraConfig.Builder setName(@NonNull String); + method @NonNull public android.companion.virtual.camera.VirtualCameraConfig.Builder setSensorOrientation(int); method @NonNull public android.companion.virtual.camera.VirtualCameraConfig.Builder setVirtualCameraCallback(@NonNull java.util.concurrent.Executor, @NonNull android.companion.virtual.camera.VirtualCameraCallback); } @FlaggedApi("android.companion.virtual.flags.virtual_camera") public final class VirtualCameraStreamConfig implements android.os.Parcelable { - ctor public VirtualCameraStreamConfig(@IntRange(from=1) int, @IntRange(from=1) int, int); method public int describeContents(); method public int getFormat(); method @IntRange(from=1) public int getHeight(); + method @IntRange(from=1) public int getMaximumFramesPerSecond(); method @IntRange(from=1) public int getWidth(); method public void writeToParcel(@NonNull android.os.Parcel, int); field @NonNull public static final android.os.Parcelable.Creator<android.companion.virtual.camera.VirtualCameraStreamConfig> CREATOR; @@ -3530,6 +3545,7 @@ package android.content { field public static final String CLOUDSEARCH_SERVICE = "cloudsearch"; field public static final String CONTENT_SUGGESTIONS_SERVICE = "content_suggestions"; field public static final String CONTEXTHUB_SERVICE = "contexthub"; + field @FlaggedApi("android.permission.flags.enhanced_confirmation_mode_apis_enabled") public static final String ECM_ENHANCED_CONFIRMATION_SERVICE = "ecm_enhanced_confirmation"; field public static final String ETHERNET_SERVICE = "ethernet"; field public static final String EUICC_CARD_SERVICE = "euicc_card"; field public static final String FONT_SERVICE = "font"; @@ -4208,11 +4224,14 @@ package android.content.pm { field @NonNull public static final android.os.Parcelable.Creator<android.content.pm.UserProperties> CREATOR; field public static final int CROSS_PROFILE_CONTENT_SHARING_DELEGATE_FROM_PARENT = 1; // 0x1 field public static final int CROSS_PROFILE_CONTENT_SHARING_NO_DELEGATION = 0; // 0x0 + field public static final int CROSS_PROFILE_CONTENT_SHARING_UNKNOWN = -1; // 0xffffffff field public static final int SHOW_IN_QUIET_MODE_DEFAULT = 2; // 0x2 field public static final int SHOW_IN_QUIET_MODE_HIDDEN = 1; // 0x1 field public static final int SHOW_IN_QUIET_MODE_PAUSED = 0; // 0x0 + field public static final int SHOW_IN_QUIET_MODE_UNKNOWN = -1; // 0xffffffff field public static final int SHOW_IN_SHARING_SURFACES_NO = 2; // 0x2 field public static final int SHOW_IN_SHARING_SURFACES_SEPARATE = 1; // 0x1 + field public static final int SHOW_IN_SHARING_SURFACES_UNKNOWN = -1; // 0xffffffff field public static final int SHOW_IN_SHARING_SURFACES_WITH_PARENT = 0; // 0x0 } @@ -5304,6 +5323,78 @@ package android.hardware.input { method @NonNull public android.hardware.input.VirtualNavigationTouchpadConfig build(); } + @FlaggedApi("android.companion.virtual.flags.virtual_stylus") public class VirtualStylus implements java.io.Closeable { + method @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void close(); + method @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void sendButtonEvent(@NonNull android.hardware.input.VirtualStylusButtonEvent); + method @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void sendMotionEvent(@NonNull android.hardware.input.VirtualStylusMotionEvent); + } + + @FlaggedApi("android.companion.virtual.flags.virtual_stylus") public final class VirtualStylusButtonEvent implements android.os.Parcelable { + method public int describeContents(); + method public int getAction(); + method public int getButtonCode(); + method public long getEventTimeNanos(); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field public static final int ACTION_BUTTON_PRESS = 11; // 0xb + field public static final int ACTION_BUTTON_RELEASE = 12; // 0xc + field public static final int BUTTON_PRIMARY = 32; // 0x20 + field public static final int BUTTON_SECONDARY = 64; // 0x40 + field @NonNull public static final android.os.Parcelable.Creator<android.hardware.input.VirtualStylusButtonEvent> CREATOR; + } + + @FlaggedApi("android.companion.virtual.flags.virtual_stylus") public static final class VirtualStylusButtonEvent.Builder { + ctor public VirtualStylusButtonEvent.Builder(); + method @NonNull public android.hardware.input.VirtualStylusButtonEvent build(); + method @NonNull public android.hardware.input.VirtualStylusButtonEvent.Builder setAction(int); + method @NonNull public android.hardware.input.VirtualStylusButtonEvent.Builder setButtonCode(int); + method @NonNull public android.hardware.input.VirtualStylusButtonEvent.Builder setEventTimeNanos(long); + } + + @FlaggedApi("android.companion.virtual.flags.virtual_stylus") public final class VirtualStylusConfig extends android.hardware.input.VirtualInputDeviceConfig implements android.os.Parcelable { + method public int describeContents(); + method public int getHeight(); + method public int getWidth(); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator<android.hardware.input.VirtualStylusConfig> CREATOR; + } + + @FlaggedApi("android.companion.virtual.flags.virtual_stylus") public static final class VirtualStylusConfig.Builder extends android.hardware.input.VirtualInputDeviceConfig.Builder<android.hardware.input.VirtualStylusConfig.Builder> { + ctor public VirtualStylusConfig.Builder(@IntRange(from=1) int, @IntRange(from=1) int); + method @NonNull public android.hardware.input.VirtualStylusConfig build(); + } + + @FlaggedApi("android.companion.virtual.flags.virtual_stylus") public final class VirtualStylusMotionEvent implements android.os.Parcelable { + method public int describeContents(); + method public int getAction(); + method public long getEventTimeNanos(); + method public int getPressure(); + method public int getTiltX(); + method public int getTiltY(); + method public int getToolType(); + method public int getX(); + method public int getY(); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field public static final int ACTION_DOWN = 0; // 0x0 + field public static final int ACTION_MOVE = 2; // 0x2 + field public static final int ACTION_UP = 1; // 0x1 + field @NonNull public static final android.os.Parcelable.Creator<android.hardware.input.VirtualStylusMotionEvent> CREATOR; + field public static final int TOOL_TYPE_ERASER = 4; // 0x4 + field public static final int TOOL_TYPE_STYLUS = 2; // 0x2 + } + + @FlaggedApi("android.companion.virtual.flags.virtual_stylus") public static final class VirtualStylusMotionEvent.Builder { + ctor public VirtualStylusMotionEvent.Builder(); + method @NonNull public android.hardware.input.VirtualStylusMotionEvent build(); + method @NonNull public android.hardware.input.VirtualStylusMotionEvent.Builder setAction(int); + method @NonNull public android.hardware.input.VirtualStylusMotionEvent.Builder setEventTimeNanos(long); + method @NonNull public android.hardware.input.VirtualStylusMotionEvent.Builder setPressure(@IntRange(from=0x0, to=0xff) int); + method @NonNull public android.hardware.input.VirtualStylusMotionEvent.Builder setTiltX(@IntRange(from=0xffffffa6, to=0x5a) int); + method @NonNull public android.hardware.input.VirtualStylusMotionEvent.Builder setTiltY(@IntRange(from=0xffffffa6, to=0x5a) int); + method @NonNull public android.hardware.input.VirtualStylusMotionEvent.Builder setToolType(int); + method @NonNull public android.hardware.input.VirtualStylusMotionEvent.Builder setX(int); + method @NonNull public android.hardware.input.VirtualStylusMotionEvent.Builder setY(int); + } + public final class VirtualTouchEvent implements android.os.Parcelable { method public int describeContents(); method public int getAction(); @@ -9955,12 +10046,17 @@ package android.os { field @RequiresPermission(android.Manifest.permission.BATTERY_STATS) public static final int BATTERY_PROPERTY_CHARGING_POLICY = 9; // 0x9 field @RequiresPermission(android.Manifest.permission.BATTERY_STATS) public static final int BATTERY_PROPERTY_FIRST_USAGE_DATE = 8; // 0x8 field @RequiresPermission(android.Manifest.permission.BATTERY_STATS) public static final int BATTERY_PROPERTY_MANUFACTURING_DATE = 7; // 0x7 + field @FlaggedApi("android.os.battery_part_status_api") @RequiresPermission(android.Manifest.permission.BATTERY_STATS) public static final int BATTERY_PROPERTY_PART_STATUS = 12; // 0xc + field @FlaggedApi("android.os.battery_part_status_api") @RequiresPermission(android.Manifest.permission.BATTERY_STATS) public static final int BATTERY_PROPERTY_SERIAL_NUMBER = 11; // 0xb field public static final int CHARGING_POLICY_ADAPTIVE_AC = 3; // 0x3 field public static final int CHARGING_POLICY_ADAPTIVE_AON = 2; // 0x2 field public static final int CHARGING_POLICY_ADAPTIVE_LONGLIFE = 4; // 0x4 field public static final int CHARGING_POLICY_DEFAULT = 1; // 0x1 field public static final String EXTRA_EVENTS = "android.os.extra.EVENTS"; field public static final String EXTRA_EVENT_TIMESTAMP = "android.os.extra.EVENT_TIMESTAMP"; + field @FlaggedApi("android.os.battery_part_status_api") public static final int PART_STATUS_ORIGINAL = 1; // 0x1 + field @FlaggedApi("android.os.battery_part_status_api") public static final int PART_STATUS_REPLACED = 2; // 0x2 + field @FlaggedApi("android.os.battery_part_status_api") public static final int PART_STATUS_UNSUPPORTED = 0; // 0x0 } public final class BatterySaverPolicyConfig implements android.os.Parcelable { @@ -14471,6 +14567,7 @@ package android.telephony { field public static final int EVENT_SERVICE_STATE_CHANGED = 1; // 0x1 field public static final int EVENT_SIGNAL_STRENGTHS_CHANGED = 9; // 0x9 field public static final int EVENT_SIGNAL_STRENGTH_CHANGED = 2; // 0x2 + field @FlaggedApi("com.android.internal.telephony.flags.simultaneous_calling_indications") @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public static final int EVENT_SIMULTANEOUS_CELLULAR_CALLING_SUBSCRIPTIONS_CHANGED = 41; // 0x29 field @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public static final int EVENT_SRVCC_STATE_CHANGED = 16; // 0x10 field public static final int EVENT_USER_MOBILE_DATA_STATE_CHANGED = 20; // 0x14 field @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public static final int EVENT_VOICE_ACTIVATION_STATE_CHANGED = 18; // 0x12 @@ -14517,6 +14614,10 @@ package android.telephony { method @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public void onRadioPowerStateChanged(int); } + @FlaggedApi("com.android.internal.telephony.flags.simultaneous_calling_indications") public static interface TelephonyCallback.SimultaneousCellularCallingSupportListener { + method @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public void onSimultaneousCellularCallingSubscriptionsChanged(@NonNull java.util.Set<java.lang.Integer>); + } + public static interface TelephonyCallback.SrvccStateListener { method @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public void onSrvccStateChanged(int); } @@ -17071,6 +17172,7 @@ package android.telephony.satellite { field @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public static final int NT_RADIO_TECHNOLOGY_NR_NTN = 2; // 0x2 field @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public static final int NT_RADIO_TECHNOLOGY_PROPRIETARY = 4; // 0x4 field @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public static final int NT_RADIO_TECHNOLOGY_UNKNOWN = 0; // 0x0 + field @FlaggedApi("com.android.internal.telephony.flags.carrier_enabled_satellite_flag") public static final int SATELLITE_COMMUNICATION_RESTRICTION_REASON_ENTITLEMENT = 2; // 0x2 field @FlaggedApi("com.android.internal.telephony.flags.carrier_enabled_satellite_flag") public static final int SATELLITE_COMMUNICATION_RESTRICTION_REASON_GEOLOCATION = 1; // 0x1 field @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public static final int SATELLITE_DATAGRAM_TRANSFER_STATE_IDLE = 0; // 0x0 field @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public static final int SATELLITE_DATAGRAM_TRANSFER_STATE_RECEIVE_FAILED = 7; // 0x7 diff --git a/core/api/test-current.txt b/core/api/test-current.txt index bbe03a3d11a2..a5c2af76479b 100644 --- a/core/api/test-current.txt +++ b/core/api/test-current.txt @@ -284,16 +284,6 @@ package android.app { method public default void onOpActiveChanged(@NonNull String, int, @NonNull String, @Nullable String, boolean, int, int); } - public final class AutomaticZenRule implements android.os.Parcelable { - method @FlaggedApi("android.app.modes_api") public int getUserModifiedFields(); - field @FlaggedApi("android.app.modes_api") public static final int FIELD_INTERRUPTION_FILTER = 2; // 0x2 - field @FlaggedApi("android.app.modes_api") public static final int FIELD_NAME = 1; // 0x1 - } - - @FlaggedApi("android.app.modes_api") public static final class AutomaticZenRule.Builder { - method @FlaggedApi("android.app.modes_api") @NonNull public android.app.AutomaticZenRule.Builder setUserModifiedFields(int); - } - public class BroadcastOptions extends android.app.ComponentOptions { ctor public BroadcastOptions(); ctor public BroadcastOptions(@NonNull android.os.Bundle); @@ -493,10 +483,15 @@ package android.app { } public class UiModeManager { + method @FlaggedApi("android.app.modes_api") @RequiresPermission(android.Manifest.permission.MODIFY_DAY_NIGHT_MODE) public int getAttentionModeThemeOverlay(); method public boolean isNightModeLocked(); method public boolean isUiModeLocked(); method @RequiresPermission(value=android.Manifest.permission.TOGGLE_AUTOMOTIVE_PROJECTION, conditional=true) public boolean releaseProjection(int); method @RequiresPermission(value=android.Manifest.permission.TOGGLE_AUTOMOTIVE_PROJECTION, conditional=true) public boolean requestProjection(int); + field @FlaggedApi("android.app.modes_api") public static final int MODE_ATTENTION_THEME_OVERLAY_DAY = 1002; // 0x3ea + field @FlaggedApi("android.app.modes_api") public static final int MODE_ATTENTION_THEME_OVERLAY_NIGHT = 1001; // 0x3e9 + field @FlaggedApi("android.app.modes_api") public static final int MODE_ATTENTION_THEME_OVERLAY_OFF = 1000; // 0x3e8 + field @FlaggedApi("android.app.modes_api") public static final int MODE_ATTENTION_THEME_OVERLAY_UNKNOWN = -1; // 0xffffffff field public static final int PROJECTION_TYPE_ALL = -1; // 0xffffffff field public static final int PROJECTION_TYPE_AUTOMOTIVE = 1; // 0x1 field public static final int PROJECTION_TYPE_NONE = 0; // 0x0 @@ -1177,6 +1172,7 @@ package android.content.pm { method public int getShowInLauncher(); field public static final int SHOW_IN_LAUNCHER_NO = 2; // 0x2 field public static final int SHOW_IN_LAUNCHER_SEPARATE = 1; // 0x1 + field public static final int SHOW_IN_LAUNCHER_UNKNOWN = -1; // 0xffffffff field public static final int SHOW_IN_LAUNCHER_WITH_PARENT = 0; // 0x0 } @@ -3021,47 +3017,8 @@ package android.service.notification { method @Deprecated public boolean isBound(); } - @FlaggedApi("android.app.modes_api") public final class ZenDeviceEffects implements android.os.Parcelable { - method public int getUserModifiedFields(); - field public static final int FIELD_DIM_WALLPAPER = 4; // 0x4 - field public static final int FIELD_DISABLE_AUTO_BRIGHTNESS = 16; // 0x10 - field public static final int FIELD_DISABLE_TAP_TO_WAKE = 32; // 0x20 - field public static final int FIELD_DISABLE_TILT_TO_WAKE = 64; // 0x40 - field public static final int FIELD_DISABLE_TOUCH = 128; // 0x80 - field public static final int FIELD_GRAYSCALE = 1; // 0x1 - field public static final int FIELD_MAXIMIZE_DOZE = 512; // 0x200 - field public static final int FIELD_MINIMIZE_RADIO_USAGE = 256; // 0x100 - field public static final int FIELD_NIGHT_MODE = 8; // 0x8 - field public static final int FIELD_SUPPRESS_AMBIENT_DISPLAY = 2; // 0x2 - } - - @FlaggedApi("android.app.modes_api") public static final class ZenDeviceEffects.Builder { - method @NonNull public android.service.notification.ZenDeviceEffects.Builder setUserModifiedFields(int); - } - - public final class ZenPolicy implements android.os.Parcelable { - method @FlaggedApi("android.app.modes_api") public int getUserModifiedFields(); - field @FlaggedApi("android.app.modes_api") public static final int FIELD_ALLOW_CHANNELS = 8; // 0x8 - field @FlaggedApi("android.app.modes_api") public static final int FIELD_CALLS = 2; // 0x2 - field @FlaggedApi("android.app.modes_api") public static final int FIELD_CONVERSATIONS = 4; // 0x4 - field @FlaggedApi("android.app.modes_api") public static final int FIELD_MESSAGES = 1; // 0x1 - field @FlaggedApi("android.app.modes_api") public static final int FIELD_PRIORITY_CATEGORY_ALARMS = 128; // 0x80 - field @FlaggedApi("android.app.modes_api") public static final int FIELD_PRIORITY_CATEGORY_EVENTS = 32; // 0x20 - field @FlaggedApi("android.app.modes_api") public static final int FIELD_PRIORITY_CATEGORY_MEDIA = 256; // 0x100 - field @FlaggedApi("android.app.modes_api") public static final int FIELD_PRIORITY_CATEGORY_REPEAT_CALLERS = 64; // 0x40 - field @FlaggedApi("android.app.modes_api") public static final int FIELD_PRIORITY_CATEGORY_SYSTEM = 512; // 0x200 - field @FlaggedApi("android.app.modes_api") public static final int FIELD_VISUAL_EFFECT_AMBIENT = 32768; // 0x8000 - field @FlaggedApi("android.app.modes_api") public static final int FIELD_VISUAL_EFFECT_BADGE = 16384; // 0x4000 - field @FlaggedApi("android.app.modes_api") public static final int FIELD_VISUAL_EFFECT_FULL_SCREEN_INTENT = 1024; // 0x400 - field @FlaggedApi("android.app.modes_api") public static final int FIELD_VISUAL_EFFECT_LIGHTS = 2048; // 0x800 - field @FlaggedApi("android.app.modes_api") public static final int FIELD_VISUAL_EFFECT_NOTIFICATION_LIST = 65536; // 0x10000 - field @FlaggedApi("android.app.modes_api") public static final int FIELD_VISUAL_EFFECT_PEEK = 4096; // 0x1000 - field @FlaggedApi("android.app.modes_api") public static final int FIELD_VISUAL_EFFECT_STATUS_BAR = 8192; // 0x2000 - } - public static final class ZenPolicy.Builder { ctor public ZenPolicy.Builder(@Nullable android.service.notification.ZenPolicy); - method @FlaggedApi("android.app.modes_api") @NonNull public android.service.notification.ZenPolicy.Builder setUserModifiedFields(int); } } diff --git a/core/java/android/adaptiveauth/OWNERS b/core/java/android/adaptiveauth/OWNERS new file mode 100644 index 000000000000..0218a7835586 --- /dev/null +++ b/core/java/android/adaptiveauth/OWNERS @@ -0,0 +1 @@ +include /services/core/java/com/android/server/adaptiveauth/OWNERS
\ No newline at end of file diff --git a/core/java/android/adaptiveauth/flags.aconfig b/core/java/android/adaptiveauth/flags.aconfig new file mode 100644 index 000000000000..39e46bbdfa6a --- /dev/null +++ b/core/java/android/adaptiveauth/flags.aconfig @@ -0,0 +1,8 @@ +package: "android.adaptiveauth" + +flag { + name: "report_biometric_auth_attempts" + namespace: "biometrics" + description: "Control the usage of the biometric auth signal in adaptive auth" + bug: "285053096" +}
\ No newline at end of file diff --git a/core/java/android/app/ActivityManager.java b/core/java/android/app/ActivityManager.java index 6b7f4880e2f0..1fd49ef43e93 100644 --- a/core/java/android/app/ActivityManager.java +++ b/core/java/android/app/ActivityManager.java @@ -3574,7 +3574,7 @@ public class ActivityManager { * foreground. This may be running a window that is behind the current * foreground (so paused and with its state saved, not interacting with * the user, but visible to them to some degree); it may also be running - * other services under the system's control that it inconsiders important. + * other services under the system's control that it considers important. */ public static final int IMPORTANCE_VISIBLE = 200; @@ -3646,9 +3646,9 @@ public class ActivityManager { public static final int IMPORTANCE_CANT_SAVE_STATE = 350; /** - * Constant for {@link #importance}: This process process contains - * cached code that is expendable, not actively running any app components - * we care about. + * Constant for {@link #importance}: This process contains cached code + * that is expendable, not actively running any app components we care + * about. */ public static final int IMPORTANCE_CACHED = 400; @@ -4052,10 +4052,28 @@ public class ActivityManager { } } + private final ArrayList<AppStartInfoCallbackWrapper> mAppStartInfoCallbacks = + new ArrayList<>(); + @Nullable + private IApplicationStartInfoCompleteListener mAppStartInfoCompleteListener = null; + + private static final class AppStartInfoCallbackWrapper { + @NonNull final Executor mExecutor; + @NonNull final Consumer<ApplicationStartInfo> mListener; + + AppStartInfoCallbackWrapper(@NonNull final Executor executor, + @NonNull final Consumer<ApplicationStartInfo> listener) { + mExecutor = executor; + mListener = listener; + } + } + /** - * Sets a callback to be notified when the {@link ApplicationStartInfo} records of this startup + * Adds a callback to be notified when the {@link ApplicationStartInfo} records of this startup * are complete. * + * <p class="note"> Note: callback will be removed automatically after being triggered.</p> + * * <p class="note"> Note: callback will not wait for {@link Activity#reportFullyDrawn} to occur. * Timestamp for fully drawn may be added after callback occurs. Set callback after invoking * {@link Activity#reportFullyDrawn} if timestamp for fully drawn is required.</p> @@ -4073,33 +4091,77 @@ public class ActivityManager { * @throws IllegalArgumentException if executor or listener are null. */ @FlaggedApi(Flags.FLAG_APP_START_INFO) - public void setApplicationStartInfoCompletionListener(@NonNull final Executor executor, + public void addApplicationStartInfoCompletionListener(@NonNull final Executor executor, @NonNull final Consumer<ApplicationStartInfo> listener) { Preconditions.checkNotNull(executor, "executor cannot be null"); Preconditions.checkNotNull(listener, "listener cannot be null"); - IApplicationStartInfoCompleteListener callback = - new IApplicationStartInfoCompleteListener.Stub() { - @Override - public void onApplicationStartInfoComplete(ApplicationStartInfo applicationStartInfo) { - executor.execute(() -> listener.accept(applicationStartInfo)); + synchronized (mAppStartInfoCallbacks) { + for (int i = 0; i < mAppStartInfoCallbacks.size(); i++) { + if (listener.equals(mAppStartInfoCallbacks.get(i).mListener)) { + return; + } + } + if (mAppStartInfoCompleteListener == null) { + mAppStartInfoCompleteListener = new IApplicationStartInfoCompleteListener.Stub() { + @Override + public void onApplicationStartInfoComplete( + ApplicationStartInfo applicationStartInfo) { + synchronized (mAppStartInfoCallbacks) { + for (int i = 0; i < mAppStartInfoCallbacks.size(); i++) { + final AppStartInfoCallbackWrapper callback = + mAppStartInfoCallbacks.get(i); + callback.mExecutor.execute(() -> callback.mListener.accept( + applicationStartInfo)); + } + mAppStartInfoCallbacks.clear(); + mAppStartInfoCompleteListener = null; + } + } + }; + boolean succeeded = false; + try { + getService().addApplicationStartInfoCompleteListener( + mAppStartInfoCompleteListener, mContext.getUserId()); + succeeded = true; + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + if (succeeded) { + mAppStartInfoCallbacks.add(new AppStartInfoCallbackWrapper(executor, listener)); + } else { + mAppStartInfoCompleteListener = null; + mAppStartInfoCallbacks.clear(); + } + } else { + mAppStartInfoCallbacks.add(new AppStartInfoCallbackWrapper(executor, listener)); } - }; - try { - getService().setApplicationStartInfoCompleteListener(callback, mContext.getUserId()); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); } } /** - * Removes the callback set by {@link #setApplicationStartInfoCompletionListener} if there is one. + * Removes the provided callback set by {@link #addApplicationStartInfoCompletionListener}. */ @FlaggedApi(Flags.FLAG_APP_START_INFO) - public void clearApplicationStartInfoCompletionListener() { - try { - getService().clearApplicationStartInfoCompleteListener(mContext.getUserId()); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); + public void removeApplicationStartInfoCompletionListener( + @NonNull final Consumer<ApplicationStartInfo> listener) { + Preconditions.checkNotNull(listener, "listener cannot be null"); + synchronized (mAppStartInfoCallbacks) { + for (int i = 0; i < mAppStartInfoCallbacks.size(); i++) { + final AppStartInfoCallbackWrapper callback = mAppStartInfoCallbacks.get(i); + if (listener.equals(callback.mListener)) { + mAppStartInfoCallbacks.remove(i); + break; + } + } + if (mAppStartInfoCompleteListener != null && mAppStartInfoCallbacks.isEmpty()) { + try { + getService().removeApplicationStartInfoCompleteListener( + mAppStartInfoCompleteListener, mContext.getUserId()); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + mAppStartInfoCompleteListener = null; + } } } diff --git a/core/java/android/app/AppOpsManager.java b/core/java/android/app/AppOpsManager.java index 1db1caf51800..4b2e93fda171 100644 --- a/core/java/android/app/AppOpsManager.java +++ b/core/java/android/app/AppOpsManager.java @@ -1540,9 +1540,16 @@ public class AppOpsManager { public static final int OP_RAPID_CLEAR_NOTIFICATIONS_BY_LISTENER = AppProtoEnums.APP_OP_RAPID_CLEAR_NOTIFICATIONS_BY_LISTENER; + /** + * See {@link #OPSTR_READ_SYSTEM_GRAMMATICAL_GENDER}. + * @hide + */ + public static final int OP_READ_SYSTEM_GRAMMATICAL_GENDER = + AppProtoEnums.APP_OP_READ_SYSTEM_GRAMMATICAL_GENDER; + /** @hide */ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) - public static final int _NUM_OP = 143; + public static final int _NUM_OP = 144; /** * All app ops represented as strings. @@ -2180,6 +2187,8 @@ public class AppOpsManager { * * @hide */ + @FlaggedApi(android.permission.flags.Flags.FLAG_ENHANCED_CONFIRMATION_MODE_APIS_ENABLED) + @SystemApi public static final String OPSTR_ACCESS_RESTRICTED_SETTINGS = "android:access_restricted_settings"; @@ -2373,6 +2382,14 @@ public class AppOpsManager { public static final String OPSTR_RAPID_CLEAR_NOTIFICATIONS_BY_LISTENER = "android:rapid_clear_notifications_by_listener"; + /** + * Allows an application to read the system grammatical gender. + * + * @hide + */ + public static final String OPSTR_READ_SYSTEM_GRAMMATICAL_GENDER = + "android:read_system_grammatical_gender"; + /** {@link #sAppOpsToNote} not initialized yet for this op */ private static final byte SHOULD_COLLECT_NOTE_OP_NOT_INITIALIZED = 0; /** Should not collect noting of this app-op in {@link #sAppOpsToNote} */ @@ -2484,6 +2501,7 @@ public class AppOpsManager { OP_RECEIVE_SANDBOX_TRIGGER_AUDIO, OP_RECEIVE_SANDBOXED_DETECTION_TRAINING_DATA, OP_MEDIA_ROUTING_CONTROL, + OP_READ_SYSTEM_GRAMMATICAL_GENDER, }; static final AppOpInfo[] sAppOpInfos = new AppOpInfo[]{ @@ -2936,6 +2954,10 @@ public class AppOpsManager { OPSTR_RAPID_CLEAR_NOTIFICATIONS_BY_LISTENER, "RAPID_CLEAR_NOTIFICATIONS_BY_LISTENER") .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) + .build(), }; // The number of longs needed to form a full bitmask of app ops diff --git a/core/java/android/app/ApplicationStartInfo.java b/core/java/android/app/ApplicationStartInfo.java index 656feb0401d6..afa513dbaaef 100644 --- a/core/java/android/app/ApplicationStartInfo.java +++ b/core/java/android/app/ApplicationStartInfo.java @@ -26,11 +26,19 @@ import android.icu.text.SimpleDateFormat; import android.os.Parcel; import android.os.Parcelable; import android.os.UserHandle; +import android.text.TextUtils; import android.util.ArrayMap; +import android.util.Xml; import android.util.proto.ProtoInputStream; import android.util.proto.ProtoOutputStream; import android.util.proto.WireTypeMismatchException; +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.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -39,12 +47,18 @@ import java.io.ObjectOutputStream; import java.io.PrintWriter; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; -import java.util.Iterator; import java.util.Map; -import java.util.Set; +import java.util.Objects; /** - * Provide information related to a processes startup. + * Describes information related to an application process's startup. + * + * <p> + * Many aspects concerning why and how an applications process was started are valuable for apps + * both for logging and for potential behavior changes. Reason for process start, start type, + * start times, throttling, and other useful diagnostic data can be obtained from + * {@link ApplicationStartInfo} records. + * </p> */ @FlaggedApi(Flags.FLAG_APP_START_INFO) public final class ApplicationStartInfo implements Parcelable { @@ -210,6 +224,11 @@ public final class ApplicationStartInfo implements Parcelable { private int mDefiningUid; /** + * @see #getPackageName + */ + private String mPackageName; + + /** * @see #getProcessName */ private String mProcessName; @@ -344,6 +363,14 @@ public final class ApplicationStartInfo implements Parcelable { } /** + * @see #getPackageName + * @hide + */ + public void setPackageName(final String packageName) { + mPackageName = intern(packageName); + } + + /** * @see #getProcessName * @hide */ @@ -456,6 +483,15 @@ public final class ApplicationStartInfo implements Parcelable { } /** + * Name of first package running in this process; + * + * @hide + */ + public String getPackageName() { + return mPackageName; + } + + /** * The actual process name it was running with. * * <p class="note"> Note: field will be set for any {@link #getStartupState} value.</p> @@ -550,15 +586,15 @@ public final class ApplicationStartInfo implements Parcelable { dest.writeInt(mRealUid); dest.writeInt(mPackageUid); dest.writeInt(mDefiningUid); + dest.writeString(mPackageName); dest.writeString(mProcessName); dest.writeInt(mReason); - dest.writeInt(mStartupTimestampsNs.size()); - Set<Map.Entry<Integer, Long>> timestampEntrySet = mStartupTimestampsNs.entrySet(); - Iterator<Map.Entry<Integer, Long>> iter = timestampEntrySet.iterator(); - while (iter.hasNext()) { - Map.Entry<Integer, Long> entry = iter.next(); - dest.writeInt(entry.getKey()); - dest.writeLong(entry.getValue()); + dest.writeInt(mStartupTimestampsNs == null ? 0 : mStartupTimestampsNs.size()); + if (mStartupTimestampsNs != null) { + for (int i = 0; i < mStartupTimestampsNs.size(); i++) { + dest.writeInt(mStartupTimestampsNs.keyAt(i)); + dest.writeLong(mStartupTimestampsNs.valueAt(i)); + } } dest.writeInt(mStartType); dest.writeParcelable(mStartIntent, flags); @@ -575,6 +611,7 @@ public final class ApplicationStartInfo implements Parcelable { mRealUid = other.mRealUid; mPackageUid = other.mPackageUid; mDefiningUid = other.mDefiningUid; + mPackageName = other.mPackageName; mProcessName = other.mProcessName; mReason = other.mReason; mStartupTimestampsNs = other.mStartupTimestampsNs; @@ -589,6 +626,7 @@ public final class ApplicationStartInfo implements Parcelable { mRealUid = in.readInt(); mPackageUid = in.readInt(); mDefiningUid = in.readInt(); + mPackageName = intern(in.readString()); mProcessName = intern(in.readString()); mReason = in.readInt(); int starupTimestampCount = in.readInt(); @@ -620,6 +658,11 @@ public final class ApplicationStartInfo implements Parcelable { } }; + private static final String PROTO_SERIALIZER_ATTRIBUTE_TIMESTAMPS = "timestamps"; + 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"; + /** * Write to a protocol buffer output stream. Protocol buffer message definition at {@link * android.app.ApplicationStartInfoProto} @@ -640,9 +683,22 @@ public final class ApplicationStartInfo implements Parcelable { if (mStartupTimestampsNs != null && mStartupTimestampsNs.size() > 0) { ByteArrayOutputStream timestampsBytes = new ByteArrayOutputStream(); ObjectOutputStream timestampsOut = new ObjectOutputStream(timestampsBytes); - timestampsOut.writeObject(mStartupTimestampsNs); + TypedXmlSerializer serializer = Xml.resolveSerializer(timestampsOut); + serializer.startDocument(null, true); + serializer.startTag(null, PROTO_SERIALIZER_ATTRIBUTE_TIMESTAMPS); + for (int i = 0; i < mStartupTimestampsNs.size(); i++) { + serializer.startTag(null, PROTO_SERIALIZER_ATTRIBUTE_TIMESTAMP); + serializer.attributeInt(null, PROTO_SERIALIZER_ATTRIBUTE_KEY, + mStartupTimestampsNs.keyAt(i)); + serializer.attributeLong(null, PROTO_SERIALIZER_ATTRIBUTE_TS, + mStartupTimestampsNs.valueAt(i)); + serializer.endTag(null, PROTO_SERIALIZER_ATTRIBUTE_TIMESTAMP); + } + serializer.endTag(null, PROTO_SERIALIZER_ATTRIBUTE_TIMESTAMPS); + serializer.endDocument(); proto.write(ApplicationStartInfoProto.STARTUP_TIMESTAMPS, timestampsBytes.toByteArray()); + timestampsOut.close(); } proto.write(ApplicationStartInfoProto.START_TYPE, mStartType); if (mStartIntent != null) { @@ -693,7 +749,24 @@ public final class ApplicationStartInfo implements Parcelable { ByteArrayInputStream timestampsBytes = new ByteArrayInputStream(proto.readBytes( ApplicationStartInfoProto.STARTUP_TIMESTAMPS)); ObjectInputStream timestampsIn = new ObjectInputStream(timestampsBytes); - mStartupTimestampsNs = (ArrayMap<Integer, Long>) timestampsIn.readObject(); + mStartupTimestampsNs = new ArrayMap<Integer, Long>(); + try { + TypedXmlPullParser parser = Xml.resolvePullParser(timestampsIn); + XmlUtils.beginDocument(parser, PROTO_SERIALIZER_ATTRIBUTE_TIMESTAMPS); + int depth = parser.getDepth(); + while (XmlUtils.nextElementWithin(parser, depth)) { + if (PROTO_SERIALIZER_ATTRIBUTE_TIMESTAMP.equals(parser.getName())) { + int key = parser.getAttributeInt(null, + PROTO_SERIALIZER_ATTRIBUTE_KEY); + long ts = parser.getAttributeLong(null, + PROTO_SERIALIZER_ATTRIBUTE_TS); + mStartupTimestampsNs.put(key, ts); + } + } + } catch (XmlPullParserException e) { + // Timestamps lost + } + timestampsIn.close(); break; case (int) ApplicationStartInfoProto.START_TYPE: mStartType = proto.readInt(ApplicationStartInfoProto.START_TYPE); @@ -730,6 +803,7 @@ public final class ApplicationStartInfo implements Parcelable { .append(" definingUid=").append(mDefiningUid) .append(" user=").append(UserHandle.getUserId(mPackageUid)) .append('\n') + .append(" package=").append(mPackageName) .append(" process=").append(mProcessName) .append(" startupState=").append(mStartupState) .append(" reason=").append(reasonToString(mReason)) @@ -740,13 +814,11 @@ public final class ApplicationStartInfo implements Parcelable { sb.append(" intent=").append(mStartIntent.toString()) .append('\n'); } - if (mStartupTimestampsNs.size() > 0) { + if (mStartupTimestampsNs != null && mStartupTimestampsNs.size() > 0) { sb.append(" timestamps: "); - Set<Map.Entry<Integer, Long>> timestampEntrySet = mStartupTimestampsNs.entrySet(); - Iterator<Map.Entry<Integer, Long>> iter = timestampEntrySet.iterator(); - while (iter.hasNext()) { - Map.Entry<Integer, Long> entry = iter.next(); - sb.append(entry.getKey()).append("=").append(entry.getValue()).append(" "); + for (int i = 0; i < mStartupTimestampsNs.size(); i++) { + sb.append(mStartupTimestampsNs.keyAt(i)).append("=").append(mStartupTimestampsNs + .valueAt(i)).append(" "); } sb.append('\n'); } @@ -780,4 +852,35 @@ public final class ApplicationStartInfo implements Parcelable { default -> ""; }; } + + /** @hide */ + @Override + public boolean equals(@Nullable Object other) { + if (other == null || !(other instanceof ApplicationStartInfo)) { + return false; + } + final ApplicationStartInfo o = (ApplicationStartInfo) other; + return mPid == o.mPid && mRealUid == o.mRealUid && mPackageUid == o.mPackageUid + && mDefiningUid == o.mDefiningUid && mReason == o.mReason + && mStartupState == o.mStartupState && mStartType == o.mStartType + && mLaunchMode == o.mLaunchMode && TextUtils.equals(mProcessName, o.mProcessName) + && timestampsEquals(o); + } + + @Override + public int hashCode() { + return Objects.hash(mPid, mRealUid, mPackageUid, mDefiningUid, mReason, mStartupState, + mStartType, mLaunchMode, mProcessName, + mStartupTimestampsNs); + } + + private boolean timestampsEquals(@NonNull ApplicationStartInfo other) { + if (mStartupTimestampsNs == null && other.mStartupTimestampsNs == null) { + return true; + } + if (mStartupTimestampsNs == null || other.mStartupTimestampsNs == null) { + return false; + } + return mStartupTimestampsNs.equals(other.mStartupTimestampsNs); + } } diff --git a/core/java/android/app/AutomaticZenRule.java b/core/java/android/app/AutomaticZenRule.java index 5b354fc3b9ed..d57a4e583a1a 100644 --- a/core/java/android/app/AutomaticZenRule.java +++ b/core/java/android/app/AutomaticZenRule.java @@ -23,7 +23,6 @@ import android.annotation.FlaggedApi; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; -import android.annotation.TestApi; import android.app.NotificationManager.InterruptionFilter; import android.content.ComponentName; import android.net.Uri; @@ -113,8 +112,8 @@ public final class AutomaticZenRule implements Parcelable { @Retention(RetentionPolicy.SOURCE) public @interface Type {} - /** Used to track which rule variables have been modified by the user. - * Should be checked against the bitmask {@link #getUserModifiedFields()}. + /** + * Enum for the user-modifiable fields in this object. * @hide */ @IntDef(flag = true, prefix = { "FIELD_" }, value = { @@ -128,13 +127,11 @@ public final class AutomaticZenRule implements Parcelable { * @hide */ @FlaggedApi(Flags.FLAG_MODES_API) - @TestApi public static final int FIELD_NAME = 1 << 0; /** * @hide */ @FlaggedApi(Flags.FLAG_MODES_API) - @TestApi public static final int FIELD_INTERRUPTION_FILTER = 1 << 1; private boolean enabled; @@ -153,7 +150,6 @@ public final class AutomaticZenRule implements Parcelable { private int mIconResId; private String mTriggerDescription; private boolean mAllowManualInvocation; - private @ModifiableField int mUserModifiedFields; // Bitwise representation /** * The maximum string length for any string contained in this automatic zen rule. This pertains @@ -256,7 +252,6 @@ public final class AutomaticZenRule implements Parcelable { mIconResId = source.readInt(); mTriggerDescription = getTrimmedString(source.readString(), MAX_DESC_LENGTH); mType = source.readInt(); - mUserModifiedFields = source.readInt(); } } @@ -307,8 +302,7 @@ public final class AutomaticZenRule implements Parcelable { * Returns whether this rule's name has been modified by the user. * @hide */ - // TODO: b/310620812 - Replace with mUserModifiedFields & FIELD_NAME once - // FLAG_MODES_API is inlined. + // TODO: b/310620812 - Consider removing completely. Seems not be used anywhere except tests. public boolean isModified() { return mModified; } @@ -506,32 +500,6 @@ public final class AutomaticZenRule implements Parcelable { return type; } - /** - * Gets the bitmask representing which fields are user modified. Bits are set using - * {@link ModifiableField}. - * @hide - */ - @FlaggedApi(Flags.FLAG_MODES_API) - @TestApi - public @ModifiableField int getUserModifiedFields() { - return mUserModifiedFields; - } - - /** - * Returns {@code true} if the {@link AutomaticZenRule} can be updated. - * When this returns {@code false}, calls to - * {@link NotificationManager#updateAutomaticZenRule(String, AutomaticZenRule)}) with this rule - * will ignore changes to user-configurable fields. - */ - @FlaggedApi(Flags.FLAG_MODES_API) - public boolean canUpdate() { - // The rule is considered updateable if its bitmask has no user modifications, and - // the bitmasks of the policy and device effects have no modification. - return mUserModifiedFields == 0 - && (mZenPolicy == null || mZenPolicy.getUserModifiedFields() == 0) - && (mDeviceEffects == null || mDeviceEffects.getUserModifiedFields() == 0); - } - @Override public int describeContents() { return 0; @@ -560,7 +528,6 @@ public final class AutomaticZenRule implements Parcelable { dest.writeInt(mIconResId); dest.writeString(mTriggerDescription); dest.writeInt(mType); - dest.writeInt(mUserModifiedFields); } } @@ -582,16 +549,14 @@ public final class AutomaticZenRule implements Parcelable { .append(",allowManualInvocation=").append(mAllowManualInvocation) .append(",iconResId=").append(mIconResId) .append(",triggerDescription=").append(mTriggerDescription) - .append(",type=").append(mType) - .append(",userModifiedFields=") - .append(modifiedFieldsToString(mUserModifiedFields)); + .append(",type=").append(mType); } return sb.append(']').toString(); } - @FlaggedApi(Flags.FLAG_MODES_API) - private String modifiedFieldsToString(int bitmask) { + /** @hide */ + public static String fieldsToString(@ModifiableField int bitmask) { ArrayList<String> modified = new ArrayList<>(); if ((bitmask & FIELD_NAME) != 0) { modified.add("FIELD_NAME"); @@ -623,8 +588,7 @@ public final class AutomaticZenRule implements Parcelable { && other.mAllowManualInvocation == mAllowManualInvocation && other.mIconResId == mIconResId && Objects.equals(other.mTriggerDescription, mTriggerDescription) - && other.mType == mType - && other.mUserModifiedFields == mUserModifiedFields; + && other.mType == mType; } return finalEquals; } @@ -634,8 +598,7 @@ public final class AutomaticZenRule implements Parcelable { if (Flags.modesApi()) { return Objects.hash(enabled, name, interruptionFilter, conditionId, owner, configurationActivity, mZenPolicy, mDeviceEffects, mModified, creationTime, - mPkg, mAllowManualInvocation, mIconResId, mTriggerDescription, mType, - mUserModifiedFields); + mPkg, mAllowManualInvocation, mIconResId, mTriggerDescription, mType); } return Objects.hash(enabled, name, interruptionFilter, conditionId, owner, configurationActivity, mZenPolicy, mModified, creationTime, mPkg); @@ -704,7 +667,6 @@ public final class AutomaticZenRule implements Parcelable { private boolean mAllowManualInvocation; private long mCreationTime; private String mPkg; - private @ModifiableField int mUserModifiedFields; public Builder(@NonNull AutomaticZenRule rule) { mName = rule.getName(); @@ -721,7 +683,6 @@ public final class AutomaticZenRule implements Parcelable { mAllowManualInvocation = rule.isManualInvocationAllowed(); mCreationTime = rule.getCreationTime(); mPkg = rule.getPackageName(); - mUserModifiedFields = rule.mUserModifiedFields; } public Builder(@NonNull String name, @NonNull Uri conditionId) { @@ -848,19 +809,6 @@ public final class AutomaticZenRule implements Parcelable { return this; } - /** - * Sets the bitmask representing which fields have been user-modified. - * This method should not be used outside of tests. The value of userModifiedFields - * should be set based on what values are changed when a rule is populated or updated.. - * @hide - */ - @FlaggedApi(Flags.FLAG_MODES_API) - @TestApi - public @NonNull Builder setUserModifiedFields(@ModifiableField int userModifiedFields) { - mUserModifiedFields = userModifiedFields; - return this; - } - public @NonNull AutomaticZenRule build() { AutomaticZenRule rule = new AutomaticZenRule(mName, mOwner, mConfigurationActivity, mConditionId, mPolicy, mInterruptionFilter, mEnabled); @@ -871,7 +819,6 @@ public final class AutomaticZenRule implements Parcelable { rule.mIconResId = mIconResId; rule.mAllowManualInvocation = mAllowManualInvocation; rule.setPackageName(mPkg); - rule.mUserModifiedFields = mUserModifiedFields; return rule; } diff --git a/core/java/android/app/ContextImpl.java b/core/java/android/app/ContextImpl.java index edeec77d48fe..ed00d9c1ddde 100644 --- a/core/java/android/app/ContextImpl.java +++ b/core/java/android/app/ContextImpl.java @@ -2409,6 +2409,17 @@ class ContextImpl extends Context { } } + @Override + public int checkContentUriPermissionFull(Uri uri, int pid, int uid, int modeFlags) { + try { + return ActivityManager.getService().checkContentUriPermissionFull( + ContentProvider.getUriWithoutUserId(uri), pid, uid, modeFlags, + resolveUserId(uri)); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + @NonNull @Override public int[] checkUriPermissions(@NonNull List<Uri> uris, int pid, int uid, diff --git a/core/java/android/app/IActivityManager.aidl b/core/java/android/app/IActivityManager.aidl index 260e9859c72d..b5d88e878d8d 100644 --- a/core/java/android/app/IActivityManager.aidl +++ b/core/java/android/app/IActivityManager.aidl @@ -274,6 +274,7 @@ interface IActivityManager { int getProcessLimit(); int checkUriPermission(in Uri uri, int pid, int uid, int mode, int userId, in IBinder callerToken); + int checkContentUriPermissionFull(in Uri uri, int pid, int uid, int mode, int userId); int[] checkUriPermissions(in List<Uri> uris, int pid, int uid, int mode, int userId, in IBinder callerToken); void grantUriPermission(in IApplicationThread caller, in String targetPkg, in Uri uri, @@ -715,7 +716,7 @@ interface IActivityManager { * @param listener A listener to for the callback upon completion of startup data collection. * @param userId The userId in the multi-user environment. */ - void setApplicationStartInfoCompleteListener(IApplicationStartInfoCompleteListener listener, + void addApplicationStartInfoCompleteListener(IApplicationStartInfoCompleteListener listener, int userId); @@ -724,7 +725,8 @@ interface IActivityManager { * * @param userId The userId in the multi-user environment. */ - void clearApplicationStartInfoCompleteListener(int userId); + void removeApplicationStartInfoCompleteListener(IApplicationStartInfoCompleteListener listener, + int userId); /** diff --git a/core/java/android/app/IUiModeManager.aidl b/core/java/android/app/IUiModeManager.aidl index 60b34cdc1b8a..3b83024d536b 100644 --- a/core/java/android/app/IUiModeManager.aidl +++ b/core/java/android/app/IUiModeManager.aidl @@ -95,6 +95,34 @@ interface IUiModeManager { int getNightModeCustomType(); /** + * Overlays current Night Mode value. + * {@code attentionModeThemeOverlayType}. + * + * @param attentionModeThemeOverlayType + * @hide + */ + @EnforcePermission("MODIFY_DAY_NIGHT_MODE") + @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.MODIFY_DAY_NIGHT_MODE)") + void setAttentionModeThemeOverlay(int attentionModeThemeOverlayType); + + + /** + * Returns current Attention Mode overlay type. + * <p> + * returns + * <ul> + * <li>{@link #MODE_ATTENTION_OFF}</li> + * <li>{@link #MODE_ATTENTION_NIGHT}</li> + * <li>{@link #MODE_ATTENTION_DAY}</li> + * </ul> + * </p> + * @hide + */ + @EnforcePermission("MODIFY_DAY_NIGHT_MODE") + @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.MODIFY_DAY_NIGHT_MODE)") + int getAttentionModeThemeOverlay(); + + /** * Sets the dark mode for the given application. This setting is persisted and will override the * system configuration for this application. * 1 - notnight mode diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java index 476232cb40b3..ed0cfbe3d9c3 100644 --- a/core/java/android/app/Notification.java +++ b/core/java/android/app/Notification.java @@ -5652,7 +5652,7 @@ public class Notification implements Parcelable pillColor = Colors.flattenAlpha( getColors(p).getTertiaryFixedDimAccentColor(), bgColor); textColor = Colors.flattenAlpha( - getColors(p).getOnTertiaryAccentTextColor(), pillColor); + getColors(p).getOnTertiaryFixedAccentTextColor(), pillColor); } contentView.setInt(R.id.expand_button, "setHighlightTextColor", textColor); contentView.setInt(R.id.expand_button, "setHighlightPillColor", pillColor); diff --git a/core/java/android/app/SystemServiceRegistry.java b/core/java/android/app/SystemServiceRegistry.java index 9cf732abb86a..d7554137fa5b 100644 --- a/core/java/android/app/SystemServiceRegistry.java +++ b/core/java/android/app/SystemServiceRegistry.java @@ -31,6 +31,7 @@ import android.app.appsearch.AppSearchManagerFrameworkInitializer; import android.app.blob.BlobStoreManagerFrameworkInitializer; import android.app.contentsuggestions.ContentSuggestionsManager; import android.app.contentsuggestions.IContentSuggestionsManager; +import android.app.ecm.EnhancedConfirmationFrameworkInitializer; import android.app.job.JobSchedulerFrameworkInitializer; import android.app.people.PeopleManager; import android.app.prediction.AppPredictionManager; @@ -1631,6 +1632,9 @@ public final class SystemServiceRegistry { OnDevicePersonalizationFrameworkInitializer.registerServiceWrappers(); DeviceLockFrameworkInitializer.registerServiceWrappers(); VirtualizationFrameworkInitializer.registerServiceWrappers(); + if (android.permission.flags.Flags.enhancedConfirmationModeApisEnabled()) { + EnhancedConfirmationFrameworkInitializer.registerServiceWrappers(); + } } finally { // If any of the above code throws, we're in a pretty bad shape and the process // will likely crash, but we'll reset it just in case there's an exception handler... diff --git a/core/java/android/app/UiModeManager.java b/core/java/android/app/UiModeManager.java index 0ccb9cddf58d..a27132872521 100644 --- a/core/java/android/app/UiModeManager.java +++ b/core/java/android/app/UiModeManager.java @@ -17,6 +17,7 @@ package android.app; import android.annotation.CallbackExecutor; +import android.annotation.FlaggedApi; import android.annotation.FloatRange; import android.annotation.IntDef; import android.annotation.IntRange; @@ -265,6 +266,60 @@ public class UiModeManager { */ public static final int MODE_NIGHT_YES = 2; + /** @hide */ + @IntDef(prefix = { "MODE_ATTENTION_THEME_OVERLAY_" }, value = { + MODE_ATTENTION_THEME_OVERLAY_OFF, + MODE_ATTENTION_THEME_OVERLAY_NIGHT, + MODE_ATTENTION_THEME_OVERLAY_DAY + }) + @Retention(RetentionPolicy.SOURCE) + public @interface AttentionModeThemeOverlayType {} + + /** @hide */ + @IntDef(prefix = { "MODE_ATTENTION_THEME_OVERLAY_" }, value = { + MODE_ATTENTION_THEME_OVERLAY_OFF, + MODE_ATTENTION_THEME_OVERLAY_NIGHT, + MODE_ATTENTION_THEME_OVERLAY_DAY, + MODE_ATTENTION_THEME_OVERLAY_UNKNOWN + }) + @Retention(RetentionPolicy.SOURCE) + public @interface AttentionModeThemeOverlayReturnType {} + + /** + * Constant for {@link #setAttentionModeThemeOverlay(int)} (int)} and {@link + * #getAttentionModeThemeOverlay()}: Keeps night mode as set by {@link #setNightMode(int)}. + * @hide + */ + @FlaggedApi(Flags.FLAG_MODES_API) + @TestApi + public static final int MODE_ATTENTION_THEME_OVERLAY_OFF = 1000; + + /** + * Constant for {@link #setAttentionModeThemeOverlay(int)} (int)} and {@link + * #getAttentionModeThemeOverlay()}: Maintains night mode always on. + * @hide + */ + @FlaggedApi(Flags.FLAG_MODES_API) + @TestApi + public static final int MODE_ATTENTION_THEME_OVERLAY_NIGHT = 1001; + + /** + * Constant for {@link #setAttentionModeThemeOverlay(int)} (int)} and {@link + * #getAttentionModeThemeOverlay()}: Maintains night mode always off (Light). + * @hide + */ + @FlaggedApi(Flags.FLAG_MODES_API) + @TestApi + public static final int MODE_ATTENTION_THEME_OVERLAY_DAY = 1002; + + /** + * Constant for {@link #getAttentionModeThemeOverlay()}: Error communication with server. + * @hide + */ + @FlaggedApi(Flags.FLAG_MODES_API) + @TestApi + public static final int MODE_ATTENTION_THEME_OVERLAY_UNKNOWN = -1; + /** * Granular types for {@link #setNightModeCustomType(int)} * @hide @@ -733,6 +788,55 @@ public class UiModeManager { } /** + * Overlays current Attention mode Night Mode overlay. + * {@code attentionModeThemeOverlayType}. + * + * @throws IllegalArgumentException if passed an unsupported type to + * {@code AttentionModeThemeOverlayType}. + * @hide + */ + @FlaggedApi(Flags.FLAG_MODES_API) + @RequiresPermission(android.Manifest.permission.MODIFY_DAY_NIGHT_MODE) + public void setAttentionModeThemeOverlay( + @AttentionModeThemeOverlayType int attentionModeThemeOverlayType) { + if (sGlobals != null) { + try { + sGlobals.mService.setAttentionModeThemeOverlay(attentionModeThemeOverlayType); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + } + + /** + * Returns the currently configured Attention Mode theme overlay. + * <p> + * May be one of: + * <ul> + * <li>{@link #MODE_ATTENTION_THEME_OVERLAY_OFF}</li> + * <li>{@link #MODE_ATTENTION_THEME_OVERLAY_NIGHT}</li> + * <li>{@link #MODE_ATTENTION_THEME_OVERLAY_DAY}</li> + * <li>{@link #MODE_ATTENTION_THEME_OVERLAY_UNKNOWN}</li> + * </ul> + * </p> + * + * @hide + */ + @FlaggedApi(Flags.FLAG_MODES_API) + @TestApi + @RequiresPermission(android.Manifest.permission.MODIFY_DAY_NIGHT_MODE) + public @AttentionModeThemeOverlayReturnType int getAttentionModeThemeOverlay() { + if (sGlobals != null) { + try { + return sGlobals.mService.getAttentionModeThemeOverlay(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + return MODE_ATTENTION_THEME_OVERLAY_UNKNOWN; + } + + /** * Sets and persist the night mode for this application. * <p> * The mode can be one of: diff --git a/core/java/android/app/activity_manager.aconfig b/core/java/android/app/activity_manager.aconfig index b303ea64406c..4fc25fd8c699 100644 --- a/core/java/android/app/activity_manager.aconfig +++ b/core/java/android/app/activity_manager.aconfig @@ -13,3 +13,10 @@ flag { description: "API to get importance of UID that's binding to the caller" bug: "292533010" } + +flag { + namespace: "backstage_power" + name: "app_restrictions_api" + description: "API to track and query restrictions applied to apps" + bug: "320150834" +}
\ No newline at end of file diff --git a/core/java/android/app/ambientcontext/AmbientContextEvent.java b/core/java/android/app/ambientcontext/AmbientContextEvent.java index f94987e8495a..5ab7991c6326 100644 --- a/core/java/android/app/ambientcontext/AmbientContextEvent.java +++ b/core/java/android/app/ambientcontext/AmbientContextEvent.java @@ -89,8 +89,11 @@ public final class AmbientContextEvent implements Parcelable { */ public static final String KEY_VENDOR_WEARABLE_EVENT_NAME = "wearable_event_name"; - /** Default value for the rate per minute data field. */ - private static final int RATE_PER_MINUTE_UNKNOWN = -1; + /** + * Default value for {@link #getRatePerMinute}. Indicates that the rate of the event is unknown. + */ + @FlaggedApi(android.app.Flags.FLAG_AMBIENT_HEART_RATE) + public static final int RATE_PER_MINUTE_UNKNOWN = -1; /** @hide */ @IntDef(prefix = { "EVENT_" }, value = { @@ -636,10 +639,10 @@ public final class AmbientContextEvent implements Parcelable { } @DataClass.Generated( - time = 1704895515931L, + time = 1705575046107L, codegenVersion = "1.0.23", sourceFile = "frameworks/base/core/java/android/app/ambientcontext/AmbientContextEvent.java", - inputSignatures = "public static final int EVENT_UNKNOWN\npublic static final int EVENT_COUGH\npublic static final int EVENT_SNORE\npublic static final int EVENT_BACK_DOUBLE_TAP\npublic static final @android.app.ambientcontext.AmbientContextEvent.Event @android.annotation.FlaggedApi int EVENT_HEART_RATE\npublic static final int EVENT_VENDOR_WEARABLE_START\npublic static final java.lang.String KEY_VENDOR_WEARABLE_EVENT_NAME\nprivate static final int RATE_PER_MINUTE_UNKNOWN\npublic static final int LEVEL_UNKNOWN\npublic static final int LEVEL_LOW\npublic static final int LEVEL_MEDIUM_LOW\npublic static final int LEVEL_MEDIUM\npublic static final int LEVEL_MEDIUM_HIGH\npublic static final int LEVEL_HIGH\nprivate final @android.app.ambientcontext.AmbientContextEvent.EventCode int mEventType\nprivate final @com.android.internal.util.DataClass.ParcelWith(com.android.internal.util.Parcelling.BuiltIn.ForInstant.class) @android.annotation.NonNull java.time.Instant mStartTime\nprivate final @com.android.internal.util.DataClass.ParcelWith(com.android.internal.util.Parcelling.BuiltIn.ForInstant.class) @android.annotation.NonNull java.time.Instant mEndTime\nprivate final @android.app.ambientcontext.AmbientContextEvent.LevelValue int mConfidenceLevel\nprivate final @android.app.ambientcontext.AmbientContextEvent.LevelValue int mDensityLevel\nprivate final @android.annotation.NonNull android.os.PersistableBundle mVendorData\nprivate final @android.annotation.IntRange int mRatePerMinute\nprivate static int defaultEventType()\nprivate static @android.annotation.NonNull java.time.Instant defaultStartTime()\nprivate static @android.annotation.NonNull java.time.Instant defaultEndTime()\nprivate static int defaultConfidenceLevel()\nprivate static int defaultDensityLevel()\nprivate static android.os.PersistableBundle defaultVendorData()\nprivate static int defaultRatePerMinute()\nclass AmbientContextEvent extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genBuilder=true, genConstructor=false, genHiddenConstDefs=true, genParcelable=true, genToString=true)") + inputSignatures = "public static final int EVENT_UNKNOWN\npublic static final int EVENT_COUGH\npublic static final int EVENT_SNORE\npublic static final int EVENT_BACK_DOUBLE_TAP\npublic static final @android.app.ambientcontext.AmbientContextEvent.Event @android.annotation.FlaggedApi int EVENT_HEART_RATE\npublic static final int EVENT_VENDOR_WEARABLE_START\npublic static final java.lang.String KEY_VENDOR_WEARABLE_EVENT_NAME\npublic static final @android.annotation.FlaggedApi int RATE_PER_MINUTE_UNKNOWN\npublic static final int LEVEL_UNKNOWN\npublic static final int LEVEL_LOW\npublic static final int LEVEL_MEDIUM_LOW\npublic static final int LEVEL_MEDIUM\npublic static final int LEVEL_MEDIUM_HIGH\npublic static final int LEVEL_HIGH\nprivate final @android.app.ambientcontext.AmbientContextEvent.EventCode int mEventType\nprivate final @com.android.internal.util.DataClass.ParcelWith(com.android.internal.util.Parcelling.BuiltIn.ForInstant.class) @android.annotation.NonNull java.time.Instant mStartTime\nprivate final @com.android.internal.util.DataClass.ParcelWith(com.android.internal.util.Parcelling.BuiltIn.ForInstant.class) @android.annotation.NonNull java.time.Instant mEndTime\nprivate final @android.app.ambientcontext.AmbientContextEvent.LevelValue int mConfidenceLevel\nprivate final @android.app.ambientcontext.AmbientContextEvent.LevelValue int mDensityLevel\nprivate final @android.annotation.NonNull android.os.PersistableBundle mVendorData\nprivate final @android.annotation.IntRange int mRatePerMinute\nprivate static int defaultEventType()\nprivate static @android.annotation.NonNull java.time.Instant defaultStartTime()\nprivate static @android.annotation.NonNull java.time.Instant defaultEndTime()\nprivate static int defaultConfidenceLevel()\nprivate static int defaultDensityLevel()\nprivate static android.os.PersistableBundle defaultVendorData()\nprivate static int defaultRatePerMinute()\nclass AmbientContextEvent extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genBuilder=true, genConstructor=false, genHiddenConstDefs=true, genParcelable=true, genToString=true)") @Deprecated private void __metadata() {} diff --git a/core/java/android/companion/virtual/IVirtualDevice.aidl b/core/java/android/companion/virtual/IVirtualDevice.aidl index 12229b12fb16..6eab363c4eb1 100644 --- a/core/java/android/companion/virtual/IVirtualDevice.aidl +++ b/core/java/android/companion/virtual/IVirtualDevice.aidl @@ -35,6 +35,9 @@ import android.hardware.input.VirtualMouseButtonEvent; import android.hardware.input.VirtualMouseConfig; import android.hardware.input.VirtualMouseRelativeEvent; import android.hardware.input.VirtualMouseScrollEvent; +import android.hardware.input.VirtualStylusButtonEvent; +import android.hardware.input.VirtualStylusConfig; +import android.hardware.input.VirtualStylusMotionEvent; import android.hardware.input.VirtualTouchEvent; import android.hardware.input.VirtualTouchscreenConfig; import android.hardware.input.VirtualNavigationTouchpadConfig; @@ -144,6 +147,12 @@ interface IVirtualDevice { void createVirtualNavigationTouchpad(in VirtualNavigationTouchpadConfig config, IBinder token); /** + * Creates a new stylus and registers it with the input framework with the given token. + */ + @EnforcePermission("CREATE_VIRTUAL_DEVICE") + void createVirtualStylus(in VirtualStylusConfig config, IBinder token); + + /** * Removes the input device corresponding to the given token from the framework. */ @EnforcePermission("CREATE_VIRTUAL_DEVICE") @@ -156,32 +165,32 @@ interface IVirtualDevice { int getInputDeviceId(IBinder token); /** - * Injects a key event to the virtual dpad corresponding to the given token. - */ + * Injects a key event to the virtual dpad corresponding to the given token. + */ @EnforcePermission("CREATE_VIRTUAL_DEVICE") boolean sendDpadKeyEvent(IBinder token, in VirtualKeyEvent event); /** - * Injects a key event to the virtual keyboard corresponding to the given token. - */ + * Injects a key event to the virtual keyboard corresponding to the given token. + */ @EnforcePermission("CREATE_VIRTUAL_DEVICE") boolean sendKeyEvent(IBinder token, in VirtualKeyEvent event); /** - * Injects a button event to the virtual mouse corresponding to the given token. - */ + * Injects a button event to the virtual mouse corresponding to the given token. + */ @EnforcePermission("CREATE_VIRTUAL_DEVICE") boolean sendButtonEvent(IBinder token, in VirtualMouseButtonEvent event); /** - * Injects a relative event to the virtual mouse corresponding to the given token. - */ + * Injects a relative event to the virtual mouse corresponding to the given token. + */ @EnforcePermission("CREATE_VIRTUAL_DEVICE") boolean sendRelativeEvent(IBinder token, in VirtualMouseRelativeEvent event); /** - * Injects a scroll event to the virtual mouse corresponding to the given token. - */ + * Injects a scroll event to the virtual mouse corresponding to the given token. + */ @EnforcePermission("CREATE_VIRTUAL_DEVICE") boolean sendScrollEvent(IBinder token, in VirtualMouseScrollEvent event); @@ -192,6 +201,18 @@ interface IVirtualDevice { boolean sendTouchEvent(IBinder token, in VirtualTouchEvent event); /** + * Injects a motion event from the virtual stylus input device corresponding to the given token. + */ + @EnforcePermission("CREATE_VIRTUAL_DEVICE") + boolean sendStylusMotionEvent(IBinder token, in VirtualStylusMotionEvent event); + + /** + * Injects a button event from the virtual stylus input device corresponding to the given token. + */ + @EnforcePermission("CREATE_VIRTUAL_DEVICE") + boolean sendStylusButtonEvent(IBinder token, in VirtualStylusButtonEvent event); + + /** * Returns all virtual sensors created for this device. */ @EnforcePermission("CREATE_VIRTUAL_DEVICE") diff --git a/core/java/android/companion/virtual/VirtualDeviceInternal.java b/core/java/android/companion/virtual/VirtualDeviceInternal.java index 2abeeeecc1c6..c1e443d1729d 100644 --- a/core/java/android/companion/virtual/VirtualDeviceInternal.java +++ b/core/java/android/companion/virtual/VirtualDeviceInternal.java @@ -42,6 +42,8 @@ import android.hardware.input.VirtualMouse; import android.hardware.input.VirtualMouseConfig; import android.hardware.input.VirtualNavigationTouchpad; import android.hardware.input.VirtualNavigationTouchpadConfig; +import android.hardware.input.VirtualStylus; +import android.hardware.input.VirtualStylusConfig; import android.hardware.input.VirtualTouchscreen; import android.hardware.input.VirtualTouchscreenConfig; import android.media.AudioManager; @@ -316,6 +318,19 @@ public class VirtualDeviceInternal { } } + @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) + @NonNull + VirtualStylus createVirtualStylus(@NonNull VirtualStylusConfig config) { + try { + final IBinder token = new Binder( + "android.hardware.input.VirtualStylus:" + config.getInputDeviceName()); + mVirtualDevice.createVirtualStylus(config, token); + return new VirtualStylus(config, mVirtualDevice, token); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + @NonNull VirtualNavigationTouchpad createVirtualNavigationTouchpad( @NonNull VirtualNavigationTouchpadConfig config) { diff --git a/core/java/android/companion/virtual/VirtualDeviceManager.java b/core/java/android/companion/virtual/VirtualDeviceManager.java index eef60f11fb1c..a4cada28999e 100644 --- a/core/java/android/companion/virtual/VirtualDeviceManager.java +++ b/core/java/android/companion/virtual/VirtualDeviceManager.java @@ -55,6 +55,8 @@ import android.hardware.input.VirtualMouse; import android.hardware.input.VirtualMouseConfig; import android.hardware.input.VirtualNavigationTouchpad; import android.hardware.input.VirtualNavigationTouchpadConfig; +import android.hardware.input.VirtualStylus; +import android.hardware.input.VirtualStylusConfig; import android.hardware.input.VirtualTouchscreen; import android.hardware.input.VirtualTouchscreenConfig; import android.media.AudioManager; @@ -859,6 +861,19 @@ public final class VirtualDeviceManager { } /** + * Creates a virtual stylus. + * + * @param config the touchscreen configurations for the virtual stylus. + */ + @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) + @NonNull + @FlaggedApi(Flags.FLAG_VIRTUAL_STYLUS) + public VirtualStylus createVirtualStylus( + @NonNull VirtualStylusConfig config) { + return mVirtualDeviceInternal.createVirtualStylus(config); + } + + /** * Creates a VirtualAudioDevice, capable of recording audio emanating from this device, * or injecting audio from another device. * @@ -886,11 +901,14 @@ public final class VirtualDeviceManager { } /** - * Creates a new virtual camera. If a virtual camera was already created, it will be closed. + * Creates a new virtual camera with the given {@link VirtualCameraConfig}. A virtual device + * can create a virtual camera only if it has + * {@link VirtualDeviceParams#DEVICE_POLICY_CUSTOM} as its + * {@link VirtualDeviceParams#POLICY_TYPE_CAMERA}. * - * @param config camera config. - * @return newly created camera; - * @hide + * @param config camera configuration. + * @return newly created camera. + * @see VirtualDeviceParams#POLICY_TYPE_CAMERA */ @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) @NonNull diff --git a/core/java/android/companion/virtual/VirtualDeviceParams.java b/core/java/android/companion/virtual/VirtualDeviceParams.java index 0253ddd93a44..f9a9da1ae448 100644 --- a/core/java/android/companion/virtual/VirtualDeviceParams.java +++ b/core/java/android/companion/virtual/VirtualDeviceParams.java @@ -159,7 +159,7 @@ public final class VirtualDeviceParams implements Parcelable { * @hide */ @IntDef(prefix = "POLICY_TYPE_", value = {POLICY_TYPE_SENSORS, POLICY_TYPE_AUDIO, - POLICY_TYPE_RECENTS, POLICY_TYPE_ACTIVITY}) + POLICY_TYPE_RECENTS, POLICY_TYPE_ACTIVITY, POLICY_TYPE_CAMERA}) @Retention(RetentionPolicy.SOURCE) @Target({ElementType.TYPE_PARAMETER, ElementType.TYPE_USE}) public @interface PolicyType {} @@ -246,6 +246,23 @@ public final class VirtualDeviceParams implements Parcelable { @FlaggedApi(Flags.FLAG_CROSS_DEVICE_CLIPBOARD) public static final int POLICY_TYPE_CLIPBOARD = 4; + /** + * Tells the camera framework how to handle camera requests for the front and back cameras from + * contexts associated with this virtual device. + * + * <ul> + * <li>{@link #DEVICE_POLICY_DEFAULT}: Returns the front and back cameras of the default + * device. + * <li>{@link #DEVICE_POLICY_CUSTOM}: Returns the front and back cameras cameras of the + * virtual device. Note that if the virtual device did not create any virtual cameras, + * then no front and back cameras will be available. + * </ul> + * + * @see Context#getDeviceId + */ + @FlaggedApi(Flags.FLAG_VIRTUAL_CAMERA) + public static final int POLICY_TYPE_CAMERA = 5; + private final int mLockState; @NonNull private final ArraySet<UserHandle> mUsersWithMatchingAccounts; @NavigationPolicy @@ -1153,6 +1170,10 @@ public final class VirtualDeviceParams implements Parcelable { mDevicePolicies.delete(POLICY_TYPE_CLIPBOARD); } + if (!Flags.virtualCamera()) { + mDevicePolicies.delete(POLICY_TYPE_CAMERA); + } + if ((mAudioPlaybackSessionId != AUDIO_SESSION_ID_GENERATE || mAudioRecordingSessionId != AUDIO_SESSION_ID_GENERATE) && mDevicePolicies.get(POLICY_TYPE_AUDIO, DEVICE_POLICY_DEFAULT) diff --git a/core/java/android/companion/virtual/camera/IVirtualCameraCallback.aidl b/core/java/android/companion/virtual/camera/IVirtualCameraCallback.aidl index 44942d67d489..f4f69b5cff1a 100644 --- a/core/java/android/companion/virtual/camera/IVirtualCameraCallback.aidl +++ b/core/java/android/companion/virtual/camera/IVirtualCameraCallback.aidl @@ -13,9 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package android.companion.virtual.camera; -import android.companion.virtual.camera.VirtualCameraStreamConfig; import android.view.Surface; /** @@ -30,38 +30,36 @@ interface IVirtualCameraCallback { * Called when one of the requested stream has been configured by the virtual camera service and * is ready to receive data onto its {@link Surface} * - * @param streamId The id of the configured stream - * @param surface The surface to write data into for this stream - * @param streamConfig The image data configuration for this stream + * @param streamId The id of the configured stream + * @param surface The surface to write data into for this stream + * @param width The width of the surface + * @param height The height of the surface + * @param format The pixel format of the surface */ - oneway void onStreamConfigured( - int streamId, - in Surface surface, - in VirtualCameraStreamConfig streamConfig); + oneway void onStreamConfigured(int streamId, in Surface surface, int width, int height, + int format); /** * The client application is requesting a camera frame for the given streamId and frameId. * * <p>The virtual camera needs to write the frame data in the {@link Surface} corresponding to - * this stream that was provided during the {@link #onStreamConfigured(int, Surface, - * VirtualCameraStreamConfig)} call. + * this stream that was provided during the + * {@link #onStreamConfigured(int, Surface, int, int, int)} call. * * @param streamId The streamId for which the frame is requested. This corresponds to the - * streamId that was given in {@link #onStreamConfigured(int, Surface, - * VirtualCameraStreamConfig)} + * streamId that was given in {@link #onStreamConfigured(int, Surface, int, int, int)} * @param frameId The frameId that is being requested. Each request will have a different * frameId, that will be increasing for each call with a particular streamId. */ oneway void onProcessCaptureRequest(int streamId, long frameId); /** - * The stream previously configured when {@link #onStreamConfigured(int, Surface, - * VirtualCameraStreamConfig)} was called is now being closed and associated resources can be - * freed. The Surface was disposed on the client side and should not be used anymore by the - * virtual camera owner. + * The stream previously configured when + * {@link #onStreamConfigured(int, Surface, int, int, int)} was called is now being closed and + * associated resources can be freed. The Surface was disposed on the client side and should not + * be used anymore by the virtual camera owner. * * @param streamId The id of the stream that was closed. */ oneway void onStreamClosed(int streamId); - }
\ No newline at end of file diff --git a/core/java/android/companion/virtual/camera/VirtualCameraCallback.java b/core/java/android/companion/virtual/camera/VirtualCameraCallback.java index 5b658b883217..c894de428b10 100644 --- a/core/java/android/companion/virtual/camera/VirtualCameraCallback.java +++ b/core/java/android/companion/virtual/camera/VirtualCameraCallback.java @@ -17,9 +17,11 @@ package android.companion.virtual.camera; import android.annotation.FlaggedApi; +import android.annotation.IntRange; import android.annotation.NonNull; import android.annotation.SystemApi; import android.companion.virtual.flags.Flags; +import android.graphics.ImageFormat; import android.view.Surface; import java.util.concurrent.Executor; @@ -41,33 +43,33 @@ public interface VirtualCameraCallback { * * @param streamId The id of the configured stream * @param surface The surface to write data into for this stream - * @param streamConfig The image data configuration for this stream + * @param width The width of the surface + * @param height The height of the surface + * @param format The {@link ImageFormat} of the surface */ - void onStreamConfigured( - int streamId, - @NonNull Surface surface, - @NonNull VirtualCameraStreamConfig streamConfig); + void onStreamConfigured(int streamId, @NonNull Surface surface, + @IntRange(from = 1) int width, @IntRange(from = 1) int height, + @ImageFormat.Format int format); /** * The client application is requesting a camera frame for the given streamId and frameId. * * <p>The virtual camera needs to write the frame data in the {@link Surface} corresponding to - * this stream that was provided during the {@link #onStreamConfigured(int, Surface, - * VirtualCameraStreamConfig)} call. + * this stream that was provided during the + * {@link #onStreamConfigured(int, Surface, int, int, int)} call. * * @param streamId The streamId for which the frame is requested. This corresponds to the - * streamId that was given in {@link #onStreamConfigured(int, Surface, - * VirtualCameraStreamConfig)} + * streamId that was given in {@link #onStreamConfigured(int, Surface, int, int, int)} * @param frameId The frameId that is being requested. Each request will have a different * frameId, that will be increasing for each call with a particular streamId. */ default void onProcessCaptureRequest(int streamId, long frameId) {} /** - * The stream previously configured when {@link #onStreamConfigured(int, Surface, - * VirtualCameraStreamConfig)} was called is now being closed and associated resources can be - * freed. The Surface corresponding to that streamId was disposed on the client side and should - * not be used anymore by the virtual camera owner + * The stream previously configured when + * {@link #onStreamConfigured(int, Surface, int, int, int)} was called is now being closed and + * associated resources can be freed. The Surface corresponding to that streamId was disposed on + * the client side and should not be used anymore by the virtual camera owner. * * @param streamId The id of the stream that was closed. */ diff --git a/core/java/android/companion/virtual/camera/VirtualCameraConfig.aidl b/core/java/android/companion/virtual/camera/VirtualCameraConfig.aidl index 88c27a52bb9d..5ceb4d12f0c0 100644 --- a/core/java/android/companion/virtual/camera/VirtualCameraConfig.aidl +++ b/core/java/android/companion/virtual/camera/VirtualCameraConfig.aidl @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 The Android Open Source Project + * Copyright 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. @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package android.companion.virtual.camera; /** @hide */ diff --git a/core/java/android/companion/virtual/camera/VirtualCameraConfig.java b/core/java/android/companion/virtual/camera/VirtualCameraConfig.java index 59fe9a1c127b..350cf3d832d6 100644 --- a/core/java/android/companion/virtual/camera/VirtualCameraConfig.java +++ b/core/java/android/companion/virtual/camera/VirtualCameraConfig.java @@ -19,16 +19,23 @@ package android.companion.virtual.camera; import static java.util.Objects.requireNonNull; import android.annotation.FlaggedApi; +import android.annotation.IntDef; +import android.annotation.IntRange; import android.annotation.NonNull; import android.annotation.SuppressLint; import android.annotation.SystemApi; +import android.companion.virtual.VirtualDevice; import android.companion.virtual.flags.Flags; import android.graphics.ImageFormat; +import android.graphics.PixelFormat; +import android.hardware.camera2.CameraMetadata; import android.os.Parcel; import android.os.Parcelable; import android.util.ArraySet; import android.view.Surface; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.util.Set; import java.util.concurrent.Executor; @@ -43,16 +50,57 @@ import java.util.concurrent.Executor; @FlaggedApi(Flags.FLAG_VIRTUAL_CAMERA) public final class VirtualCameraConfig implements Parcelable { + private static final int LENS_FACING_UNKNOWN = -1; + + /** + * Sensor orientation of {@code 0} degrees. + * @see #getSensorOrientation + */ + public static final int SENSOR_ORIENTATION_0 = 0; + /** + * Sensor orientation of {@code 90} degrees. + * @see #getSensorOrientation + */ + public static final int SENSOR_ORIENTATION_90 = 90; + /** + * Sensor orientation of {@code 180} degrees. + * @see #getSensorOrientation + */ + public static final int SENSOR_ORIENTATION_180 = 180; + /** + * Sensor orientation of {@code 270} degrees. + * @see #getSensorOrientation + */ + public static final int SENSOR_ORIENTATION_270 = 270; + /** @hide */ + @IntDef(prefix = {"SENSOR_ORIENTATION_"}, value = { + SENSOR_ORIENTATION_0, + SENSOR_ORIENTATION_90, + SENSOR_ORIENTATION_180, + SENSOR_ORIENTATION_270 + }) + @Retention(RetentionPolicy.SOURCE) + public @interface SensorOrientation {} + private final String mName; private final Set<VirtualCameraStreamConfig> mStreamConfigurations; private final IVirtualCameraCallback mCallback; + @SensorOrientation + private final int mSensorOrientation; + private final int mLensFacing; private VirtualCameraConfig( @NonNull String name, @NonNull Set<VirtualCameraStreamConfig> streamConfigurations, @NonNull Executor executor, - @NonNull VirtualCameraCallback callback) { + @NonNull VirtualCameraCallback callback, + @SensorOrientation int sensorOrientation, + int lensFacing) { mName = requireNonNull(name, "Missing name"); + if (lensFacing == LENS_FACING_UNKNOWN) { + throw new IllegalArgumentException("Lens facing must be set"); + } + mLensFacing = lensFacing; mStreamConfigurations = Set.copyOf(requireNonNull(streamConfigurations, "Missing stream configurations")); if (mStreamConfigurations.isEmpty()) { @@ -63,6 +111,7 @@ public final class VirtualCameraConfig implements Parcelable { new VirtualCameraCallbackInternal( requireNonNull(callback, "Missing callback"), requireNonNull(executor, "Missing callback executor")); + mSensorOrientation = sensorOrientation; } private VirtualCameraConfig(@NonNull Parcel in) { @@ -73,6 +122,8 @@ public final class VirtualCameraConfig implements Parcelable { in.readParcelableArray( VirtualCameraStreamConfig.class.getClassLoader(), VirtualCameraStreamConfig.class)); + mSensorOrientation = in.readInt(); + mLensFacing = in.readInt(); } @Override @@ -86,6 +137,8 @@ public final class VirtualCameraConfig implements Parcelable { dest.writeStrongInterface(mCallback); dest.writeParcelableArray( mStreamConfigurations.toArray(new VirtualCameraStreamConfig[0]), flags); + dest.writeInt(mSensorOrientation); + dest.writeInt(mLensFacing); } /** @@ -100,7 +153,7 @@ public final class VirtualCameraConfig implements Parcelable { * Returns an unmodifiable set of the stream configurations added to this {@link * VirtualCameraConfig}. * - * @see VirtualCameraConfig.Builder#addStreamConfig(int, int, int) + * @see VirtualCameraConfig.Builder#addStreamConfig(int, int, int, int) */ @NonNull public Set<VirtualCameraStreamConfig> getStreamConfigs() { @@ -118,13 +171,33 @@ public final class VirtualCameraConfig implements Parcelable { } /** + * Returns the sensor orientation of this stream, which represents the clockwise angle (in + * degrees) through which the output image needs to be rotated to be upright on the device + * screen in its native orientation. Returns {@link #SENSOR_ORIENTATION_0} if omitted. + */ + @SensorOrientation + public int getSensorOrientation() { + return mSensorOrientation; + } + + /** + * Returns the direction that the virtual camera faces relative to the virtual device's screen. + * + * @see Builder#setLensFacing(int) + */ + public int getLensFacing() { + return mLensFacing; + } + + /** * Builder for {@link VirtualCameraConfig}. * * <p>To build an instance of {@link VirtualCameraConfig} the following conditions must be met: - * <li>At least one stream must be added with {@link #addStreamConfig(int, int, int)}. + * <li>At least one stream must be added with {@link #addStreamConfig(int, int, int, int)}. * <li>A callback must be set with {@link #setVirtualCameraCallback(Executor, * VirtualCameraCallback)} * <li>A camera name must be set with {@link #setName(String)} + * <li>A lens facing must be set with {@link #setLensFacing(int)} */ @FlaggedApi(Flags.FLAG_VIRTUAL_CAMERA) public static final class Builder { @@ -133,9 +206,11 @@ public final class VirtualCameraConfig implements Parcelable { private final ArraySet<VirtualCameraStreamConfig> mStreamConfigurations = new ArraySet<>(); private Executor mCallbackExecutor; private VirtualCameraCallback mCallback; + private int mSensorOrientation = SENSOR_ORIENTATION_0; + private int mLensFacing = LENS_FACING_UNKNOWN; /** - * Set the name of the virtual camera instance. + * Sets the name of the virtual camera instance. */ @NonNull public Builder setName(@NonNull String name) { @@ -144,25 +219,94 @@ public final class VirtualCameraConfig implements Parcelable { } /** - * Add an available stream configuration fot this {@link VirtualCamera}. + * Adds a supported input stream configuration for this {@link VirtualCamera}. * * <p>At least one {@link VirtualCameraStreamConfig} must be added. * * @param width The width of the stream. * @param height The height of the stream. - * @param format The {@link ImageFormat} of the stream. + * @param format The input format of the stream. Supported formats are + * {@link ImageFormat#YUV_420_888} and {@link PixelFormat#RGBA_8888}. + * @param maximumFramesPerSecond The maximum frame rate (in frames per second) for the + * stream. * - * @throws IllegalArgumentException if invalid format or dimensions are passed. + * @throws IllegalArgumentException if invalid dimensions, format or frame rate are passed. */ @NonNull - public Builder addStreamConfig(int width, int height, @ImageFormat.Format int format) { - if (width <= 0 || height <= 0) { - throw new IllegalArgumentException("Invalid dimensions passed for stream config"); + public Builder addStreamConfig( + @IntRange(from = 1) int width, + @IntRange(from = 1) int height, + @ImageFormat.Format int format, + @IntRange(from = 1) int maximumFramesPerSecond) { + // TODO(b/310857519): Check dimension upper limits based on the maximum texture size + // supported by the current device, instead of hardcoded limits. + if (width <= 0 || width > VirtualCameraStreamConfig.DIMENSION_UPPER_LIMIT) { + throw new IllegalArgumentException( + "Invalid width passed for stream config: " + width + + ", must be between 1 and " + + VirtualCameraStreamConfig.DIMENSION_UPPER_LIMIT); + } + if (height <= 0 || height > VirtualCameraStreamConfig.DIMENSION_UPPER_LIMIT) { + throw new IllegalArgumentException( + "Invalid height passed for stream config: " + height + + ", must be between 1 and " + + VirtualCameraStreamConfig.DIMENSION_UPPER_LIMIT); } - if (!ImageFormat.isPublicFormat(format)) { - throw new IllegalArgumentException("Invalid format passed for stream config"); + if (!isFormatSupported(format)) { + throw new IllegalArgumentException( + "Invalid format passed for stream config: " + format); } - mStreamConfigurations.add(new VirtualCameraStreamConfig(width, height, format)); + if (maximumFramesPerSecond <= 0 + || maximumFramesPerSecond > VirtualCameraStreamConfig.MAX_FPS_UPPER_LIMIT) { + throw new IllegalArgumentException( + "Invalid maximumFramesPerSecond, must be greater than 0 and less than " + + VirtualCameraStreamConfig.MAX_FPS_UPPER_LIMIT); + } + mStreamConfigurations.add(new VirtualCameraStreamConfig(width, height, format, + maximumFramesPerSecond)); + return this; + } + + /** + * Sets the sensor orientation of the virtual camera. This field is optional and can be + * omitted (defaults to {@link #SENSOR_ORIENTATION_0}). + * + * @param sensorOrientation The sensor orientation of the camera, which represents the + * clockwise angle (in degrees) through which the output image + * needs to be rotated to be upright on the device screen in its + * native orientation. + */ + @NonNull + public Builder setSensorOrientation(@SensorOrientation int sensorOrientation) { + if (sensorOrientation != SENSOR_ORIENTATION_0 + && sensorOrientation != SENSOR_ORIENTATION_90 + && sensorOrientation != SENSOR_ORIENTATION_180 + && sensorOrientation != SENSOR_ORIENTATION_270) { + throw new IllegalArgumentException( + "Invalid sensor orientation: " + sensorOrientation); + } + mSensorOrientation = sensorOrientation; + return this; + } + + /** + * Sets the lens facing direction of the virtual camera, can be either + * {@link CameraMetadata#LENS_FACING_FRONT} or {@link CameraMetadata#LENS_FACING_BACK}. + * + * <p>A {@link VirtualDevice} can have at most one {@link VirtualCamera} with + * {@link CameraMetadata#LENS_FACING_FRONT} and at most one {@link VirtualCamera} with + * {@link CameraMetadata#LENS_FACING_BACK}. + * + * @param lensFacing The direction that the virtual camera faces relative to the device's + * screen. + */ + @NonNull + public Builder setLensFacing(int lensFacing) { + if (lensFacing != CameraMetadata.LENS_FACING_BACK + && lensFacing != CameraMetadata.LENS_FACING_FRONT) { + throw new IllegalArgumentException("Unsupported lens facing: " + lensFacing); + } + mLensFacing = lensFacing; return this; } @@ -189,11 +333,13 @@ public final class VirtualCameraConfig implements Parcelable { * Builds a new instance of {@link VirtualCameraConfig} * * @throws NullPointerException if some required parameters are missing. + * @throws IllegalArgumentException if any parameter is invalid. */ @NonNull public VirtualCameraConfig build() { return new VirtualCameraConfig( - mName, mStreamConfigurations, mCallbackExecutor, mCallback); + mName, mStreamConfigurations, mCallbackExecutor, mCallback, mSensorOrientation, + mLensFacing); } } @@ -208,9 +354,10 @@ public final class VirtualCameraConfig implements Parcelable { } @Override - public void onStreamConfigured( - int streamId, Surface surface, VirtualCameraStreamConfig streamConfig) { - mExecutor.execute(() -> mCallback.onStreamConfigured(streamId, surface, streamConfig)); + public void onStreamConfigured(int streamId, Surface surface, int width, int height, + int format) { + mExecutor.execute(() -> mCallback.onStreamConfigured(streamId, surface, width, height, + format)); } @Override @@ -237,4 +384,11 @@ public final class VirtualCameraConfig implements Parcelable { return new VirtualCameraConfig[size]; } }; + + private static boolean isFormatSupported(@ImageFormat.Format int format) { + return switch (format) { + case ImageFormat.YUV_420_888, PixelFormat.RGBA_8888 -> true; + default -> false; + }; + } } diff --git a/core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.java b/core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.java index 1272f1683230..00a814e7a02e 100644 --- a/core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.java +++ b/core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 The Android Open Source Project + * Copyright 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. @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package android.companion.virtual.camera; import android.annotation.FlaggedApi; @@ -24,6 +25,8 @@ import android.graphics.ImageFormat; import android.os.Parcel; import android.os.Parcelable; +import com.android.internal.annotations.VisibleForTesting; + import java.util.Objects; /** @@ -34,32 +37,47 @@ import java.util.Objects; @SystemApi @FlaggedApi(Flags.FLAG_VIRTUAL_CAMERA) public final class VirtualCameraStreamConfig implements Parcelable { + // TODO(b/310857519): Check if we should increase the fps upper limit in future. + static final int MAX_FPS_UPPER_LIMIT = 60; + // This is the minimum guaranteed upper bound of texture size supported by all devices. + // Keep this in sync with kMaxTextureSize from services/camera/virtualcamera/util/Util.cc + // TODO(b/310857519): Remove this once we add support for fetching the maximum texture size + // supported by the current device. + static final int DIMENSION_UPPER_LIMIT = 2048; private final int mWidth; private final int mHeight; private final int mFormat; + private final int mMaxFps; /** * Construct a new instance of {@link VirtualCameraStreamConfig} initialized with the provided - * width, height and {@link ImageFormat} + * width, height and {@link ImageFormat}. * * @param width The width of the stream. * @param height The height of the stream. * @param format The {@link ImageFormat} of the stream. + * @param maxFps The maximum frame rate (in frames per second) for the stream. + * + * @hide */ + @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) public VirtualCameraStreamConfig( @IntRange(from = 1) int width, @IntRange(from = 1) int height, - @ImageFormat.Format int format) { + @ImageFormat.Format int format, + @IntRange(from = 1) int maxFps) { this.mWidth = width; this.mHeight = height; this.mFormat = format; + this.mMaxFps = maxFps; } private VirtualCameraStreamConfig(@NonNull Parcel in) { mWidth = in.readInt(); mHeight = in.readInt(); mFormat = in.readInt(); + mMaxFps = in.readInt(); } @Override @@ -72,6 +90,7 @@ public final class VirtualCameraStreamConfig implements Parcelable { dest.writeInt(mWidth); dest.writeInt(mHeight); dest.writeInt(mFormat); + dest.writeInt(mMaxFps); } @NonNull @@ -105,12 +124,13 @@ public final class VirtualCameraStreamConfig implements Parcelable { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; VirtualCameraStreamConfig that = (VirtualCameraStreamConfig) o; - return mWidth == that.mWidth && mHeight == that.mHeight && mFormat == that.mFormat; + return mWidth == that.mWidth && mHeight == that.mHeight && mFormat == that.mFormat + && mMaxFps == that.mMaxFps; } @Override public int hashCode() { - return Objects.hash(mWidth, mHeight, mFormat); + return Objects.hash(mWidth, mHeight, mFormat, mMaxFps); } /** Returns the {@link ImageFormat} of this stream. */ @@ -118,4 +138,10 @@ public final class VirtualCameraStreamConfig implements Parcelable { public int getFormat() { return mFormat; } + + /** Returns the maximum frame rate (in frames per second) of this stream. */ + @IntRange(from = 1) + public int getMaximumFramesPerSecond() { + return mMaxFps; + } } diff --git a/core/java/android/companion/virtual/flags.aconfig b/core/java/android/companion/virtual/flags.aconfig index ce2490b8efb8..588e4fce1f3d 100644 --- a/core/java/android/companion/virtual/flags.aconfig +++ b/core/java/android/companion/virtual/flags.aconfig @@ -100,3 +100,10 @@ flag { description: "Enable interactive screen mirroring using Virtual Devices" bug: "292212199" } + +flag { + name: "virtual_stylus" + namespace: "virtual_devices" + description: "Enable virtual stylus input" + bug: "304829446" +} diff --git a/core/java/android/content/AttributionSource.java b/core/java/android/content/AttributionSource.java index a1357c91b2cf..bde562dbf95b 100644 --- a/core/java/android/content/AttributionSource.java +++ b/core/java/android/content/AttributionSource.java @@ -231,7 +231,7 @@ public final class AttributionSource implements Parcelable { } /** @hide */ - public AttributionSource withToken(@NonNull Binder token) { + public AttributionSource withToken(@NonNull IBinder token) { return new AttributionSource(getUid(), getPid(), getPackageName(), getAttributionTag(), token, mAttributionSourceState.renouncedPermissions, getDeviceId(), getNext()); } diff --git a/core/java/android/content/ContentProvider.java b/core/java/android/content/ContentProvider.java index e9b94c9f5791..c7a75ed5ea9c 100644 --- a/core/java/android/content/ContentProvider.java +++ b/core/java/android/content/ContentProvider.java @@ -41,7 +41,6 @@ import android.content.res.Configuration; 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; @@ -147,7 +146,6 @@ public abstract class ContentProvider implements ContentInterface, ComponentCall private boolean mExported; private boolean mNoPerms; private boolean mSingleUser; - private boolean mSystemUserOnly; private SparseBooleanArray mUsersRedirectedToOwnerForMedia = new SparseBooleanArray(); private ThreadLocal<AttributionSource> mCallingAttributionSource; @@ -379,9 +377,7 @@ public abstract class ContentProvider implements ContentInterface, ComponentCall != PermissionChecker.PERMISSION_GRANTED && getContext().checkUriPermission(userUri, Binder.getCallingPid(), callingUid, Intent.FLAG_GRANT_READ_URI_PERMISSION) - != PackageManager.PERMISSION_GRANTED - && !deniedAccessSystemUserOnlyProvider(callingUserId, - mSystemUserOnly)) { + != PackageManager.PERMISSION_GRANTED) { FrameworkStatsLog.write(GET_TYPE_ACCESSED_WITHOUT_PERMISSION, enumCheckUriPermission, callingUid, uri.getAuthority(), type); @@ -869,10 +865,6 @@ public abstract class ContentProvider implements ContentInterface, ComponentCall 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; } @@ -995,9 +987,6 @@ public abstract class ContentProvider implements ContentInterface, ComponentCall // 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) @@ -2634,7 +2623,6 @@ public abstract class ContentProvider implements ContentInterface, ComponentCall 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) { @@ -2768,11 +2756,6 @@ public abstract class ContentProvider implements ContentInterface, ComponentCall 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 @@ -2946,16 +2929,4 @@ public abstract class ContentProvider implements ContentInterface, ComponentCall 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/Context.java b/core/java/android/content/Context.java index fa76e3976a58..249c0e434e78 100644 --- a/core/java/android/content/Context.java +++ b/core/java/android/content/Context.java @@ -16,6 +16,8 @@ package android.content; +import static android.content.flags.Flags.FLAG_ENABLE_BIND_PACKAGE_ISOLATED_PROCESS; + import android.annotation.AttrRes; import android.annotation.CallbackExecutor; import android.annotation.CheckResult; @@ -296,6 +298,7 @@ public abstract class Context { BIND_ALLOW_ACTIVITY_STARTS, BIND_INCLUDE_CAPABILITIES, BIND_SHARED_ISOLATED_PROCESS, + BIND_PACKAGE_ISOLATED_PROCESS, BIND_EXTERNAL_SERVICE }) @Retention(RetentionPolicy.SOURCE) @@ -318,6 +321,7 @@ public abstract class Context { BIND_ALLOW_ACTIVITY_STARTS, BIND_INCLUDE_CAPABILITIES, BIND_SHARED_ISOLATED_PROCESS, + BIND_PACKAGE_ISOLATED_PROCESS, // Intentionally not include BIND_EXTERNAL_SERVICE, because it'd cause sign-extension. // This would allow Android Studio to show a warning, if someone tries to use // BIND_EXTERNAL_SERVICE BindServiceFlags. @@ -511,6 +515,26 @@ public abstract class Context { */ public static final int BIND_SHARED_ISOLATED_PROCESS = 0x00002000; + /** + * Flag for {@link #bindIsolatedService}: Bind the service into a shared isolated process, + * but only with other isolated services from the same package that declare the same process + * name. + * + * <p>Specifying this flag allows multiple isolated services defined in the same package to be + * running in a single shared isolated process. This shared isolated process must be specified + * since this flag will not work with the default application process. + * + * <p>This flag is different from {@link #BIND_SHARED_ISOLATED_PROCESS} since it only + * allows binding services from the same package in the same shared isolated process. This also + * means the shared package isolated process is global, and not scoped to each potential + * calling app. + * + * <p>The shared isolated process instance is identified by the "android:process" attribute + * defined by the service. This flag cannot be used without this attribute set. + */ + @FlaggedApi(FLAG_ENABLE_BIND_PACKAGE_ISOLATED_PROCESS) + public static final int BIND_PACKAGE_ISOLATED_PROCESS = 1 << 14; + /*********** Public flags above this line ***********/ /*********** Hidden flags below this line ***********/ @@ -4218,6 +4242,7 @@ public abstract class Context { VIRTUALIZATION_SERVICE, GRAMMATICAL_INFLECTION_SERVICE, SECURITY_STATE_SERVICE, + //@hide: ECM_ENHANCED_CONFIRMATION_SERVICE, }) @Retention(RetentionPolicy.SOURCE) @@ -6503,6 +6528,18 @@ public abstract class Context { public static final String SECURITY_STATE_SERVICE = "security_state"; /** + * Use with {@link #getSystemService(String)} to retrieve an + * {@link android.app.ecm.EnhancedConfirmationManager}. + * + * @see #getSystemService(String) + * @see android.app.ecm.EnhancedConfirmationManager + * @hide + */ + @FlaggedApi(android.permission.flags.Flags.FLAG_ENHANCED_CONFIRMATION_MODE_APIS_ENABLED) + @SystemApi + public static final String ECM_ENHANCED_CONFIRMATION_SERVICE = "ecm_enhanced_confirmation"; + + /** * Determine whether the given permission is allowed for a particular * process and user ID running in the system. * @@ -6734,7 +6771,7 @@ public abstract class Context { @Intent.AccessUriMode int modeFlags); /** - * Determine whether a particular process and user ID has been granted + * Determine whether a particular process and uid has been granted * permission to access a specific URI. This only checks for permissions * that have been explicitly granted -- if the given process/uid has * more general access to the URI's content provider then this check will @@ -6758,7 +6795,38 @@ public abstract class Context { @Intent.AccessUriMode int modeFlags); /** - * Determine whether a particular process and user ID has been granted + * Determine whether a particular process and uid has been granted + * permission to access a specific content URI. + * + * <p>Unlike {@link #checkUriPermission(Uri, int, int, int)}, this method + * checks for general access to the URI's content provider, as well as + * explicitly granted permissions.</p> + * + * <p>Note, this check will throw an {@link IllegalArgumentException} + * for non-content URIs.</p> + * + * @param uri The content uri that is being checked. + * @param pid (Optional) The process ID being checked against. If the + * pid is unknown, pass -1. + * @param uid The UID being checked against. A uid of 0 is the root + * user, which will pass every permission check. + * @param modeFlags The access modes to check. + * + * @return {@link PackageManager#PERMISSION_GRANTED} if the given + * pid/uid is allowed to access that uri, or + * {@link PackageManager#PERMISSION_DENIED} if it is not. + * + * @see #checkUriPermission(Uri, int, int, int) + */ + @FlaggedApi(android.security.Flags.FLAG_CONTENT_URI_PERMISSION_APIS) + @PackageManager.PermissionResult + public int checkContentUriPermissionFull(@NonNull Uri uri, int pid, int uid, + @Intent.AccessUriMode int modeFlags) { + throw new RuntimeException("Not implemented. Must override in a subclass."); + } + + /** + * Determine whether a particular process and uid has been granted * permission to access a list of URIs. This only checks for permissions * that have been explicitly granted -- if the given process/uid has * more general access to the URI's content provider then this check will @@ -6794,7 +6862,7 @@ public abstract class Context { @Intent.AccessUriMode int modeFlags, IBinder callerToken); /** - * Determine whether the calling process and user ID has been + * Determine whether the calling process and uid has been * granted permission to access a specific URI. This is basically * the same as calling {@link #checkUriPermission(Uri, int, int, * int)} with the pid and uid returned by {@link @@ -6817,7 +6885,7 @@ public abstract class Context { public abstract int checkCallingUriPermission(Uri uri, @Intent.AccessUriMode int modeFlags); /** - * Determine whether the calling process and user ID has been + * Determine whether the calling process and uid has been * granted permission to access a list of URIs. This is basically * the same as calling {@link #checkUriPermissions(List, int, int, int)} * with the pid and uid returned by {@link @@ -6911,7 +6979,7 @@ public abstract class Context { @Intent.AccessUriMode int modeFlags); /** - * If a particular process and user ID has not been granted + * If a particular process and uid has not been granted * permission to access a specific URI, throw {@link * SecurityException}. This only checks for permissions that have * been explicitly granted -- if the given process/uid has more @@ -6931,7 +6999,7 @@ public abstract class Context { Uri uri, int pid, int uid, @Intent.AccessUriMode int modeFlags, String message); /** - * If the calling process and user ID has not been granted + * If the calling process and uid has not been granted * permission to access a specific URI, throw {@link * SecurityException}. This is basically the same as calling * {@link #enforceUriPermission(Uri, int, int, int, String)} with diff --git a/core/java/android/content/ContextWrapper.java b/core/java/android/content/ContextWrapper.java index 0a8029c44d73..e0cf0a5f8178 100644 --- a/core/java/android/content/ContextWrapper.java +++ b/core/java/android/content/ContextWrapper.java @@ -17,6 +17,7 @@ package android.content; import android.annotation.CallbackExecutor; +import android.annotation.FlaggedApi; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.RequiresPermission; @@ -1003,6 +1004,12 @@ public class ContextWrapper extends Context { return mBase.checkUriPermission(uri, pid, uid, modeFlags); } + @FlaggedApi(android.security.Flags.FLAG_CONTENT_URI_PERMISSION_APIS) + @Override + public int checkContentUriPermissionFull(@NonNull Uri uri, int pid, int uid, int modeFlags) { + return mBase.checkContentUriPermissionFull(uri, pid, uid, modeFlags); + } + @NonNull @Override public int[] checkUriPermissions(@NonNull List<Uri> uris, int pid, int uid, diff --git a/core/java/android/content/flags/flags.aconfig b/core/java/android/content/flags/flags.aconfig new file mode 100644 index 000000000000..3445fb53d307 --- /dev/null +++ b/core/java/android/content/flags/flags.aconfig @@ -0,0 +1,8 @@ +package: "android.content.flags" + +flag { + name: "enable_bind_package_isolated_process" + namespace: "machine_learning" + description: "This flag enables the newly added flag for binding package-private isolated processes." + bug: "312706530" +}
\ No newline at end of file diff --git a/core/java/android/content/pm/IPackageInstaller.aidl b/core/java/android/content/pm/IPackageInstaller.aidl index 32ecb58ce241..1f25fd039dd8 100644 --- a/core/java/android/content/pm/IPackageInstaller.aidl +++ b/core/java/android/content/pm/IPackageInstaller.aidl @@ -80,7 +80,7 @@ interface IPackageInstaller { long timeout); @JavaPassthrough(annotation="@android.annotation.RequiresPermission(anyOf={android.Manifest.permission.DELETE_PACKAGES,android.Manifest.permission.REQUEST_DELETE_PACKAGES})") - void requestArchive(String packageName, String callerPackageName, in IntentSender statusReceiver, in UserHandle userHandle, int flags); + void requestArchive(String packageName, String callerPackageName, in IntentSender statusReceiver, in UserHandle userHandle); @JavaPassthrough(annotation="@android.annotation.RequiresPermission(anyOf={android.Manifest.permission.INSTALL_PACKAGES,android.Manifest.permission.REQUEST_INSTALL_PACKAGES})") void requestUnarchive(String packageName, String callerPackageName, in IntentSender statusReceiver, in UserHandle userHandle); diff --git a/core/java/android/content/pm/PackageInstaller.java b/core/java/android/content/pm/PackageInstaller.java index 433226413917..f0efed97d8ed 100644 --- a/core/java/android/content/pm/PackageInstaller.java +++ b/core/java/android/content/pm/PackageInstaller.java @@ -2352,7 +2352,6 @@ public class PackageInstaller { * communicated. * * @param statusReceiver Callback used to notify when the operation is completed. - * @param flags Flags for archiving. Can be 0 or {@link PackageManager#DELETE_SHOW_DIALOG}. * @throws PackageManager.NameNotFoundException If {@code packageName} isn't found or not * available to the caller or isn't archived. */ @@ -2360,12 +2359,11 @@ public class PackageInstaller { Manifest.permission.DELETE_PACKAGES, Manifest.permission.REQUEST_DELETE_PACKAGES}) @FlaggedApi(Flags.FLAG_ARCHIVING) - public void requestArchive(@NonNull String packageName, @NonNull IntentSender statusReceiver, - @DeleteFlags int flags) + public void requestArchive(@NonNull String packageName, @NonNull IntentSender statusReceiver) throws PackageManager.NameNotFoundException { try { mInstaller.requestArchive(packageName, mInstallerPackageName, statusReceiver, - new UserHandle(mUserId), flags); + new UserHandle(mUserId)); } catch (ParcelableException e) { e.maybeRethrow(PackageManager.NameNotFoundException.class); } catch (RemoteException e) { diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java index 8e5e8250c85d..aabbe698703c 100644 --- a/core/java/android/content/pm/PackageManager.java +++ b/core/java/android/content/pm/PackageManager.java @@ -2596,7 +2596,6 @@ public abstract class PackageManager { DELETE_SYSTEM_APP, DELETE_DONT_KILL_APP, DELETE_CHATTY, - DELETE_SHOW_DIALOG, }) @Retention(RetentionPolicy.SOURCE) public @interface DeleteFlags {} @@ -2649,12 +2648,6 @@ public abstract class PackageManager { public static final int DELETE_ARCHIVE = 0x00000010; /** - * Show a confirmation dialog to the user when app is being deleted. - */ - @FlaggedApi(android.content.pm.Flags.FLAG_ARCHIVING) - public static final int DELETE_SHOW_DIALOG = 0x00000020; - - /** * Flag parameter for {@link #deletePackage} to indicate that package deletion * should be chatty. * diff --git a/core/java/android/content/pm/ProviderInfo.java b/core/java/android/content/pm/ProviderInfo.java index de33fa8b2328..9e553dbfb719 100644 --- a/core/java/android/content/pm/ProviderInfo.java +++ b/core/java/android/content/pm/ProviderInfo.java @@ -89,15 +89,6 @@ public final class ProviderInfo extends ComponentInfo 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 2b378b1f09d0..ae46c027505e 100644 --- a/core/java/android/content/pm/ServiceInfo.java +++ b/core/java/android/content/pm/ServiceInfo.java @@ -101,14 +101,6 @@ public class ServiceInfo extends ComponentInfo 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/UserProperties.java b/core/java/android/content/pm/UserProperties.java index 57749d43eb37..269c6c27821c 100644 --- a/core/java/android/content/pm/UserProperties.java +++ b/core/java/android/content/pm/UserProperties.java @@ -123,6 +123,7 @@ public final class UserProperties implements Parcelable { * @hide */ @IntDef(prefix = "SHOW_IN_LAUNCHER_", value = { + SHOW_IN_LAUNCHER_UNKNOWN, SHOW_IN_LAUNCHER_WITH_PARENT, SHOW_IN_LAUNCHER_SEPARATE, SHOW_IN_LAUNCHER_NO, @@ -131,6 +132,13 @@ public final class UserProperties implements Parcelable { public @interface ShowInLauncher { } /** + * Indicates that the show in launcher value for this profile is unknown or unsupported. + * @hide + */ + @TestApi + @SuppressLint("UnflaggedApi") // b/306636213 + public static final int SHOW_IN_LAUNCHER_UNKNOWN = -1; + /** * Suggests that the launcher should show this user's apps in the main tab. * That is, either this user is a full user, so its apps should be presented accordingly, or, if * this user is a profile, then its apps should be shown alongside its parent's apps. @@ -157,6 +165,7 @@ public final class UserProperties implements Parcelable { * @hide */ @IntDef(prefix = "SHOW_IN_SETTINGS_", value = { + SHOW_IN_SETTINGS_UNKNOWN, SHOW_IN_SETTINGS_WITH_PARENT, SHOW_IN_SETTINGS_SEPARATE, SHOW_IN_SETTINGS_NO, @@ -165,6 +174,12 @@ public final class UserProperties implements Parcelable { public @interface ShowInSettings { } /** + * Indicates that the show in settings value for this profile is unknown or unsupported. + * @hide + */ + @SuppressLint("UnflaggedApi") // b/306636213 + public static final int SHOW_IN_SETTINGS_UNKNOWN = -1; + /** * Suggests that the Settings app should show this user's apps in the main tab. * That is, either this user is a full user, so its apps should be presented accordingly, or, if * this user is a profile, then its apps should be shown alongside its parent's apps. @@ -309,6 +324,7 @@ public final class UserProperties implements Parcelable { @Retention(RetentionPolicy.SOURCE) @IntDef(prefix = "SHOW_IN_QUIET_MODE_", value = { + SHOW_IN_QUIET_MODE_UNKNOWN, SHOW_IN_QUIET_MODE_PAUSED, SHOW_IN_QUIET_MODE_HIDDEN, SHOW_IN_QUIET_MODE_DEFAULT, @@ -318,6 +334,12 @@ public final class UserProperties implements Parcelable { } /** + * Indicates that the show in quiet mode value for this profile is unknown. + */ + @SuppressLint("UnflaggedApi") // b/306636213 + public static final int SHOW_IN_QUIET_MODE_UNKNOWN = -1; + + /** * Indicates that the profile should still be visible in quiet mode but should be shown as * paused (e.g. by greying out its icons). */ @@ -347,6 +369,7 @@ public final class UserProperties implements Parcelable { @Retention(RetentionPolicy.SOURCE) @IntDef(prefix = "SHOW_IN_SHARING_SURFACES_", value = { + SHOW_IN_SHARING_SURFACES_UNKNOWN, SHOW_IN_SHARING_SURFACES_SEPARATE, SHOW_IN_SHARING_SURFACES_WITH_PARENT, SHOW_IN_SHARING_SURFACES_NO, @@ -356,6 +379,12 @@ public final class UserProperties implements Parcelable { } /** + * Indicates that the show in launcher value for this profile is unknown or unsupported. + */ + @SuppressLint("UnflaggedApi") // b/306636213 + public static final int SHOW_IN_SHARING_SURFACES_UNKNOWN = SHOW_IN_LAUNCHER_UNKNOWN; + + /** * Indicates that the profile data and apps should be shown in sharing surfaces intermixed with * parent user's data and apps. */ @@ -379,7 +408,8 @@ public final class UserProperties implements Parcelable { * * @hide */ - @IntDef(prefix = {"CROSS_PROFILE_CONTENT_SHARING_STRATEGY_"}, value = { + @IntDef(prefix = {"CROSS_PROFILE_CONTENT_SHARING_"}, value = { + CROSS_PROFILE_CONTENT_SHARING_UNKNOWN, CROSS_PROFILE_CONTENT_SHARING_NO_DELEGATION, CROSS_PROFILE_CONTENT_SHARING_DELEGATE_FROM_PARENT }) @@ -388,6 +418,13 @@ public final class UserProperties implements Parcelable { } /** + * Signifies that cross-profile content sharing strategy, both to and from this profile, is + * unknown/unsupported. + */ + @SuppressLint("UnflaggedApi") // b/306636213 + public static final int CROSS_PROFILE_CONTENT_SHARING_UNKNOWN = -1; + + /** * Signifies that cross-profile content sharing strategy, both to and from this profile, should * not be delegated to any other user/profile. * For ex: diff --git a/core/java/android/content/pm/multiuser.aconfig b/core/java/android/content/pm/multiuser.aconfig index 10368653f0c4..7c5d3054c945 100644 --- a/core/java/android/content/pm/multiuser.aconfig +++ b/core/java/android/content/pm/multiuser.aconfig @@ -71,10 +71,10 @@ flag { description: "Add support for Private Space in resolver sheet" bug: "307515485" } + 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 -} + name: "move_quiet_mode_operations_to_separate_thread" + namespace: "profile_experiences" + description: "Move the quiet mode operations, happening on a background thread today, to a separate thread." + bug: "320483504" +}
\ No newline at end of file diff --git a/core/java/android/content/res/FontScaleConverter.java b/core/java/android/content/res/FontScaleConverter.java index 088949e7eec2..f4312a905110 100644 --- a/core/java/android/content/res/FontScaleConverter.java +++ b/core/java/android/content/res/FontScaleConverter.java @@ -17,7 +17,9 @@ package android.content.res; +import android.annotation.AnyThread; import android.annotation.FlaggedApi; +import android.annotation.Nullable; /** * A converter for non-linear font scaling. Converts font sizes given in "sp" dimensions to a @@ -40,4 +42,35 @@ public interface FontScaleConverter { * Converts a dimension in "dp" back to "sp". */ float convertDpToSp(float dp); + + /** + * Returns true if non-linear font scaling curves would be in effect for the given scale, false + * if the scaling would follow a linear curve or for no scaling. + * + * <p>Example usage: {@code + * isNonLinearFontScalingActive(getResources().getConfiguration().fontScale)} + */ + @AnyThread + static boolean isNonLinearFontScalingActive(float fontScale) { + return FontScaleConverterFactory.isNonLinearFontScalingActive(fontScale); + } + + /** + * Finds a matching FontScaleConverter for the given fontScale factor. + * + * Generally you shouldn't need this; you can use {@link + * android.util.TypedValue#applyDimension(int, float, DisplayMetrics)} directly and it will do + * the scaling conversion for you. Dimens and resources loaded from XML will also be + * automatically converted. But for UI frameworks or other situations where you need to do the + * conversion without an Android Context, you can use this method. + * + * @param fontScale the scale factor, usually from {@link Configuration#fontScale}. + * + * @return a converter for the given scale, or null if non-linear scaling should not be used. + */ + @Nullable + @AnyThread + static FontScaleConverter forScale(float fontScale) { + return FontScaleConverterFactory.forScale(fontScale); + } } diff --git a/core/java/android/content/res/FontScaleConverterFactory.java b/core/java/android/content/res/FontScaleConverterFactory.java index 5d31cc0f0243..cbe4c62d7069 100644 --- a/core/java/android/content/res/FontScaleConverterFactory.java +++ b/core/java/android/content/res/FontScaleConverterFactory.java @@ -17,7 +17,6 @@ package android.content.res; import android.annotation.AnyThread; -import android.annotation.FlaggedApi; import android.annotation.NonNull; import android.annotation.Nullable; import android.util.MathUtils; @@ -32,8 +31,9 @@ import com.android.internal.annotations.VisibleForTesting; * android.util.TypedValue#applyDimension(int, float, DisplayMetrics)} directly and it will do the * scaling conversion for you. But for UI frameworks or other situations where you need to do the * conversion without an Android Context, you can use this class. + * + * @hide */ -@FlaggedApi(Flags.FLAG_FONT_SCALE_CONVERTER_PUBLIC) public class FontScaleConverterFactory { private static final float SCALE_KEY_MULTIPLIER = 100f; @@ -124,7 +124,6 @@ public class FontScaleConverterFactory { * <p>Example usage: * <code>isNonLinearFontScalingActive(getResources().getConfiguration().fontScale)</code> */ - @FlaggedApi(Flags.FLAG_FONT_SCALE_CONVERTER_PUBLIC) @AnyThread public static boolean isNonLinearFontScalingActive(float fontScale) { return fontScale >= sMinScaleBeforeCurvesApplied; @@ -137,7 +136,6 @@ public class FontScaleConverterFactory { * * @return a converter for the given scale, or null if non-linear scaling should not be used. */ - @FlaggedApi(Flags.FLAG_FONT_SCALE_CONVERTER_PUBLIC) @Nullable @AnyThread public static FontScaleConverter forScale(float fontScale) { diff --git a/core/java/android/credentials/CredentialManager.java b/core/java/android/credentials/CredentialManager.java index 47ee76e50c9a..796a57bf6880 100644 --- a/core/java/android/credentials/CredentialManager.java +++ b/core/java/android/credentials/CredentialManager.java @@ -33,12 +33,12 @@ import android.content.Context; import android.content.IntentSender; import android.os.Binder; import android.os.CancellationSignal; +import android.os.IBinder; import android.os.ICancellationSignal; import android.os.OutcomeReceiver; import android.os.RemoteException; import android.provider.DeviceConfig; import android.util.Log; -import android.view.autofill.IAutoFillManagerClient; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -138,7 +138,7 @@ public final class CredentialManager { @CallbackExecutor @NonNull Executor executor, @NonNull OutcomeReceiver<GetCandidateCredentialsResponse, GetCandidateCredentialsException> callback, - @NonNull IAutoFillManagerClient clientCallback + @NonNull IBinder clientCallback ) { requireNonNull(request, "request must not be null"); requireNonNull(callingPackage, "callingPackage must not be null"); diff --git a/core/java/android/credentials/ICredentialManager.aidl b/core/java/android/credentials/ICredentialManager.aidl index 726bc979ea8f..d4a2d9735232 100644 --- a/core/java/android/credentials/ICredentialManager.aidl +++ b/core/java/android/credentials/ICredentialManager.aidl @@ -22,7 +22,6 @@ import android.credentials.CredentialProviderInfo; import android.credentials.ClearCredentialStateRequest; import android.credentials.CreateCredentialRequest; import android.credentials.GetCandidateCredentialsRequest; -import android.view.autofill.IAutoFillManagerClient; import android.credentials.GetCredentialRequest; import android.credentials.RegisterCredentialDescriptionRequest; import android.credentials.UnregisterCredentialDescriptionRequest; @@ -33,6 +32,7 @@ import android.credentials.IGetCandidateCredentialsCallback; import android.credentials.IPrepareGetCredentialCallback; import android.credentials.ISetEnabledProvidersCallback; import android.content.ComponentName; +import android.os.IBinder; import android.os.ICancellationSignal; /** @@ -48,7 +48,7 @@ interface ICredentialManager { @nullable ICancellationSignal executeCreateCredential(in CreateCredentialRequest request, in ICreateCredentialCallback callback, String callingPackage); - @nullable ICancellationSignal getCandidateCredentials(in GetCredentialRequest request, in IGetCandidateCredentialsCallback callback, in IAutoFillManagerClient clientCallback, String callingPackage); + @nullable ICancellationSignal getCandidateCredentials(in GetCredentialRequest request, in IGetCandidateCredentialsCallback callback, in IBinder clientCallback, String callingPackage); @nullable ICancellationSignal clearCredentialState(in ClearCredentialStateRequest request, in IClearCredentialStateCallback callback, String callingPackage); diff --git a/core/java/android/credentials/flags.aconfig b/core/java/android/credentials/flags.aconfig index f876eebe64c1..1165f9a697de 100644 --- a/core/java/android/credentials/flags.aconfig +++ b/core/java/android/credentials/flags.aconfig @@ -33,4 +33,11 @@ flag { name: "new_settings_ui" description: "Enables new settings UI for VIC" bug: "315209085" +} + +flag { + namespace: "credential_manager" + name: "selector_ui_improvements_enabled" + description: "Enables Credential Selector UI improvements for VIC" + bug: "319448437" }
\ No newline at end of file diff --git a/core/java/android/hardware/biometrics/BiometricPrompt.java b/core/java/android/hardware/biometrics/BiometricPrompt.java index a0f4d8df0bd5..c0424dbeb813 100644 --- a/core/java/android/hardware/biometrics/BiometricPrompt.java +++ b/core/java/android/hardware/biometrics/BiometricPrompt.java @@ -16,6 +16,7 @@ package android.hardware.biometrics; +import static android.Manifest.permission.MANAGE_BIOMETRIC_DIALOG; import static android.Manifest.permission.TEST_BIOMETRIC; import static android.Manifest.permission.USE_BIOMETRIC; import static android.Manifest.permission.USE_BIOMETRIC_INTERNAL; @@ -25,6 +26,7 @@ import static android.hardware.biometrics.Flags.FLAG_GET_OP_ID_CRYPTO_OBJECT; import static android.hardware.biometrics.Flags.FLAG_CUSTOM_BIOMETRIC_PROMPT; import android.annotation.CallbackExecutor; +import android.annotation.DrawableRes; import android.annotation.FlaggedApi; import android.annotation.IntDef; import android.annotation.NonNull; @@ -33,6 +35,7 @@ import android.annotation.RequiresPermission; import android.annotation.TestApi; import android.content.Context; import android.content.DialogInterface; +import android.graphics.Bitmap; import android.hardware.face.FaceManager; import android.hardware.fingerprint.FingerprintManager; import android.os.Binder; @@ -160,6 +163,45 @@ public class BiometricPrompt implements BiometricAuthenticator, BiometricConstan } /** + * Optional: Sets the drawable resource of the logo that will be shown on the prompt. + * + * <p> Note that using this method is not recommended in most scenarios because the calling + * application's icon will be used by default. Setting the logo is intended for large + * bundled applications that perform a wide range of functions and need to show distinct + * icons for each function. + * + * @param logoRes A drawable resource of the logo that will be shown on the prompt. + * @return This builder. + */ + @FlaggedApi(FLAG_CUSTOM_BIOMETRIC_PROMPT) + @RequiresPermission(MANAGE_BIOMETRIC_DIALOG) + @NonNull + public BiometricPrompt.Builder setLogo(@DrawableRes int logoRes) { + mPromptInfo.setLogoRes(logoRes); + return this; + } + + /** + * Optional: Sets the bitmap drawable of the logo that will be shown on the prompt. + * + * <p> Note that using this method is not recommended in most scenarios because the calling + * application's icon will be used by default. Setting the logo is intended for large + * bundled applications that perform a wide range of functions and need to show distinct + * icons for each function. + * + * @param logoBitmap A bitmap drawable of the logo that will be shown on the prompt. + * @return This builder. + */ + @FlaggedApi(FLAG_CUSTOM_BIOMETRIC_PROMPT) + @RequiresPermission(MANAGE_BIOMETRIC_DIALOG) + @NonNull + public BiometricPrompt.Builder setLogo(@NonNull Bitmap logoBitmap) { + mPromptInfo.setLogoBitmap(logoBitmap); + return this; + } + + + /** * Required: Sets the title that will be shown on the prompt. * @param title The title to display. * @return This builder. @@ -676,6 +718,34 @@ public class BiometricPrompt implements BiometricAuthenticator, BiometricConstan } /** + * Gets the drawable resource of the logo for the prompt, as set by + * {@link Builder#setLogo(int)}. Currently for system applications use only. + * + * @return The drawable resource of the logo, or -1 if the prompt has no logo resource set. + */ + @FlaggedApi(FLAG_CUSTOM_BIOMETRIC_PROMPT) + @RequiresPermission(MANAGE_BIOMETRIC_DIALOG) + @DrawableRes + public int getLogoRes() { + return mPromptInfo.getLogoRes(); + } + + /** + * Gets the logo bitmap for the prompt, as set by {@link Builder#setLogo(Bitmap)}. Currently for + * system applications use only. + * + * @return The logo bitmap of the prompt, or null if the prompt has no logo bitmap set. + */ + @FlaggedApi(FLAG_CUSTOM_BIOMETRIC_PROMPT) + @RequiresPermission(MANAGE_BIOMETRIC_DIALOG) + @Nullable + public Bitmap getLogoBitmap() { + return mPromptInfo.getLogoBitmap(); + } + + + + /** * Gets the title for the prompt, as set by {@link Builder#setTitle(CharSequence)}. * @return The title of the prompt, which is guaranteed to be non-null. */ diff --git a/core/java/android/hardware/biometrics/IBiometricContextListener.aidl b/core/java/android/hardware/biometrics/IBiometricContextListener.aidl index 6ac658144164..2f58f51d5483 100644 --- a/core/java/android/hardware/biometrics/IBiometricContextListener.aidl +++ b/core/java/android/hardware/biometrics/IBiometricContextListener.aidl @@ -38,4 +38,8 @@ oneway interface IBiometricContextListener { // Called when the display state of the device changes. // Where `displayState` is defined in AuthenticateOptions.DisplayState void onDisplayStateChanged(int displayState); + + // Called when the HAL ignoring touches state changes. + // When true, the HAL ignores touches on the sensor. + void onHardwareIgnoreTouchesChanged(boolean shouldIgnore); } diff --git a/core/java/android/hardware/biometrics/PromptContentListItem.java b/core/java/android/hardware/biometrics/PromptContentItem.java index fa3783d6d874..c47b37aca2ea 100644 --- a/core/java/android/hardware/biometrics/PromptContentListItem.java +++ b/core/java/android/hardware/biometrics/PromptContentItem.java @@ -21,9 +21,9 @@ import static android.hardware.biometrics.Flags.FLAG_CUSTOM_BIOMETRIC_PROMPT; import android.annotation.FlaggedApi; /** - * A list item shown on {@link PromptVerticalListContentView}. + * An item shown on {@link PromptContentView}. */ @FlaggedApi(FLAG_CUSTOM_BIOMETRIC_PROMPT) -public interface PromptContentListItem { +public interface PromptContentItem { } diff --git a/core/java/android/hardware/biometrics/PromptContentListItemBulletedText.java b/core/java/android/hardware/biometrics/PromptContentItemBulletedText.java index c31f8a63bcb8..c5e5a8076747 100644 --- a/core/java/android/hardware/biometrics/PromptContentListItemBulletedText.java +++ b/core/java/android/hardware/biometrics/PromptContentItemBulletedText.java @@ -27,7 +27,7 @@ import android.os.Parcelable; * A list item with bulleted text shown on {@link PromptVerticalListContentView}. */ @FlaggedApi(FLAG_CUSTOM_BIOMETRIC_PROMPT) -public final class PromptContentListItemBulletedText implements PromptContentListItemParcelable { +public final class PromptContentItemBulletedText implements PromptContentItemParcelable { private final CharSequence mText; /** @@ -35,7 +35,7 @@ public final class PromptContentListItemBulletedText implements PromptContentLis * * @param text The text of this list item. */ - public PromptContentListItemBulletedText(@NonNull CharSequence text) { + public PromptContentItemBulletedText(@NonNull CharSequence text) { mText = text; } @@ -67,15 +67,15 @@ public final class PromptContentListItemBulletedText implements PromptContentLis * @see Parcelable.Creator */ @NonNull - public static final Creator<PromptContentListItemBulletedText> CREATOR = new Creator<>() { + public static final Creator<PromptContentItemBulletedText> CREATOR = new Creator<>() { @Override - public PromptContentListItemBulletedText createFromParcel(Parcel in) { - return new PromptContentListItemBulletedText(in.readCharSequence()); + public PromptContentItemBulletedText createFromParcel(Parcel in) { + return new PromptContentItemBulletedText(in.readCharSequence()); } @Override - public PromptContentListItemBulletedText[] newArray(int size) { - return new PromptContentListItemBulletedText[size]; + public PromptContentItemBulletedText[] newArray(int size) { + return new PromptContentItemBulletedText[size]; } }; } diff --git a/core/java/android/hardware/biometrics/PromptContentListItemParcelable.java b/core/java/android/hardware/biometrics/PromptContentItemParcelable.java index 15271f003c50..668912cfdf24 100644 --- a/core/java/android/hardware/biometrics/PromptContentListItemParcelable.java +++ b/core/java/android/hardware/biometrics/PromptContentItemParcelable.java @@ -22,9 +22,9 @@ import android.annotation.FlaggedApi; import android.os.Parcelable; /** - * A parcelable {@link PromptContentListItem}. + * A parcelable {@link PromptContentItem}. */ @FlaggedApi(FLAG_CUSTOM_BIOMETRIC_PROMPT) -sealed interface PromptContentListItemParcelable extends PromptContentListItem, Parcelable - permits PromptContentListItemPlainText, PromptContentListItemBulletedText { +sealed interface PromptContentItemParcelable extends PromptContentItem, Parcelable + permits PromptContentItemPlainText, PromptContentItemBulletedText { } diff --git a/core/java/android/hardware/biometrics/PromptContentListItemPlainText.java b/core/java/android/hardware/biometrics/PromptContentItemPlainText.java index d72a7586d6ad..6434c5975c12 100644 --- a/core/java/android/hardware/biometrics/PromptContentListItemPlainText.java +++ b/core/java/android/hardware/biometrics/PromptContentItemPlainText.java @@ -27,7 +27,7 @@ import android.os.Parcelable; * A list item with plain text shown on {@link PromptVerticalListContentView}. */ @FlaggedApi(FLAG_CUSTOM_BIOMETRIC_PROMPT) -public final class PromptContentListItemPlainText implements PromptContentListItemParcelable { +public final class PromptContentItemPlainText implements PromptContentItemParcelable { private final CharSequence mText; /** @@ -35,7 +35,7 @@ public final class PromptContentListItemPlainText implements PromptContentListIt * * @param text The text of this list item. */ - public PromptContentListItemPlainText(@NonNull CharSequence text) { + public PromptContentItemPlainText(@NonNull CharSequence text) { mText = text; } @@ -67,15 +67,15 @@ public final class PromptContentListItemPlainText implements PromptContentListIt * @see Parcelable.Creator */ @NonNull - public static final Creator<PromptContentListItemPlainText> CREATOR = new Creator<>() { + public static final Creator<PromptContentItemPlainText> CREATOR = new Creator<>() { @Override - public PromptContentListItemPlainText createFromParcel(Parcel in) { - return new PromptContentListItemPlainText(in.readCharSequence()); + public PromptContentItemPlainText createFromParcel(Parcel in) { + return new PromptContentItemPlainText(in.readCharSequence()); } @Override - public PromptContentListItemPlainText[] newArray(int size) { - return new PromptContentListItemPlainText[size]; + public PromptContentItemPlainText[] newArray(int size) { + return new PromptContentItemPlainText[size]; } }; } diff --git a/core/java/android/hardware/biometrics/PromptInfo.java b/core/java/android/hardware/biometrics/PromptInfo.java index c73ebd4dbe76..d788b37c781d 100644 --- a/core/java/android/hardware/biometrics/PromptInfo.java +++ b/core/java/android/hardware/biometrics/PromptInfo.java @@ -16,8 +16,10 @@ package android.hardware.biometrics; +import android.annotation.DrawableRes; import android.annotation.NonNull; import android.annotation.Nullable; +import android.graphics.Bitmap; import android.os.Parcel; import android.os.Parcelable; @@ -30,6 +32,8 @@ import java.util.List; */ public class PromptInfo implements Parcelable { + @DrawableRes private int mLogoRes = -1; + @Nullable private Bitmap mLogoBitmap; @NonNull private CharSequence mTitle; private boolean mUseDefaultTitle; @Nullable private CharSequence mSubtitle; @@ -56,6 +60,8 @@ public class PromptInfo implements Parcelable { } PromptInfo(Parcel in) { + mLogoRes = in.readInt(); + mLogoBitmap = in.readTypedObject(Bitmap.CREATOR); mTitle = in.readCharSequence(); mUseDefaultTitle = in.readBoolean(); mSubtitle = in.readCharSequence(); @@ -98,6 +104,8 @@ public class PromptInfo implements Parcelable { @Override public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(mLogoRes); + dest.writeTypedObject(mLogoBitmap, 0); dest.writeCharSequence(mTitle); dest.writeBoolean(mUseDefaultTitle); dest.writeCharSequence(mSubtitle); @@ -156,9 +164,30 @@ public class PromptInfo implements Parcelable { } return false; } + + /** + * Returns whether MANAGE_BIOMETRIC_DIALOG is contained. + */ + public boolean containsManageBioApiConfigurations() { + if (mLogoRes != -1) { + return true; + } else if (mLogoBitmap != null) { + return true; + } + return false; + } // LINT.ThenChange(frameworks/base/core/java/android/hardware/biometrics/BiometricPrompt.java) // Setters + public void setLogoRes(@DrawableRes int logoRes) { + mLogoRes = logoRes; + checkOnlyOneLogoSet(); + } + + public void setLogoBitmap(@NonNull Bitmap logoBitmap) { + mLogoBitmap = logoBitmap; + checkOnlyOneLogoSet(); + } public void setTitle(CharSequence title) { mTitle = title; @@ -244,6 +273,14 @@ public class PromptInfo implements Parcelable { } // Getters + @DrawableRes + public int getLogoRes() { + return mLogoRes; + } + + public Bitmap getLogoBitmap() { + return mLogoBitmap; + } public CharSequence getTitle() { return mTitle; @@ -337,4 +374,11 @@ public class PromptInfo implements Parcelable { public boolean isShowEmergencyCallButton() { return mShowEmergencyCallButton; } + + private void checkOnlyOneLogoSet() { + if (mLogoRes != -1 && mLogoBitmap != null) { + throw new IllegalStateException( + "Exclusively one of logo resource or logo bitmap can be set"); + } + } } diff --git a/core/java/android/hardware/biometrics/PromptVerticalListContentView.java b/core/java/android/hardware/biometrics/PromptVerticalListContentView.java index f3cb189a5df5..f3e62907d845 100644 --- a/core/java/android/hardware/biometrics/PromptVerticalListContentView.java +++ b/core/java/android/hardware/biometrics/PromptVerticalListContentView.java @@ -40,9 +40,9 @@ import java.util.List; * .setSubTitle(...) * .setContentView(new PromptVerticalListContentView.Builder() * .setDescription("test description") - * .addListItem(new PromptContentListItemPlainText("test item 1")) - * .addListItem(new PromptContentListItemPlainText("test item 2")) - * .addListItem(new PromptContentListItemBulletedText("test item 3")) + * .addListItem(new PromptContentItemPlainText("test item 1")) + * .addListItem(new PromptContentItemPlainText("test item 2")) + * .addListItem(new PromptContentItemBulletedText("test item 3")) * .build()) * .build(); * </pre> @@ -51,11 +51,11 @@ import java.util.List; public final class PromptVerticalListContentView implements PromptContentViewParcelable { private static final int MAX_ITEM_NUMBER = 20; private static final int MAX_EACH_ITEM_CHARACTER_NUMBER = 640; - private final List<PromptContentListItemParcelable> mContentList; + private final List<PromptContentItemParcelable> mContentList; private final CharSequence mDescription; private PromptVerticalListContentView( - @NonNull List<PromptContentListItemParcelable> contentList, + @NonNull List<PromptContentItemParcelable> contentList, @NonNull CharSequence description) { mContentList = contentList; mDescription = description; @@ -63,8 +63,8 @@ public final class PromptVerticalListContentView implements PromptContentViewPar private PromptVerticalListContentView(Parcel in) { mContentList = in.readArrayList( - PromptContentListItemParcelable.class.getClassLoader(), - PromptContentListItemParcelable.class); + PromptContentItemParcelable.class.getClassLoader(), + PromptContentItemParcelable.class); mDescription = in.readCharSequence(); } @@ -94,13 +94,13 @@ public final class PromptVerticalListContentView implements PromptContentViewPar } /** - * Gets the list of ListItem on the content view, as set by - * {@link PromptVerticalListContentView.Builder#addListItem(PromptContentListItem)}. + * Gets the list of items on the content view, as set by + * {@link PromptVerticalListContentView.Builder#addListItem(PromptContentItem)}. * * @return The item list on the content view. */ @NonNull - public List<PromptContentListItem> getListItems() { + public List<PromptContentItem> getListItems() { return new ArrayList<>(mContentList); } @@ -142,7 +142,7 @@ public final class PromptVerticalListContentView implements PromptContentViewPar * A builder that collects arguments to be shown on the vertical list view. */ public static final class Builder { - private final List<PromptContentListItemParcelable> mContentList = new ArrayList<>(); + private final List<PromptContentItemParcelable> mContentList = new ArrayList<>(); private CharSequence mDescription; /** @@ -159,28 +159,50 @@ public final class PromptVerticalListContentView implements PromptContentViewPar /** * Optional: Adds a list item in the current row. Maximum {@value MAX_ITEM_NUMBER} items in - * total. + * total. The maximum length for each item is {@value MAX_EACH_ITEM_CHARACTER_NUMBER} + * characters. * * @param listItem The list item view to display * @return This builder. */ @NonNull - public Builder addListItem(@NonNull PromptContentListItem listItem) { + public Builder addListItem(@NonNull PromptContentItem listItem) { if (doesListItemExceedsCharLimit(listItem)) { throw new IllegalStateException( "The character number of list item exceeds " + MAX_EACH_ITEM_CHARACTER_NUMBER); } - mContentList.add((PromptContentListItemParcelable) listItem); + mContentList.add((PromptContentItemParcelable) listItem); return this; } - private boolean doesListItemExceedsCharLimit(PromptContentListItem listItem) { - if (listItem instanceof PromptContentListItemPlainText) { - return ((PromptContentListItemPlainText) listItem).getText().length() + + /** + * Optional: Adds a list item in the current row. Maximum {@value MAX_ITEM_NUMBER} items in + * total. The maximum length for each item is {@value MAX_EACH_ITEM_CHARACTER_NUMBER} + * characters. + * + * @param listItem The list item view to display + * @param index The position at which to add the item + * @return This builder. + */ + @NonNull + public Builder addListItem(@NonNull PromptContentItem listItem, int index) { + if (doesListItemExceedsCharLimit(listItem)) { + throw new IllegalStateException( + "The character number of list item exceeds " + + MAX_EACH_ITEM_CHARACTER_NUMBER); + } + mContentList.add(index, (PromptContentItemParcelable) listItem); + return this; + } + + private boolean doesListItemExceedsCharLimit(PromptContentItem listItem) { + if (listItem instanceof PromptContentItemPlainText) { + return ((PromptContentItemPlainText) listItem).getText().length() > MAX_EACH_ITEM_CHARACTER_NUMBER; - } else if (listItem instanceof PromptContentListItemBulletedText) { - return ((PromptContentListItemBulletedText) listItem).getText().length() + } else if (listItem instanceof PromptContentItemBulletedText) { + return ((PromptContentItemBulletedText) listItem).getText().length() > MAX_EACH_ITEM_CHARACTER_NUMBER; } else { return false; diff --git a/core/java/android/hardware/input/VirtualStylus.java b/core/java/android/hardware/input/VirtualStylus.java new file mode 100644 index 000000000000..c763f7406f3a --- /dev/null +++ b/core/java/android/hardware/input/VirtualStylus.java @@ -0,0 +1,80 @@ +/* + * Copyright 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.hardware.input; + +import android.annotation.FlaggedApi; +import android.annotation.NonNull; +import android.annotation.RequiresPermission; +import android.annotation.SystemApi; +import android.companion.virtual.IVirtualDevice; +import android.companion.virtual.flags.Flags; +import android.os.IBinder; +import android.os.RemoteException; +import android.util.Log; + +/** + * A virtual stylus which can be used to inject input into the framework that represents a stylus + * on a remote device. + * + * This registers an {@link android.view.InputDevice} that is interpreted like a + * physically-connected device and dispatches received events to it. + * + * @hide + */ +@FlaggedApi(Flags.FLAG_VIRTUAL_STYLUS) +@SystemApi +public class VirtualStylus extends VirtualInputDevice { + /** @hide */ + public VirtualStylus(VirtualStylusConfig config, IVirtualDevice virtualDevice, + IBinder token) { + super(config, virtualDevice, token); + } + + /** + * Sends a motion event to the system. + * + * @param event the event to send + */ + @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) + public void sendMotionEvent(@NonNull VirtualStylusMotionEvent event) { + try { + if (!mVirtualDevice.sendStylusMotionEvent(mToken, event)) { + Log.w(TAG, "Failed to send motion event from virtual stylus " + + mConfig.getInputDeviceName()); + } + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Sends a button event to the system. + * + * @param event the event to send + */ + @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) + public void sendButtonEvent(@NonNull VirtualStylusButtonEvent event) { + try { + if (!mVirtualDevice.sendStylusButtonEvent(mToken, event)) { + Log.w(TAG, "Failed to send button event from virtual stylus " + + mConfig.getInputDeviceName()); + } + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } +} diff --git a/core/java/android/hardware/input/VirtualStylusButtonEvent.aidl b/core/java/android/hardware/input/VirtualStylusButtonEvent.aidl new file mode 100644 index 000000000000..7de32cc7d33e --- /dev/null +++ b/core/java/android/hardware/input/VirtualStylusButtonEvent.aidl @@ -0,0 +1,19 @@ +/* + * Copyright 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.hardware.input; + +parcelable VirtualStylusButtonEvent; diff --git a/core/java/android/hardware/input/VirtualStylusButtonEvent.java b/core/java/android/hardware/input/VirtualStylusButtonEvent.java new file mode 100644 index 000000000000..97a4cd0f692b --- /dev/null +++ b/core/java/android/hardware/input/VirtualStylusButtonEvent.java @@ -0,0 +1,215 @@ +/* + * Copyright 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.hardware.input; + +import android.annotation.FlaggedApi; +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.SystemApi; +import android.companion.virtual.flags.Flags; +import android.os.Parcel; +import android.os.Parcelable; +import android.os.SystemClock; +import android.view.InputEvent; +import android.view.MotionEvent; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * An event describing a stylus button click interaction originating from a remote device. + * + * @hide + */ +@FlaggedApi(Flags.FLAG_VIRTUAL_STYLUS) +@SystemApi +public final class VirtualStylusButtonEvent implements Parcelable { + /** @hide */ + public static final int ACTION_UNKNOWN = -1; + /** Action indicating the stylus button has been pressed. */ + public static final int ACTION_BUTTON_PRESS = MotionEvent.ACTION_BUTTON_PRESS; + /** Action indicating the stylus button has been released. */ + public static final int ACTION_BUTTON_RELEASE = MotionEvent.ACTION_BUTTON_RELEASE; + /** @hide */ + @IntDef(prefix = {"ACTION_"}, value = { + ACTION_UNKNOWN, + ACTION_BUTTON_PRESS, + ACTION_BUTTON_RELEASE, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface Action {} + + /** @hide */ + public static final int BUTTON_UNKNOWN = -1; + /** Action indicating the stylus button involved in this event is primary. */ + public static final int BUTTON_PRIMARY = MotionEvent.BUTTON_STYLUS_PRIMARY; + /** Action indicating the stylus button involved in this event is secondary. */ + public static final int BUTTON_SECONDARY = MotionEvent.BUTTON_STYLUS_SECONDARY; + /** @hide */ + @IntDef(prefix = {"BUTTON_"}, value = { + BUTTON_UNKNOWN, + BUTTON_PRIMARY, + BUTTON_SECONDARY, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface Button {} + + @Action + private final int mAction; + @Button + private final int mButtonCode; + private final long mEventTimeNanos; + + private VirtualStylusButtonEvent(@Action int action, @Button int buttonCode, + long eventTimeNanos) { + mAction = action; + mButtonCode = buttonCode; + mEventTimeNanos = eventTimeNanos; + } + + private VirtualStylusButtonEvent(@NonNull Parcel parcel) { + mAction = parcel.readInt(); + mButtonCode = parcel.readInt(); + mEventTimeNanos = parcel.readLong(); + } + + @Override + public void writeToParcel(@NonNull Parcel parcel, int parcelableFlags) { + parcel.writeInt(mAction); + parcel.writeInt(mButtonCode); + parcel.writeLong(mEventTimeNanos); + } + + @Override + public int describeContents() { + return 0; + } + + /** + * Returns the button code associated with this event. + */ + @Button + public int getButtonCode() { + return mButtonCode; + } + + /** + * Returns the action associated with this event. + */ + @Action + public int getAction() { + return mAction; + } + + /** + * Returns the time this event occurred, in the {@link SystemClock#uptimeMillis()} time base but + * with nanosecond (instead of millisecond) precision. + * + * @see InputEvent#getEventTime() + */ + public long getEventTimeNanos() { + return mEventTimeNanos; + } + + /** + * Builder for {@link VirtualStylusButtonEvent}. + */ + @FlaggedApi(Flags.FLAG_VIRTUAL_STYLUS) + public static final class Builder { + + @Action + private int mAction = ACTION_UNKNOWN; + @Button + private int mButtonCode = BUTTON_UNKNOWN; + private long mEventTimeNanos = 0L; + + /** + * Creates a {@link VirtualStylusButtonEvent} object with the current builder configuration. + */ + @NonNull + public VirtualStylusButtonEvent build() { + if (mAction == ACTION_UNKNOWN) { + throw new IllegalArgumentException( + "Cannot build stylus button event with unset action"); + } + if (mButtonCode == BUTTON_UNKNOWN) { + throw new IllegalArgumentException( + "Cannot build stylus button event with unset button code"); + } + return new VirtualStylusButtonEvent(mAction, mButtonCode, mEventTimeNanos); + } + + /** + * Sets the button code of the event. + * + * @return this builder, to allow for chaining of calls + */ + @NonNull + public Builder setButtonCode(@Button int buttonCode) { + if (buttonCode != BUTTON_PRIMARY && buttonCode != BUTTON_SECONDARY) { + throw new IllegalArgumentException( + "Unsupported stylus button code : " + buttonCode); + } + mButtonCode = buttonCode; + return this; + } + + /** + * Sets the action of the event. + * + * @return this builder, to allow for chaining of calls + */ + @NonNull + public Builder setAction(@Action int action) { + if (action != ACTION_BUTTON_PRESS && action != ACTION_BUTTON_RELEASE) { + throw new IllegalArgumentException("Unsupported stylus button action : " + action); + } + mAction = action; + return this; + } + + /** + * Sets the time (in nanoseconds) when this specific event was generated. This may be + * obtained from {@link SystemClock#uptimeMillis()} (with nanosecond precision instead of + * millisecond), but can be different depending on the use case. + * This field is optional and can be omitted. + * + * @return this builder, to allow for chaining of calls + * @see InputEvent#getEventTime() + */ + @NonNull + public Builder setEventTimeNanos(long eventTimeNanos) { + if (eventTimeNanos < 0L) { + throw new IllegalArgumentException("Event time cannot be negative"); + } + this.mEventTimeNanos = eventTimeNanos; + return this; + } + } + + @NonNull + public static final Parcelable.Creator<VirtualStylusButtonEvent> CREATOR = + new Parcelable.Creator<>() { + public VirtualStylusButtonEvent createFromParcel(Parcel source) { + return new VirtualStylusButtonEvent(source); + } + + public VirtualStylusButtonEvent[] newArray(int size) { + return new VirtualStylusButtonEvent[size]; + } + }; +} diff --git a/core/java/android/hardware/input/VirtualStylusConfig.aidl b/core/java/android/hardware/input/VirtualStylusConfig.aidl new file mode 100644 index 000000000000..a13eec2a43b5 --- /dev/null +++ b/core/java/android/hardware/input/VirtualStylusConfig.aidl @@ -0,0 +1,19 @@ +/* + * Copyright 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.hardware.input; + +parcelable VirtualStylusConfig; diff --git a/core/java/android/hardware/input/VirtualStylusConfig.java b/core/java/android/hardware/input/VirtualStylusConfig.java new file mode 100644 index 000000000000..64cf1f56d8bc --- /dev/null +++ b/core/java/android/hardware/input/VirtualStylusConfig.java @@ -0,0 +1,98 @@ +/* + * Copyright 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.hardware.input; + +import android.annotation.FlaggedApi; +import android.annotation.IntRange; +import android.annotation.NonNull; +import android.annotation.SystemApi; +import android.companion.virtual.flags.Flags; +import android.os.Parcel; +import android.os.Parcelable; + +/** + * Configurations to create a virtual stylus. + * + * @hide + */ +@FlaggedApi(Flags.FLAG_VIRTUAL_STYLUS) +@SystemApi +public final class VirtualStylusConfig extends VirtualTouchDeviceConfig implements Parcelable { + + private VirtualStylusConfig(@NonNull Builder builder) { + super(builder); + } + + private VirtualStylusConfig(@NonNull Parcel in) { + super(in); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + super.writeToParcel(dest, flags); + } + + @NonNull + public static final Creator<VirtualStylusConfig> CREATOR = + new Creator<>() { + @Override + public VirtualStylusConfig createFromParcel(Parcel in) { + return new VirtualStylusConfig(in); + } + + @Override + public VirtualStylusConfig[] newArray(int size) { + return new VirtualStylusConfig[size]; + } + }; + + /** + * Builder for creating a {@link VirtualStylusConfig}. + */ + @FlaggedApi(Flags.FLAG_VIRTUAL_STYLUS) + public static final class Builder extends VirtualTouchDeviceConfig.Builder<Builder> { + + /** + * Creates a new instance for the given dimensions of the screen targeted by the + * {@link VirtualStylus}. + * + * <p>The dimensions are not pixels but in the screen's raw coordinate space. They do + * not necessarily have to correspond to the display size or aspect ratio. In this case the + * framework will handle the scaling appropriately. + * + * @param screenWidth The width of the targeted screen. + * @param screenHeight The height of the targeted screen. + */ + public Builder(@IntRange(from = 1) int screenWidth, + @IntRange(from = 1) int screenHeight) { + super(screenWidth, screenHeight); + } + + /** + * Builds the {@link VirtualStylusConfig} instance. + */ + @NonNull + public VirtualStylusConfig build() { + return new VirtualStylusConfig(this); + } + } +} diff --git a/core/java/android/hardware/input/VirtualStylusMotionEvent.aidl b/core/java/android/hardware/input/VirtualStylusMotionEvent.aidl new file mode 100644 index 000000000000..42d14ab35b90 --- /dev/null +++ b/core/java/android/hardware/input/VirtualStylusMotionEvent.aidl @@ -0,0 +1,19 @@ +/* + * Copyright 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.hardware.input; + +parcelable VirtualStylusMotionEvent; diff --git a/core/java/android/hardware/input/VirtualStylusMotionEvent.java b/core/java/android/hardware/input/VirtualStylusMotionEvent.java new file mode 100644 index 000000000000..2ab76aee74a4 --- /dev/null +++ b/core/java/android/hardware/input/VirtualStylusMotionEvent.java @@ -0,0 +1,411 @@ +/* + * Copyright 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.hardware.input; + +import android.annotation.FlaggedApi; +import android.annotation.IntDef; +import android.annotation.IntRange; +import android.annotation.NonNull; +import android.annotation.SystemApi; +import android.companion.virtual.flags.Flags; +import android.os.Parcel; +import android.os.Parcelable; +import android.os.SystemClock; +import android.view.InputEvent; +import android.view.MotionEvent; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * An event describing a stylus interaction originating from a remote device. + * + * The tool type, location and action are required; tilts and pressure are optional. + * + * @hide + */ +@FlaggedApi(Flags.FLAG_VIRTUAL_STYLUS) +@SystemApi +public final class VirtualStylusMotionEvent implements Parcelable { + private static final int TILT_MIN = -90; + private static final int TILT_MAX = 90; + private static final int PRESSURE_MIN = 0; + private static final int PRESSURE_MAX = 255; + + /** @hide */ + public static final int TOOL_TYPE_UNKNOWN = MotionEvent.TOOL_TYPE_UNKNOWN; + /** Tool type indicating that a stylus is the origin of the event. */ + public static final int TOOL_TYPE_STYLUS = MotionEvent.TOOL_TYPE_STYLUS; + /** Tool type indicating that an eraser is the origin of the event. */ + public static final int TOOL_TYPE_ERASER = MotionEvent.TOOL_TYPE_ERASER; + /** @hide */ + @IntDef(prefix = { "TOOL_TYPE_" }, value = { + TOOL_TYPE_UNKNOWN, + TOOL_TYPE_STYLUS, + TOOL_TYPE_ERASER, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface ToolType {} + + /** @hide */ + public static final int ACTION_UNKNOWN = -1; + /** + * Action indicating the stylus has been pressed down to the screen. ACTION_DOWN with pressure + * {@code 0} indicates that the stylus is hovering over the screen, and non-zero pressure + * indicates that the stylus is touching the screen. + */ + public static final int ACTION_DOWN = MotionEvent.ACTION_DOWN; + /** Action indicating the stylus has been lifted from the screen. */ + public static final int ACTION_UP = MotionEvent.ACTION_UP; + /** Action indicating the stylus has been moved along the screen. */ + public static final int ACTION_MOVE = MotionEvent.ACTION_MOVE; + /** @hide */ + @IntDef(prefix = { "ACTION_" }, value = { + ACTION_UNKNOWN, + ACTION_DOWN, + ACTION_UP, + ACTION_MOVE, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface Action {} + + @ToolType + private final int mToolType; + @Action + private final int mAction; + private final int mX; + private final int mY; + private final int mPressure; + private final int mTiltX; + private final int mTiltY; + private final long mEventTimeNanos; + + private VirtualStylusMotionEvent(@ToolType int toolType, @Action int action, int x, int y, + int pressure, int tiltX, int tiltY, long eventTimeNanos) { + mToolType = toolType; + mAction = action; + mX = x; + mY = y; + mPressure = pressure; + mTiltX = tiltX; + mTiltY = tiltY; + mEventTimeNanos = eventTimeNanos; + } + + private VirtualStylusMotionEvent(@NonNull Parcel parcel) { + mToolType = parcel.readInt(); + mAction = parcel.readInt(); + mX = parcel.readInt(); + mY = parcel.readInt(); + mPressure = parcel.readInt(); + mTiltX = parcel.readInt(); + mTiltY = parcel.readInt(); + mEventTimeNanos = parcel.readLong(); + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeInt(mToolType); + dest.writeInt(mAction); + dest.writeInt(mX); + dest.writeInt(mY); + dest.writeInt(mPressure); + dest.writeInt(mTiltX); + dest.writeInt(mTiltY); + dest.writeLong(mEventTimeNanos); + } + + @Override + public int describeContents() { + return 0; + } + + /** + * Returns the tool type associated with this event. + */ + @ToolType + public int getToolType() { + return mToolType; + } + + /** + * Returns the action associated with this event. + */ + @Action + public int getAction() { + return mAction; + } + + /** + * Returns the x-axis location associated with this event. + */ + public int getX() { + return mX; + } + + /** + * Returns the y-axis location associated with this event. + */ + public int getY() { + return mY; + } + + /** + * Returns the pressure associated with this event. {@code 0} pressure indicates that the stylus + * is hovering, otherwise the stylus is touching the screen. Returns {@code 255} if omitted. + */ + public int getPressure() { + return mPressure; + } + + /** + * Returns the plane angle (in degrees, in the range of [{@code -90}, {@code 90}]) between the + * y-z plane and the plane containing both the stylus axis and the y axis. A positive tiltX is + * to the right, in the direction of increasing x values. {@code 0} tilt indicates that the + * stylus is perpendicular to the x-axis. Returns {@code 0} if omitted. + * + * @see Builder#setTiltX + */ + public int getTiltX() { + return mTiltX; + } + + /** + * Returns the plane angle (in degrees, in the range of [{@code -90}, {@code 90}]) between the + * x-z plane and the plane containing both the stylus axis and the x axis. A positive tiltY is + * towards the user, in the direction of increasing y values. {@code 0} tilt indicates that the + * stylus is perpendicular to the y-axis. Returns {@code 0} if omitted. + * + * @see Builder#setTiltY + */ + public int getTiltY() { + return mTiltY; + } + + /** + * Returns the time this event occurred, in the {@link SystemClock#uptimeMillis()} time base but + * with nanosecond (instead of millisecond) precision. + * + * @see InputEvent#getEventTime() + */ + public long getEventTimeNanos() { + return mEventTimeNanos; + } + + /** + * Builder for {@link VirtualStylusMotionEvent}. + */ + @FlaggedApi(Flags.FLAG_VIRTUAL_STYLUS) + public static final class Builder { + + @ToolType + private int mToolType = TOOL_TYPE_UNKNOWN; + @Action + private int mAction = ACTION_UNKNOWN; + private int mX = 0; + private int mY = 0; + private boolean mIsXSet = false; + private boolean mIsYSet = false; + private int mPressure = PRESSURE_MAX; + private int mTiltX = 0; + private int mTiltY = 0; + private long mEventTimeNanos = 0L; + + /** + * Creates a {@link VirtualStylusMotionEvent} object with the current builder configuration. + * + * @throws IllegalArgumentException if one of the required arguments (action, tool type, + * x-axis location and y-axis location) is missing. + * {@link VirtualStylusMotionEvent} for a detailed explanation. + */ + @NonNull + public VirtualStylusMotionEvent build() { + if (mToolType == TOOL_TYPE_UNKNOWN) { + throw new IllegalArgumentException( + "Cannot build stylus motion event with unset tool type"); + } + if (mAction == ACTION_UNKNOWN) { + throw new IllegalArgumentException( + "Cannot build stylus motion event with unset action"); + } + if (!mIsXSet) { + throw new IllegalArgumentException( + "Cannot build stylus motion event with unset x-axis location"); + } + if (!mIsYSet) { + throw new IllegalArgumentException( + "Cannot build stylus motion event with unset y-axis location"); + } + return new VirtualStylusMotionEvent(mToolType, mAction, mX, mY, mPressure, mTiltX, + mTiltY, mEventTimeNanos); + } + + /** + * Sets the tool type of the event. + * + * @return this builder, to allow for chaining of calls + */ + @NonNull + public Builder setToolType(@ToolType int toolType) { + if (toolType != TOOL_TYPE_STYLUS && toolType != TOOL_TYPE_ERASER) { + throw new IllegalArgumentException("Unsupported stylus tool type: " + toolType); + } + mToolType = toolType; + return this; + } + + /** + * Sets the action of the event. + * + * @return this builder, to allow for chaining of calls + */ + @NonNull + public Builder setAction(@Action int action) { + if (action != ACTION_DOWN && action != ACTION_UP && action != ACTION_MOVE) { + throw new IllegalArgumentException("Unsupported stylus action : " + action); + } + mAction = action; + return this; + } + + /** + * Sets the x-axis location of the event. + * + * @return this builder, to allow for chaining of calls + */ + @NonNull + public Builder setX(int absX) { + mX = absX; + mIsXSet = true; + return this; + } + + /** + * Sets the y-axis location of the event. + * + * @return this builder, to allow for chaining of calls + */ + @NonNull + public Builder setY(int absY) { + mY = absY; + mIsYSet = true; + return this; + } + + /** + * Sets the pressure of the event. {@code 0} pressure indicates that the stylus is hovering, + * otherwise the stylus is touching the screen. This field is optional and can be omitted + * (defaults to {@code 255}). + * + * @param pressure The pressure of the stylus. + * + * @throws IllegalArgumentException if the pressure is smaller than 0 or greater than 255. + * + * @return this builder, to allow for chaining of calls + */ + @NonNull + public Builder setPressure( + @IntRange(from = PRESSURE_MIN, to = PRESSURE_MAX) int pressure) { + if (pressure < PRESSURE_MIN || pressure > PRESSURE_MAX) { + throw new IllegalArgumentException( + "Pressure should be between " + PRESSURE_MIN + " and " + PRESSURE_MAX); + } + mPressure = pressure; + return this; + } + + /** + * Sets the x-axis tilt of the event in degrees. {@code 0} tilt indicates that the stylus is + * perpendicular to the x-axis. This field is optional and can be omitted (defaults to + * {@code 0}). Both x-axis tilt and y-axis tilt are used to derive the tilt and orientation + * of the stylus, given by {@link MotionEvent#AXIS_TILT} and + * {@link MotionEvent#AXIS_ORIENTATION} respectively. + * + * @throws IllegalArgumentException if the tilt is smaller than -90 or greater than 90. + * + * @return this builder, to allow for chaining of calls + * + * @see VirtualStylusMotionEvent#getTiltX + * @see <a href="https://source.android.com/docs/core/interaction/input/touch-devices#orientation-and-tilt-fields"> + * Stylus tilt and orientation</a> + */ + @NonNull + public Builder setTiltX(@IntRange(from = TILT_MIN, to = TILT_MAX) int tiltX) { + validateTilt(tiltX); + mTiltX = tiltX; + return this; + } + + /** + * Sets the y-axis tilt of the event in degrees. {@code 0} tilt indicates that the stylus is + * perpendicular to the y-axis. This field is optional and can be omitted (defaults to + * {@code 0}). Both x-axis tilt and y-axis tilt are used to derive the tilt and orientation + * of the stylus, given by {@link MotionEvent#AXIS_TILT} and + * {@link MotionEvent#AXIS_ORIENTATION} respectively. + * + * @throws IllegalArgumentException if the tilt is smaller than -90 or greater than 90. + * + * @return this builder, to allow for chaining of calls + * + * @see VirtualStylusMotionEvent#getTiltY + * @see <a href="https://source.android.com/docs/core/interaction/input/touch-devices#orientation-and-tilt-fields"> + * Stylus tilt and orientation</a> + */ + @NonNull + public Builder setTiltY(@IntRange(from = TILT_MIN, to = TILT_MAX) int tiltY) { + validateTilt(tiltY); + mTiltY = tiltY; + return this; + } + + /** + * Sets the time (in nanoseconds) when this specific event was generated. This may be + * obtained from {@link SystemClock#uptimeMillis()} (with nanosecond precision instead of + * millisecond), but can be different depending on the use case. + * This field is optional and can be omitted. + * + * @return this builder, to allow for chaining of calls + * @see InputEvent#getEventTime() + */ + @NonNull + public Builder setEventTimeNanos(long eventTimeNanos) { + if (eventTimeNanos < 0L) { + throw new IllegalArgumentException("Event time cannot be negative"); + } + mEventTimeNanos = eventTimeNanos; + return this; + } + + private void validateTilt(int tilt) { + if (tilt < TILT_MIN || tilt > TILT_MAX) { + throw new IllegalArgumentException( + "Tilt must be between " + TILT_MIN + " and " + TILT_MAX); + } + } + } + + @NonNull + public static final Parcelable.Creator<VirtualStylusMotionEvent> CREATOR = + new Parcelable.Creator<>() { + public VirtualStylusMotionEvent createFromParcel(Parcel source) { + return new VirtualStylusMotionEvent(source); + } + public VirtualStylusMotionEvent[] newArray(int size) { + return new VirtualStylusMotionEvent[size]; + } + }; +} diff --git a/core/java/android/hardware/input/VirtualTouchDeviceConfig.java b/core/java/android/hardware/input/VirtualTouchDeviceConfig.java new file mode 100644 index 000000000000..2e2cfab0eee2 --- /dev/null +++ b/core/java/android/hardware/input/VirtualTouchDeviceConfig.java @@ -0,0 +1,103 @@ +/* + * Copyright 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.hardware.input; + +import android.annotation.IntRange; +import android.annotation.NonNull; +import android.os.Parcel; + +/** + * Configurations to create a virtual touch-based device. + * + * @hide + */ +abstract class VirtualTouchDeviceConfig extends VirtualInputDeviceConfig { + + /** The touch device width. */ + private final int mWidth; + /** The touch device height. */ + private final int mHeight; + + VirtualTouchDeviceConfig(@NonNull Builder<? extends Builder<?>> builder) { + super(builder); + mWidth = builder.mWidth; + mHeight = builder.mHeight; + } + + VirtualTouchDeviceConfig(@NonNull Parcel in) { + super(in); + mWidth = in.readInt(); + mHeight = in.readInt(); + } + + /** Returns the touch device width. */ + public int getWidth() { + return mWidth; + } + + /** Returns the touch device height. */ + public int getHeight() { + return mHeight; + } + + @Override + void writeToParcel(@NonNull Parcel dest, int flags) { + super.writeToParcel(dest, flags); + dest.writeInt(mWidth); + dest.writeInt(mHeight); + } + + @Override + @NonNull + String additionalFieldsToString() { + return " width=" + mWidth + " height=" + mHeight; + } + + /** + * Builder for creating a {@link VirtualTouchDeviceConfig}. + * + * @param <T> The subclass to be built. + */ + abstract static class Builder<T extends Builder<T>> + extends VirtualInputDeviceConfig.Builder<T> { + + private final int mWidth; + private final int mHeight; + + /** + * Creates a new instance for the given dimensions of the touch device. + * + * <p>The dimensions are not pixels but in the screen's raw coordinate space. They do + * not necessarily have to correspond to the display size or aspect ratio. In this case the + * framework will handle the scaling appropriately. + * + * @param touchDeviceWidth The width of the touch device. + * @param touchDeviceHeight The height of the touch device. + */ + Builder(@IntRange(from = 1) int touchDeviceWidth, + @IntRange(from = 1) int touchDeviceHeight) { + if (touchDeviceHeight <= 0 || touchDeviceWidth <= 0) { + throw new IllegalArgumentException( + "Cannot create a virtual touch-based device, dimensions must be " + + "positive. Got: (" + touchDeviceHeight + ", " + + touchDeviceWidth + ")"); + } + mHeight = touchDeviceHeight; + mWidth = touchDeviceWidth; + } + } +} diff --git a/core/java/android/hardware/input/VirtualTouchscreenConfig.java b/core/java/android/hardware/input/VirtualTouchscreenConfig.java index 63084592a2e8..851cee6e51d2 100644 --- a/core/java/android/hardware/input/VirtualTouchscreenConfig.java +++ b/core/java/android/hardware/input/VirtualTouchscreenConfig.java @@ -23,38 +23,19 @@ import android.os.Parcel; import android.os.Parcelable; /** - * Configurations to create virtual touchscreen. + * Configurations to create a virtual touchscreen. * * @hide */ @SystemApi -public final class VirtualTouchscreenConfig extends VirtualInputDeviceConfig implements Parcelable { - - /** The touchscreen width. */ - private final int mWidth; - /** The touchscreen height. */ - private final int mHeight; +public final class VirtualTouchscreenConfig extends VirtualTouchDeviceConfig implements Parcelable { private VirtualTouchscreenConfig(@NonNull Builder builder) { super(builder); - mWidth = builder.mWidth; - mHeight = builder.mHeight; } private VirtualTouchscreenConfig(@NonNull Parcel in) { super(in); - mWidth = in.readInt(); - mHeight = in.readInt(); - } - - /** Returns the touchscreen width. */ - public int getWidth() { - return mWidth; - } - - /** Returns the touchscreen height. */ - public int getHeight() { - return mHeight; } @Override @@ -65,19 +46,11 @@ public final class VirtualTouchscreenConfig extends VirtualInputDeviceConfig imp @Override public void writeToParcel(@NonNull Parcel dest, int flags) { super.writeToParcel(dest, flags); - dest.writeInt(mWidth); - dest.writeInt(mHeight); - } - - @Override - @NonNull - String additionalFieldsToString() { - return " width=" + mWidth + " height=" + mHeight; } @NonNull public static final Creator<VirtualTouchscreenConfig> CREATOR = - new Creator<VirtualTouchscreenConfig>() { + new Creator<>() { @Override public VirtualTouchscreenConfig createFromParcel(Parcel in) { return new VirtualTouchscreenConfig(in); @@ -92,9 +65,7 @@ public final class VirtualTouchscreenConfig extends VirtualInputDeviceConfig imp /** * Builder for creating a {@link VirtualTouchscreenConfig}. */ - public static final class Builder extends VirtualInputDeviceConfig.Builder<Builder> { - private int mWidth; - private int mHeight; + public static final class Builder extends VirtualTouchDeviceConfig.Builder<Builder> { /** * Creates a new instance for the given dimensions of the {@link VirtualTouchscreen}. @@ -108,14 +79,7 @@ public final class VirtualTouchscreenConfig extends VirtualInputDeviceConfig imp */ public Builder(@IntRange(from = 1) int touchscreenWidth, @IntRange(from = 1) int touchscreenHeight) { - if (touchscreenHeight <= 0 || touchscreenWidth <= 0) { - throw new IllegalArgumentException( - "Cannot create a virtual touchscreen, touchscreen dimensions must be " - + "positive. Got: (" + touchscreenHeight + ", " - + touchscreenWidth + ")"); - } - mHeight = touchscreenHeight; - mWidth = touchscreenWidth; + super(touchscreenWidth, touchscreenHeight); } /** diff --git a/core/java/android/hardware/radio/TEST_MAPPING b/core/java/android/hardware/radio/TEST_MAPPING new file mode 100644 index 000000000000..ee4eeb634c84 --- /dev/null +++ b/core/java/android/hardware/radio/TEST_MAPPING @@ -0,0 +1,7 @@ +{ + "imports": [ + { + "path": "frameworks/base/core/tests/BroadcastRadioTests" + } + ] +} diff --git a/core/java/android/os/BatteryManager.java b/core/java/android/os/BatteryManager.java index 25fba60b9bb5..b9bb059bef42 100644 --- a/core/java/android/os/BatteryManager.java +++ b/core/java/android/os/BatteryManager.java @@ -17,9 +17,11 @@ package android.os; import static android.os.Flags.FLAG_STATE_OF_HEALTH_PUBLIC; +import static android.os.Flags.FLAG_BATTERY_PART_STATUS_API; import android.Manifest.permission; import android.annotation.FlaggedApi; +import android.annotation.Nullable; import android.annotation.RequiresPermission; import android.annotation.SuppressLint; import android.annotation.SystemApi; @@ -236,6 +238,31 @@ public class BatteryManager { public static final int CHARGING_POLICY_ADAPTIVE_LONGLIFE = OsProtoEnums.CHARGING_POLICY_ADAPTIVE_LONGLIFE; // = 4 + // values for "battery part status" property + /** + * Battery part status is not supported. + * @hide + */ + @SystemApi + @FlaggedApi(FLAG_BATTERY_PART_STATUS_API) + public static final int PART_STATUS_UNSUPPORTED = 0; + + /** + * Battery is the original device battery. + * @hide + */ + @SystemApi + @FlaggedApi(FLAG_BATTERY_PART_STATUS_API) + public static final int PART_STATUS_ORIGINAL = 1; + + /** + * Battery has been replaced. + * @hide + */ + @SystemApi + @FlaggedApi(FLAG_BATTERY_PART_STATUS_API) + public static final int PART_STATUS_REPLACED = 2; + /** @hide */ @SuppressLint("UnflaggedApi") // TestApi without associated feature. @TestApi @@ -366,6 +393,32 @@ public class BatteryManager { @FlaggedApi(FLAG_STATE_OF_HEALTH_PUBLIC) public static final int BATTERY_PROPERTY_STATE_OF_HEALTH = 10; + /** + * Battery part serial number. + * + * <p class="note"> + * The sender must hold the {@link android.Manifest.permission#BATTERY_STATS} permission. + * + * @hide + */ + @RequiresPermission(permission.BATTERY_STATS) + @SystemApi + @FlaggedApi(FLAG_BATTERY_PART_STATUS_API) + public static final int BATTERY_PROPERTY_SERIAL_NUMBER = 11; + + /** + * Battery part status from a BATTERY_PART_STATUS_* value. + * + * <p class="note"> + * The sender must hold the {@link android.Manifest.permission#BATTERY_STATS} permission. + * + * @hide + */ + @RequiresPermission(permission.BATTERY_STATS) + @SystemApi + @FlaggedApi(FLAG_BATTERY_PART_STATUS_API) + public static final int BATTERY_PROPERTY_PART_STATUS = 12; + private final Context mContext; private final IBatteryStats mBatteryStats; private final IBatteryPropertiesRegistrar mBatteryPropertiesRegistrar; @@ -431,6 +484,25 @@ public class BatteryManager { } /** + * Same as queryProperty, but for strings. + */ + private String queryStringProperty(int id) { + if (mBatteryPropertiesRegistrar == null) { + return null; + } + + try { + BatteryProperty prop = new BatteryProperty(); + if (mBatteryPropertiesRegistrar.getProperty(id, prop) == 0) { + return prop.getString(); + } + return null; + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** * Return the value of a battery property of integer type. * * @param id identifier of the requested property @@ -464,6 +536,21 @@ public class BatteryManager { } /** + * Return the value of a battery property of String type. If the + * platform does not provide the property queried, this value will + * be null. + * + * @param id identifier of the requested property. + * + * @return the property value, or null if not supported. + */ + @Nullable + @FlaggedApi(FLAG_BATTERY_PART_STATUS_API) + public String getStringProperty(int id) { + return queryStringProperty(id); + } + + /** * Return true if the plugType given is wired * @param plugType {@link #BATTERY_PLUGGED_AC}, {@link #BATTERY_PLUGGED_USB}, * or {@link #BATTERY_PLUGGED_WIRELESS} diff --git a/core/java/android/os/BatteryProperty.java b/core/java/android/os/BatteryProperty.java index b40988a938bc..464577f58241 100644 --- a/core/java/android/os/BatteryProperty.java +++ b/core/java/android/os/BatteryProperty.java @@ -28,12 +28,14 @@ import android.os.Parcelable; */ public class BatteryProperty implements Parcelable { private long mValueLong; + private String mValueString; /** * @hide */ public BatteryProperty() { mValueLong = Long.MIN_VALUE; + mValueString = null; } /** @@ -46,14 +48,23 @@ public class BatteryProperty implements Parcelable { /** * @hide */ + public String getString() { + return mValueString; + } + + /** + * @hide + */ public void setLong(long val) { mValueLong = val; } - /* - * Parcel read/write code must be kept in sync with - * frameworks/native/services/batteryservice/BatteryProperty.cpp + /** + * @hide */ + public void setString(String val) { + mValueString = val; + } private BatteryProperty(Parcel p) { readFromParcel(p); @@ -61,10 +72,12 @@ public class BatteryProperty implements Parcelable { public void readFromParcel(Parcel p) { mValueLong = p.readLong(); + mValueString = p.readString8(); } public void writeToParcel(Parcel p, int flags) { p.writeLong(mValueLong); + p.writeString8(mValueString); } public static final @android.annotation.NonNull Parcelable.Creator<BatteryProperty> CREATOR diff --git a/core/java/android/os/BugreportParams.java b/core/java/android/os/BugreportParams.java index 8510084c309d..f2ef185a0500 100644 --- a/core/java/android/os/BugreportParams.java +++ b/core/java/android/os/BugreportParams.java @@ -21,6 +21,7 @@ import android.annotation.IntDef; import android.annotation.SystemApi; import android.annotation.TestApi; import android.app.admin.flags.Flags; +import android.compat.annotation.UnsupportedAppUsage; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -131,6 +132,7 @@ public final class BugreportParams { */ @TestApi @FlaggedApi(Flags.FLAG_ONBOARDING_BUGREPORT_V2_ENABLED) + @UnsupportedAppUsage public static final int BUGREPORT_MODE_ONBOARDING = IDumpstate.BUGREPORT_MODE_ONBOARDING; /** diff --git a/core/java/android/os/PerformanceHintManager.java b/core/java/android/os/PerformanceHintManager.java index 746278fc296c..e6bfcd728a52 100644 --- a/core/java/android/os/PerformanceHintManager.java +++ b/core/java/android/os/PerformanceHintManager.java @@ -183,13 +183,14 @@ public final class PerformanceHintManager { /** @hide */ @Retention(RetentionPolicy.SOURCE) - @IntDef(prefix = {"CPU_LOAD_"}, value = { + @IntDef(prefix = {"CPU_LOAD_", "GPU_LOAD_"}, value = { CPU_LOAD_UP, CPU_LOAD_DOWN, CPU_LOAD_RESET, CPU_LOAD_RESUME, GPU_LOAD_UP, - GPU_LOAD_DOWN + GPU_LOAD_DOWN, + GPU_LOAD_RESET }) public @interface Hint {} diff --git a/core/java/android/os/flags.aconfig b/core/java/android/os/flags.aconfig index 0db90bff48fd..82518bfbfd8d 100644 --- a/core/java/android/os/flags.aconfig +++ b/core/java/android/os/flags.aconfig @@ -106,3 +106,11 @@ flag { bug: "305311707" is_fixed_read_only: true } + +flag { + name: "battery_part_status_api" + namespace: "phoenix" + description: "Feature flag for adding Health HAL v3 APIs." + is_fixed_read_only: true + bug: "309792384" +} diff --git a/core/java/android/os/storage/StorageManagerInternal.java b/core/java/android/os/storage/StorageManagerInternal.java index 8961846728a6..6995ea87ebe3 100644 --- a/core/java/android/os/storage/StorageManagerInternal.java +++ b/core/java/android/os/storage/StorageManagerInternal.java @@ -193,7 +193,7 @@ public abstract class StorageManagerInternal { * @see com.android.server.pm.Installer#createFsveritySetupAuthToken() */ public abstract IInstalld.IFsveritySetupAuthToken createFsveritySetupAuthToken( - ParcelFileDescriptor authFd, int appUid, @UserIdInt int userId) throws IOException; + ParcelFileDescriptor authFd, int uid) throws IOException; /** * A proxy call to the corresponding method in Installer. diff --git a/core/java/android/permission/IPermissionManager.aidl b/core/java/android/permission/IPermissionManager.aidl index 7cecfdca851a..471f95bb21f3 100644 --- a/core/java/android/permission/IPermissionManager.aidl +++ b/core/java/android/permission/IPermissionManager.aidl @@ -92,7 +92,7 @@ interface IPermissionManager { boolean isAutoRevokeExempted(String packageName, int userId); - void registerAttributionSource(in AttributionSourceState source); + IBinder registerAttributionSource(in AttributionSourceState source); boolean isRegisteredAttributionSource(in AttributionSourceState source); diff --git a/core/java/android/permission/PermissionManager.java b/core/java/android/permission/PermissionManager.java index 91adc37cb654..4af6e3a9f8d4 100644 --- a/core/java/android/permission/PermissionManager.java +++ b/core/java/android/permission/PermissionManager.java @@ -23,6 +23,7 @@ import static android.content.pm.PackageManager.FLAG_PERMISSION_SYSTEM_FIXED; import static android.content.pm.PackageManager.FLAG_PERMISSION_USER_FIXED; import static android.content.pm.PackageManager.FLAG_PERMISSION_USER_SET; import static android.os.Build.VERSION_CODES.S; +import static android.permission.flags.Flags.serverSideAttributionRegistration; import android.Manifest; import android.annotation.CheckResult; @@ -59,6 +60,7 @@ import android.media.AudioManager; import android.os.Binder; import android.os.Build; import android.os.Handler; +import android.os.IBinder; import android.os.Looper; import android.os.Message; import android.os.Process; @@ -1464,13 +1466,19 @@ public final class PermissionManager { // We use a shared static token for sources that are not registered since the token's // only used for process death detection. If we are about to use the source for security // enforcement we need to replace the binder with a unique one. - final AttributionSource registeredSource = source.withToken(new Binder()); try { - mPermissionManager.registerAttributionSource(registeredSource.asState()); + if (serverSideAttributionRegistration()) { + IBinder newToken = mPermissionManager.registerAttributionSource(source.asState()); + return source.withToken(newToken); + } else { + AttributionSource registeredSource = source.withToken(new Binder()); + mPermissionManager.registerAttributionSource(registeredSource.asState()); + return registeredSource; + } } catch (RemoteException e) { e.rethrowFromSystemServer(); } - return registeredSource; + return source; } /** diff --git a/core/java/android/permission/flags.aconfig b/core/java/android/permission/flags.aconfig index 60143cc79d2d..db8f52c307a1 100644 --- a/core/java/android/permission/flags.aconfig +++ b/core/java/android/permission/flags.aconfig @@ -45,7 +45,8 @@ flag { } flag { - name: "enhanced_confirmation_mode_apis" + name: "enhanced_confirmation_mode_apis_enabled" + is_fixed_read_only: true namespace: "permissions" description: "enable enhanced confirmation mode apis" bug: "310220212" @@ -73,8 +74,22 @@ flag { } flag { + name: "server_side_attribution_registration" + namespace: "permissions" + description: "controls whether the binder representing an AttributionSource is created in the system server, or client process" + bug: "310953959" +} + +flag { name: "wallet_role_enabled" namespace: "wallet_integration" description: "This flag is used to enabled the Wallet Role for all users on the device" bug: "283989236" } + +flag { + name: "runtime_permission_appops_mapping" + namespace: "permissions" + description: "Use runtime permission state to determine appop state" + bug: "266164193" +} diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index f974ef43a9dc..58159c2b5693 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -1931,9 +1931,7 @@ public final class Settings { * A matching Activity will only be found if * {@link NotificationManager#areAutomaticZenRulesUserManaged()} is true. * <p> - * Input: Intent's data URI set with an application name, using the "package" schema (like - * "package:com.my.app"). - * Input: The id of the rule, provided in {@link #EXTRA_AUTOMATIC_ZEN_RULE_ID}. + * Input: The id of the rule, provided in the {@link #EXTRA_AUTOMATIC_ZEN_RULE_ID} extra. * <p> * Output: Nothing. */ @@ -3178,15 +3176,7 @@ public final class Settings { } public void destroy() { - try { - // If this process is the system server process, mArray is the same object as - // the memory int array kept inside SettingsProvider, so skipping the close() - if (!Settings.isInSystemServer() && !mArray.isClosed()) { - mArray.close(); - } - } catch (IOException e) { - Log.e(TAG, "Error closing backing array", e); - } + maybeCloseGenerationArray(mArray); } @Override @@ -3199,6 +3189,21 @@ public final class Settings { } } + private static void maybeCloseGenerationArray(@Nullable MemoryIntArray array) { + if (array == null) { + return; + } + try { + // If this process is the system server process, the MemoryIntArray received from Parcel + // is the same object as the one kept inside SettingsProvider, so skipping the close(). + if (!Settings.isInSystemServer() && !array.isClosed()) { + array.close(); + } + } catch (IOException e) { + Log.e(TAG, "Error closing the generation tracking array", e); + } + } + private static final class ContentProviderHolder { private final Object mLock = new Object(); @@ -3498,6 +3503,8 @@ public final class Settings { mGenerationTrackers.put(name, new GenerationTracker(name, array, index, generation, mGenerationTrackerErrorHandler)); + } else { + maybeCloseGenerationArray(array); } } if (mGenerationTrackers.get(name) != null @@ -3735,6 +3742,8 @@ public final class Settings { new GenerationTracker(prefix, array, index, generation, mGenerationTrackerErrorHandler)); currentGeneration = generation; + } else { + maybeCloseGenerationArray(array); } } if (mGenerationTrackers.get(prefix) != null && currentGeneration @@ -7312,6 +7321,28 @@ public final class Settings { "bluetooth_le_broadcast_app_source_name"; /** + * This is used by LocalBluetoothLeBroadcast to downgrade the broadcast quality to improve + * compatibility. + * + * <ul> + * <li>0 = false + * <li>1 = true + * </ul> + * + * @hide + */ + public static final String BLUETOOTH_LE_BROADCAST_IMPROVE_COMPATIBILITY = + "bluetooth_le_broadcast_improve_compatibility"; + + /** + * This is used by LocalBluetoothLeBroadcast to store the fallback active device address. + * + * @hide + */ + public static final String BLUETOOTH_LE_BROADCAST_FALLBACK_ACTIVE_DEVICE_ADDRESS = + "bluetooth_le_broadcast_fallback_active_device_address"; + + /** * Ringtone routing value for hearing aid. It routes ringtone to hearing aid or device * speaker. * <ul> diff --git a/core/java/android/provider/TEST_MAPPING b/core/java/android/provider/TEST_MAPPING index 8b4a99e38299..d5ac7a7de461 100644 --- a/core/java/android/provider/TEST_MAPPING +++ b/core/java/android/provider/TEST_MAPPING @@ -28,5 +28,10 @@ } ] } + ], + "postsubmit": [ + { + "name": "CtsDeviceConfigTestCases" + } ] } diff --git a/core/java/android/security/flags.aconfig b/core/java/android/security/flags.aconfig index 30524a1132fa..1994058441d5 100644 --- a/core/java/android/security/flags.aconfig +++ b/core/java/android/security/flags.aconfig @@ -53,7 +53,7 @@ flag { flag { name: "frp_enforcement" - namespace: "android_hw_security" + namespace: "hardware_backed_security" description: "This flag controls whether PDB enforces FRP" bug: "290312729" is_fixed_read_only: true diff --git a/core/java/android/service/notification/ZenDeviceEffects.java b/core/java/android/service/notification/ZenDeviceEffects.java index 03ebae5c5199..90049e6a934a 100644 --- a/core/java/android/service/notification/ZenDeviceEffects.java +++ b/core/java/android/service/notification/ZenDeviceEffects.java @@ -20,7 +20,6 @@ import android.annotation.FlaggedApi; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; -import android.annotation.TestApi; import android.app.Flags; import android.os.Parcel; import android.os.Parcelable; @@ -37,8 +36,8 @@ import java.util.Objects; @FlaggedApi(Flags.FLAG_MODES_API) public final class ZenDeviceEffects implements Parcelable { - /** Used to track which rule variables have been modified by the user. - * Should be checked against the bitmask {@link #getUserModifiedFields()}. + /** + * Enum for the user-modifiable fields in this object. * @hide */ @IntDef(flag = true, prefix = { "FIELD_" }, value = { @@ -59,52 +58,42 @@ public final class ZenDeviceEffects implements Parcelable { /** * @hide */ - @TestApi public static final int FIELD_GRAYSCALE = 1 << 0; /** * @hide */ - @TestApi public static final int FIELD_SUPPRESS_AMBIENT_DISPLAY = 1 << 1; /** * @hide */ - @TestApi public static final int FIELD_DIM_WALLPAPER = 1 << 2; /** * @hide */ - @TestApi public static final int FIELD_NIGHT_MODE = 1 << 3; /** * @hide */ - @TestApi public static final int FIELD_DISABLE_AUTO_BRIGHTNESS = 1 << 4; /** * @hide */ - @TestApi public static final int FIELD_DISABLE_TAP_TO_WAKE = 1 << 5; /** * @hide */ - @TestApi public static final int FIELD_DISABLE_TILT_TO_WAKE = 1 << 6; /** * @hide */ - @TestApi public static final int FIELD_DISABLE_TOUCH = 1 << 7; /** * @hide */ - @TestApi public static final int FIELD_MINIMIZE_RADIO_USAGE = 1 << 8; /** * @hide */ - @TestApi public static final int FIELD_MAXIMIZE_DOZE = 1 << 9; private final boolean mGrayscale; @@ -119,13 +108,10 @@ public final class ZenDeviceEffects implements Parcelable { private final boolean mMinimizeRadioUsage; private final boolean mMaximizeDoze; - private final @ModifiableField int mUserModifiedFields; // Bitwise representation - private ZenDeviceEffects(boolean grayscale, boolean suppressAmbientDisplay, boolean dimWallpaper, boolean nightMode, boolean disableAutoBrightness, boolean disableTapToWake, boolean disableTiltToWake, boolean disableTouch, - boolean minimizeRadioUsage, boolean maximizeDoze, - @ModifiableField int userModifiedFields) { + boolean minimizeRadioUsage, boolean maximizeDoze) { mGrayscale = grayscale; mSuppressAmbientDisplay = suppressAmbientDisplay; mDimWallpaper = dimWallpaper; @@ -136,7 +122,6 @@ public final class ZenDeviceEffects implements Parcelable { mDisableTouch = disableTouch; mMinimizeRadioUsage = minimizeRadioUsage; mMaximizeDoze = maximizeDoze; - mUserModifiedFields = userModifiedFields; } @Override @@ -153,15 +138,14 @@ public final class ZenDeviceEffects implements Parcelable { && this.mDisableTiltToWake == that.mDisableTiltToWake && this.mDisableTouch == that.mDisableTouch && this.mMinimizeRadioUsage == that.mMinimizeRadioUsage - && this.mMaximizeDoze == that.mMaximizeDoze - && this.mUserModifiedFields == that.mUserModifiedFields; + && this.mMaximizeDoze == that.mMaximizeDoze; } @Override public int hashCode() { return Objects.hash(mGrayscale, mSuppressAmbientDisplay, mDimWallpaper, mNightMode, mDisableAutoBrightness, mDisableTapToWake, mDisableTiltToWake, mDisableTouch, - mMinimizeRadioUsage, mMaximizeDoze, mUserModifiedFields); + mMinimizeRadioUsage, mMaximizeDoze); } @Override @@ -177,11 +161,11 @@ public final class ZenDeviceEffects implements Parcelable { if (mDisableTouch) effects.add("disableTouch"); if (mMinimizeRadioUsage) effects.add("minimizeRadioUsage"); if (mMaximizeDoze) effects.add("maximizeDoze"); - return "[" + String.join(", ", effects) + "]" - + " userModifiedFields: " + modifiedFieldsToString(mUserModifiedFields); + return "[" + String.join(", ", effects) + "]"; } - private String modifiedFieldsToString(int bitmask) { + /** @hide */ + public static String fieldsToString(@ModifiableField int bitmask) { ArrayList<String> modified = new ArrayList<>(); if ((bitmask & FIELD_GRAYSCALE) != 0) { modified.add("FIELD_GRAYSCALE"); @@ -312,7 +296,7 @@ public final class ZenDeviceEffects implements Parcelable { return new ZenDeviceEffects(in.readBoolean(), in.readBoolean(), in.readBoolean(), in.readBoolean(), in.readBoolean(), in.readBoolean(), in.readBoolean(), in.readBoolean(), in.readBoolean(), - in.readBoolean(), in.readInt()); + in.readBoolean()); } @Override @@ -321,16 +305,6 @@ public final class ZenDeviceEffects implements Parcelable { } }; - /** - * Gets the bitmask representing which fields are user modified. Bits are set using - * {@link ModifiableField}. - * @hide - */ - @TestApi - public @ModifiableField int getUserModifiedFields() { - return mUserModifiedFields; - } - @Override public int describeContents() { return 0; @@ -348,7 +322,6 @@ public final class ZenDeviceEffects implements Parcelable { dest.writeBoolean(mDisableTouch); dest.writeBoolean(mMinimizeRadioUsage); dest.writeBoolean(mMaximizeDoze); - dest.writeInt(mUserModifiedFields); } /** Builder class for {@link ZenDeviceEffects} objects. */ @@ -365,7 +338,6 @@ public final class ZenDeviceEffects implements Parcelable { private boolean mDisableTouch; private boolean mMinimizeRadioUsage; private boolean mMaximizeDoze; - private @ModifiableField int mUserModifiedFields; /** * Instantiates a new {@link ZenPolicy.Builder} with all effects set to default (disabled). @@ -388,7 +360,6 @@ public final class ZenDeviceEffects implements Parcelable { mDisableTouch = zenDeviceEffects.shouldDisableTouch(); mMinimizeRadioUsage = zenDeviceEffects.shouldMinimizeRadioUsage(); mMaximizeDoze = zenDeviceEffects.shouldMaximizeDoze(); - mUserModifiedFields = zenDeviceEffects.mUserModifiedFields; } /** @@ -510,24 +481,13 @@ public final class ZenDeviceEffects implements Parcelable { return this; } - /** - * Sets the bitmask representing which fields are user modified. See the FIELD_ constants. - * @hide - */ - @TestApi - @NonNull - public Builder setUserModifiedFields(@ModifiableField int userModifiedFields) { - mUserModifiedFields = userModifiedFields; - return this; - } - /** Builds a {@link ZenDeviceEffects} object based on the builder's state. */ @NonNull public ZenDeviceEffects build() { return new ZenDeviceEffects(mGrayscale, mSuppressAmbientDisplay, mDimWallpaper, mNightMode, mDisableAutoBrightness, mDisableTapToWake, mDisableTiltToWake, mDisableTouch, mMinimizeRadioUsage, - mMaximizeDoze, mUserModifiedFields); + mMaximizeDoze); } } } diff --git a/core/java/android/service/notification/ZenModeConfig.java b/core/java/android/service/notification/ZenModeConfig.java index 54248be74e04..c479877fe98e 100644 --- a/core/java/android/service/notification/ZenModeConfig.java +++ b/core/java/android/service/notification/ZenModeConfig.java @@ -205,8 +205,8 @@ public class ZenModeConfig implements Parcelable { private static final String ALLOW_ATT_SCREEN_ON = "visualScreenOn"; private static final String ALLOW_ATT_CONV = "convos"; private static final String ALLOW_ATT_CONV_FROM = "convosFrom"; - private static final String ALLOW_ATT_CHANNELS = "channels"; - private static final String USER_MODIFIED_FIELDS = "policyUserModifiedFields"; + private static final String ALLOW_ATT_CHANNELS = "priorityChannels"; + private static final String POLICY_USER_MODIFIED_FIELDS = "policyUserModifiedFields"; private static final String DISALLOW_TAG = "disallow"; private static final String DISALLOW_ATT_VISUAL_EFFECTS = "visualEffects"; private static final String STATE_TAG = "state"; @@ -806,6 +806,9 @@ public class ZenModeConfig implements Parcelable { rt.triggerDescription = parser.getAttributeValue(null, RULE_ATT_TRIGGER_DESC); rt.type = safeInt(parser, RULE_ATT_TYPE, AutomaticZenRule.TYPE_UNKNOWN); rt.userModifiedFields = safeInt(parser, RULE_ATT_USER_MODIFIED_FIELDS, 0); + rt.zenPolicyUserModifiedFields = safeInt(parser, POLICY_USER_MODIFIED_FIELDS, 0); + rt.zenDeviceEffectsUserModifiedFields = safeInt(parser, + DEVICE_EFFECT_USER_MODIFIED_FIELDS, 0); Long deletionInstant = tryParseLong( parser.getAttributeValue(null, RULE_ATT_DELETION_INSTANT), null); if (deletionInstant != null) { @@ -858,6 +861,9 @@ public class ZenModeConfig implements Parcelable { } out.attributeInt(null, RULE_ATT_TYPE, rule.type); out.attributeInt(null, RULE_ATT_USER_MODIFIED_FIELDS, rule.userModifiedFields); + out.attributeInt(null, POLICY_USER_MODIFIED_FIELDS, rule.zenPolicyUserModifiedFields); + out.attributeInt(null, DEVICE_EFFECT_USER_MODIFIED_FIELDS, + rule.zenDeviceEffectsUserModifiedFields); if (rule.deletionInstant != null) { out.attributeLong(null, RULE_ATT_DELETION_INSTANT, rule.deletionInstant.toEpochMilli()); @@ -919,12 +925,11 @@ public class ZenModeConfig implements Parcelable { final int events = safeInt(parser, ALLOW_ATT_EVENTS, ZenPolicy.STATE_UNSET); final int reminders = safeInt(parser, ALLOW_ATT_REMINDERS, ZenPolicy.STATE_UNSET); if (Flags.modesApi()) { - final int channels = safeInt(parser, ALLOW_ATT_CHANNELS, ZenPolicy.CHANNEL_TYPE_UNSET); - if (channels != ZenPolicy.CHANNEL_TYPE_UNSET) { - builder.allowChannels(channels); + final int channels = safeInt(parser, ALLOW_ATT_CHANNELS, ZenPolicy.STATE_UNSET); + if (channels != ZenPolicy.STATE_UNSET) { + builder.allowPriorityChannels(channels == ZenPolicy.STATE_ALLOW); policySet = true; } - builder.setUserModifiedFields(safeInt(parser, USER_MODIFIED_FIELDS, 0)); } if (calls != ZenPolicy.PEOPLE_TYPE_UNSET) { @@ -1036,8 +1041,7 @@ public class ZenModeConfig implements Parcelable { out); if (Flags.modesApi()) { - writeZenPolicyState(ALLOW_ATT_CHANNELS, policy.getAllowedChannels(), out); - out.attributeInt(null, USER_MODIFIED_FIELDS, policy.getUserModifiedFields()); + writeZenPolicyState(ALLOW_ATT_CHANNELS, policy.getPriorityChannels(), out); } } @@ -1053,7 +1057,7 @@ public class ZenModeConfig implements Parcelable { out.attributeInt(null, attr, val); } } else if (Flags.modesApi() && Objects.equals(attr, ALLOW_ATT_CHANNELS)) { - if (val != ZenPolicy.CHANNEL_TYPE_UNSET) { + if (val != ZenPolicy.STATE_UNSET) { out.attributeInt(null, attr, val); } } else { @@ -1083,7 +1087,6 @@ public class ZenModeConfig implements Parcelable { .setShouldMinimizeRadioUsage( safeBoolean(parser, DEVICE_EFFECT_MINIMIZE_RADIO_USAGE, false)) .setShouldMaximizeDoze(safeBoolean(parser, DEVICE_EFFECT_MAXIMIZE_DOZE, false)) - .setUserModifiedFields(safeInt(parser, DEVICE_EFFECT_USER_MODIFIED_FIELDS, 0)) .build(); return deviceEffects.hasEffects() ? deviceEffects : null; @@ -1108,8 +1111,6 @@ public class ZenModeConfig implements Parcelable { writeBooleanIfTrue(out, DEVICE_EFFECT_MINIMIZE_RADIO_USAGE, deviceEffects.shouldMinimizeRadioUsage()); writeBooleanIfTrue(out, DEVICE_EFFECT_MAXIMIZE_DOZE, deviceEffects.shouldMaximizeDoze()); - out.attributeInt(null, DEVICE_EFFECT_USER_MODIFIED_FIELDS, - deviceEffects.getUserModifiedFields()); } private static void writeBooleanIfTrue(TypedXmlSerializer out, String att, boolean value) @@ -1238,8 +1239,7 @@ public class ZenModeConfig implements Parcelable { } if (Flags.modesApi()) { - builder.allowChannels(allowPriorityChannels ? ZenPolicy.CHANNEL_TYPE_PRIORITY - : ZenPolicy.CHANNEL_TYPE_NONE); + builder.allowPriorityChannels(allowPriorityChannels); } return builder.build(); } @@ -1369,7 +1369,7 @@ public class ZenModeConfig implements Parcelable { int state = defaultPolicy.state; if (Flags.modesApi()) { state = Policy.policyState(defaultPolicy.hasPriorityChannels(), - getAllowPriorityChannelsWithDefault(zenPolicy.getAllowedChannels(), + ZenPolicy.stateToBoolean(zenPolicy.getPriorityChannels(), DEFAULT_ALLOW_PRIORITY_CHANNELS)); } @@ -1412,24 +1412,6 @@ public class ZenModeConfig implements Parcelable { } /** - * Gets whether priority channels are permitted by this channel type, with the specified - * default if the value is unset. This effectively converts the channel enum to a boolean, - * where "true" indicates priority channels are allowed to break through and "false" means - * they are not. - */ - public static boolean getAllowPriorityChannelsWithDefault( - @ZenPolicy.ChannelType int channelType, boolean defaultAllowChannels) { - switch (channelType) { - case ZenPolicy.CHANNEL_TYPE_PRIORITY: - return true; - case ZenPolicy.CHANNEL_TYPE_NONE: - return false; - default: - return defaultAllowChannels; - } - } - - /** * Maps NotificationManager.Policy senders type to ZenPolicy.PeopleType */ public static @ZenPolicy.PeopleType int getZenPolicySenders(int senders) { @@ -2060,7 +2042,9 @@ public class ZenModeConfig implements Parcelable { public String triggerDescription; public String iconResName; public boolean allowManualInvocation; - public int userModifiedFields; + @AutomaticZenRule.ModifiableField public int userModifiedFields; + @ZenPolicy.ModifiableField public int zenPolicyUserModifiedFields; + @ZenDeviceEffects.ModifiableField public int zenDeviceEffectsUserModifiedFields; @Nullable public Instant deletionInstant; // Only set on deleted rules. public ZenRule() { } @@ -2095,6 +2079,8 @@ public class ZenModeConfig implements Parcelable { triggerDescription = source.readString(); type = source.readInt(); userModifiedFields = source.readInt(); + zenPolicyUserModifiedFields = source.readInt(); + zenDeviceEffectsUserModifiedFields = source.readInt(); if (source.readInt() == 1) { deletionInstant = Instant.ofEpochMilli(source.readLong()); } @@ -2102,15 +2088,21 @@ public class ZenModeConfig implements Parcelable { } /** - * @see AutomaticZenRule#canUpdate() + * Whether this ZenRule can be updated by an app. In general, rules that have been + * customized by the user cannot be further updated by an app, with some exceptions: + * <ul> + * <li>Non user-configurable fields, like type, icon, configurationActivity, etc. + * <li>Name, if the name was not specifically modified by the user (to support language + * switches). + * </ul> */ @FlaggedApi(Flags.FLAG_MODES_API) public boolean canBeUpdatedByApp() { // The rule is considered updateable if its bitmask has no user modifications, and // the bitmasks of the policy and device effects have no modification. return userModifiedFields == 0 - && (zenPolicy == null || zenPolicy.getUserModifiedFields() == 0) - && (zenDeviceEffects == null || zenDeviceEffects.getUserModifiedFields() == 0); + && zenPolicyUserModifiedFields == 0 + && zenDeviceEffectsUserModifiedFields == 0; } @Override @@ -2158,6 +2150,8 @@ public class ZenModeConfig implements Parcelable { dest.writeString(triggerDescription); dest.writeInt(type); dest.writeInt(userModifiedFields); + dest.writeInt(zenPolicyUserModifiedFields); + dest.writeInt(zenDeviceEffectsUserModifiedFields); if (deletionInstant != null) { dest.writeInt(1); dest.writeLong(deletionInstant.toEpochMilli()); @@ -2192,8 +2186,20 @@ public class ZenModeConfig implements Parcelable { .append(",allowManualInvocation=").append(allowManualInvocation) .append(",iconResName=").append(iconResName) .append(",triggerDescription=").append(triggerDescription) - .append(",type=").append(type) - .append(",userModifiedFields=").append(userModifiedFields); + .append(",type=").append(type); + if (userModifiedFields != 0) { + sb.append(",userModifiedFields=") + .append(AutomaticZenRule.fieldsToString(userModifiedFields)); + } + if (zenPolicyUserModifiedFields != 0) { + sb.append(",zenPolicyUserModifiedFields=") + .append(ZenPolicy.fieldsToString(zenPolicyUserModifiedFields)); + } + if (zenDeviceEffectsUserModifiedFields != 0) { + sb.append(",zenDeviceEffectsUserModifiedFields=") + .append(ZenDeviceEffects.fieldsToString( + zenDeviceEffectsUserModifiedFields)); + } if (deletionInstant != null) { sb.append(",deletionInstant=").append(deletionInstant); } @@ -2257,6 +2263,9 @@ public class ZenModeConfig implements Parcelable { && Objects.equals(other.triggerDescription, triggerDescription) && other.type == type && other.userModifiedFields == userModifiedFields + && other.zenPolicyUserModifiedFields == zenPolicyUserModifiedFields + && other.zenDeviceEffectsUserModifiedFields + == zenDeviceEffectsUserModifiedFields && Objects.equals(other.deletionInstant, deletionInstant); } @@ -2269,7 +2278,8 @@ public class ZenModeConfig implements Parcelable { return Objects.hash(enabled, snoozing, name, zenMode, conditionId, condition, component, configurationActivity, pkg, id, enabler, zenPolicy, zenDeviceEffects, modified, allowManualInvocation, iconResName, - triggerDescription, type, userModifiedFields, deletionInstant); + triggerDescription, type, userModifiedFields, zenPolicyUserModifiedFields, + zenDeviceEffectsUserModifiedFields, deletionInstant); } return Objects.hash(enabled, snoozing, name, zenMode, conditionId, condition, component, configurationActivity, pkg, id, enabler, zenPolicy, modified); diff --git a/core/java/android/service/notification/ZenPolicy.java b/core/java/android/service/notification/ZenPolicy.java index 8477eb7120c2..fb491d010f54 100644 --- a/core/java/android/service/notification/ZenPolicy.java +++ b/core/java/android/service/notification/ZenPolicy.java @@ -45,8 +45,8 @@ import java.util.Objects; */ public final class ZenPolicy implements Parcelable { - /** Used to track which rule variables have been modified by the user. - * Should be checked against the bitmask {@link #getUserModifiedFields()}. + /** + * Enum for the user-modifiable fields in this object. * @hide */ @IntDef(flag = true, prefix = { "FIELD_" }, value = { @@ -76,7 +76,6 @@ public final class ZenPolicy implements Parcelable { * the same time. * @hide */ - @TestApi @FlaggedApi(Flags.FLAG_MODES_API) public static final int FIELD_MESSAGES = 1 << 0; /** @@ -84,7 +83,6 @@ public final class ZenPolicy implements Parcelable { * the same time. * @hide */ - @TestApi @FlaggedApi(Flags.FLAG_MODES_API) public static final int FIELD_CALLS = 1 << 1; /** @@ -92,13 +90,11 @@ public final class ZenPolicy implements Parcelable { * set at the same time. * @hide */ - @TestApi @FlaggedApi(Flags.FLAG_MODES_API) public static final int FIELD_CONVERSATIONS = 1 << 2; /** * @hide */ - @TestApi @FlaggedApi(Flags.FLAG_MODES_API) public static final int FIELD_ALLOW_CHANNELS = 1 << 3; /** @@ -109,73 +105,61 @@ public final class ZenPolicy implements Parcelable { /** * @hide */ - @TestApi @FlaggedApi(Flags.FLAG_MODES_API) public static final int FIELD_PRIORITY_CATEGORY_EVENTS = 1 << 5; /** * @hide */ - @TestApi @FlaggedApi(Flags.FLAG_MODES_API) public static final int FIELD_PRIORITY_CATEGORY_REPEAT_CALLERS = 1 << 6; /** * @hide */ - @TestApi @FlaggedApi(Flags.FLAG_MODES_API) public static final int FIELD_PRIORITY_CATEGORY_ALARMS = 1 << 7; /** * @hide */ - @TestApi @FlaggedApi(Flags.FLAG_MODES_API) public static final int FIELD_PRIORITY_CATEGORY_MEDIA = 1 << 8; /** * @hide */ - @TestApi @FlaggedApi(Flags.FLAG_MODES_API) public static final int FIELD_PRIORITY_CATEGORY_SYSTEM = 1 << 9; /** * @hide */ - @TestApi @FlaggedApi(Flags.FLAG_MODES_API) public static final int FIELD_VISUAL_EFFECT_FULL_SCREEN_INTENT = 1 << 10; /** * @hide */ - @TestApi @FlaggedApi(Flags.FLAG_MODES_API) public static final int FIELD_VISUAL_EFFECT_LIGHTS = 1 << 11; /** * @hide */ - @TestApi @FlaggedApi(Flags.FLAG_MODES_API) public static final int FIELD_VISUAL_EFFECT_PEEK = 1 << 12; /** * @hide */ - @TestApi @FlaggedApi(Flags.FLAG_MODES_API) public static final int FIELD_VISUAL_EFFECT_STATUS_BAR = 1 << 13; /** * @hide */ - @TestApi @FlaggedApi(Flags.FLAG_MODES_API) public static final int FIELD_VISUAL_EFFECT_BADGE = 1 << 14; /** * @hide */ - @TestApi @FlaggedApi(Flags.FLAG_MODES_API) public static final int FIELD_VISUAL_EFFECT_AMBIENT = 1 << 15; /** * @hide */ - @TestApi @FlaggedApi(Flags.FLAG_MODES_API) public static final int FIELD_VISUAL_EFFECT_NOTIFICATION_LIST = 1 << 16; @@ -184,8 +168,8 @@ public final class ZenPolicy implements Parcelable { private @PeopleType int mPriorityMessages = PEOPLE_TYPE_UNSET; private @PeopleType int mPriorityCalls = PEOPLE_TYPE_UNSET; private @ConversationSenders int mConversationSenders = CONVERSATION_SENDERS_UNSET; - private @ChannelType int mAllowChannels = CHANNEL_TYPE_UNSET; - private final @ModifiableField int mUserModifiedFields; // Bitwise representation + @FlaggedApi(Flags.FLAG_MODES_API) + private @ChannelType int mAllowChannels = CHANNEL_POLICY_UNSET; /** @hide */ @IntDef(prefix = { "PRIORITY_CATEGORY_" }, value = { @@ -354,56 +338,52 @@ public final class ZenPolicy implements Parcelable { */ public static final int STATE_DISALLOW = 2; - /** @hide */ - @IntDef(prefix = { "CHANNEL_TYPE_" }, value = { - CHANNEL_TYPE_UNSET, - CHANNEL_TYPE_PRIORITY, - CHANNEL_TYPE_NONE, + @IntDef(prefix = { "CHANNEL_POLICY_" }, value = { + CHANNEL_POLICY_UNSET, + CHANNEL_POLICY_PRIORITY, + CHANNEL_POLICY_NONE, }) @Retention(RetentionPolicy.SOURCE) - public @interface ChannelType {} + private @interface ChannelType {} /** * Indicates no explicit setting for which channels may bypass DND when this policy is active. - * Defaults to {@link #CHANNEL_TYPE_PRIORITY}. + * Defaults to {@link #CHANNEL_POLICY_PRIORITY}. */ @FlaggedApi(Flags.FLAG_MODES_API) - public static final int CHANNEL_TYPE_UNSET = 0; + private static final int CHANNEL_POLICY_UNSET = 0; /** * Indicates that channels marked as {@link NotificationChannel#canBypassDnd()} can bypass DND * when this policy is active. */ @FlaggedApi(Flags.FLAG_MODES_API) - public static final int CHANNEL_TYPE_PRIORITY = 1; + private static final int CHANNEL_POLICY_PRIORITY = 1; /** * Indicates that no channels can bypass DND when this policy is active, even those marked as * {@link NotificationChannel#canBypassDnd()}. */ @FlaggedApi(Flags.FLAG_MODES_API) - public static final int CHANNEL_TYPE_NONE = 2; + private static final int CHANNEL_POLICY_NONE = 2; /** @hide */ public ZenPolicy() { mPriorityCategories = new ArrayList<>(Collections.nCopies(NUM_PRIORITY_CATEGORIES, 0)); mVisualEffects = new ArrayList<>(Collections.nCopies(NUM_VISUAL_EFFECTS, 0)); - mUserModifiedFields = 0; } /** @hide */ @FlaggedApi(Flags.FLAG_MODES_API) public ZenPolicy(List<Integer> priorityCategories, List<Integer> visualEffects, @PeopleType int priorityMessages, @PeopleType int priorityCalls, - @ConversationSenders int conversationSenders, @ChannelType int allowChannels, - @ModifiableField int userModifiedFields) { + @ConversationSenders int conversationSenders, @ChannelType int allowChannels) { mPriorityCategories = priorityCategories; mVisualEffects = visualEffects; mPriorityMessages = priorityMessages; mPriorityCalls = priorityCalls; mConversationSenders = conversationSenders; mAllowChannels = allowChannels; - mUserModifiedFields = userModifiedFields; } /** @@ -584,16 +564,21 @@ public final class ZenPolicy implements Parcelable { } /** - * Which types of {@link NotificationChannel channels} this policy allows to bypass DND. When - * this value is {@link #CHANNEL_TYPE_PRIORITY priority} channels, any channel with - * canBypassDnd() may bypass DND; when it is {@link #CHANNEL_TYPE_NONE none}, even channels - * with canBypassDnd() will be intercepted. - * @return {@link #CHANNEL_TYPE_UNSET}, {@link #CHANNEL_TYPE_PRIORITY}, or - * {@link #CHANNEL_TYPE_NONE} + * Whether this policy allows {@link NotificationChannel channels} marked as + * {@link NotificationChannel#canBypassDnd()} to bypass DND. If {@link #STATE_ALLOW}, these + * channels may bypass; if {@link #STATE_DISALLOW}, then even notifications from channels + * with {@link NotificationChannel#canBypassDnd()} will be intercepted. */ @FlaggedApi(Flags.FLAG_MODES_API) - public @ChannelType int getAllowedChannels() { - return mAllowChannels; + public @State int getPriorityChannels() { + switch (mAllowChannels) { + case CHANNEL_POLICY_PRIORITY: + return STATE_ALLOW; + case CHANNEL_POLICY_NONE: + return STATE_DISALLOW; + default: + return STATE_UNSET; + } } /** @@ -628,8 +613,6 @@ public final class ZenPolicy implements Parcelable { * is not set, it is (@link STATE_UNSET} and will not change the current set policy. */ public static final class Builder { - private @ModifiableField int mUserModifiedFields; - private ZenPolicy mZenPolicy; public Builder() { @@ -644,9 +627,6 @@ public final class ZenPolicy implements Parcelable { public Builder(@Nullable ZenPolicy policy) { if (policy != null) { mZenPolicy = policy.copy(); - if (Flags.modesApi()) { - mUserModifiedFields = policy.mUserModifiedFields; - } } else { mZenPolicy = new ZenPolicy(); } @@ -657,11 +637,10 @@ public final class ZenPolicy implements Parcelable { */ public @NonNull ZenPolicy build() { if (Flags.modesApi()) { - return new ZenPolicy(new ArrayList<Integer>(mZenPolicy.mPriorityCategories), - new ArrayList<Integer>(mZenPolicy.mVisualEffects), + return new ZenPolicy(new ArrayList<>(mZenPolicy.mPriorityCategories), + new ArrayList<>(mZenPolicy.mVisualEffects), mZenPolicy.mPriorityMessages, mZenPolicy.mPriorityCalls, - mZenPolicy.mConversationSenders, mZenPolicy.mAllowChannels, - mUserModifiedFields); + mZenPolicy.mConversationSenders, mZenPolicy.mAllowChannels); } else { return mZenPolicy.copy(); } @@ -1016,34 +995,12 @@ public final class ZenPolicy implements Parcelable { * Set whether priority channels are permitted to break through DND. */ @FlaggedApi(Flags.FLAG_MODES_API) - public @NonNull Builder allowChannels(@ChannelType int channelType) { - mZenPolicy.mAllowChannels = channelType; - return this; - } - - /** - * Sets the user modified fields bitmask. - * @hide - */ - @TestApi - @FlaggedApi(Flags.FLAG_MODES_API) - public @NonNull Builder setUserModifiedFields(@ModifiableField int userModifiedFields) { - mUserModifiedFields = userModifiedFields; + public @NonNull Builder allowPriorityChannels(boolean allow) { + mZenPolicy.mAllowChannels = allow ? CHANNEL_POLICY_PRIORITY : CHANNEL_POLICY_NONE; return this; } } - /** - Gets the bitmask representing which fields are user modified. Bits are set using - * {@link ModifiableField}. - * @hide - */ - @TestApi - @FlaggedApi(Flags.FLAG_MODES_API) - public @ModifiableField int getUserModifiedFields() { - return mUserModifiedFields; - } - @Override public int describeContents() { return 0; @@ -1058,7 +1015,6 @@ public final class ZenPolicy implements Parcelable { dest.writeInt(mConversationSenders); if (Flags.modesApi()) { dest.writeInt(mAllowChannels); - dest.writeInt(mUserModifiedFields); } } @@ -1074,7 +1030,7 @@ public final class ZenPolicy implements Parcelable { trimList(source.readArrayList(Integer.class.getClassLoader(), Integer.class), NUM_VISUAL_EFFECTS), source.readInt(), source.readInt(), source.readInt(), - source.readInt(), source.readInt() + source.readInt() ); } else { policy = new ZenPolicy(); @@ -1109,14 +1065,12 @@ public final class ZenPolicy implements Parcelable { conversationTypeToString(mConversationSenders)); if (Flags.modesApi()) { sb.append(", allowChannels=").append(channelTypeToString(mAllowChannels)); - sb.append(", userModifiedFields=") - .append(modifiedFieldsToString(mUserModifiedFields)); } return sb.append('}').toString(); } - @FlaggedApi(Flags.FLAG_MODES_API) - private String modifiedFieldsToString(@ModifiableField int bitmask) { + /** @hide */ + public static String fieldsToString(@ModifiableField int bitmask) { ArrayList<String> modified = new ArrayList<>(); if ((bitmask & FIELD_MESSAGES) != 0) { modified.add("FIELD_MESSAGES"); @@ -1305,11 +1259,11 @@ public final class ZenPolicy implements Parcelable { @FlaggedApi(Flags.FLAG_MODES_API) public static String channelTypeToString(@ChannelType int channelType) { switch (channelType) { - case CHANNEL_TYPE_UNSET: + case CHANNEL_POLICY_UNSET: return "unset"; - case CHANNEL_TYPE_PRIORITY: + case CHANNEL_POLICY_PRIORITY: return "priority"; - case CHANNEL_TYPE_NONE: + case CHANNEL_POLICY_NONE: return "none"; } return "invalidChannelType{" + channelType + "}"; @@ -1327,8 +1281,7 @@ public final class ZenPolicy implements Parcelable { && other.mPriorityMessages == mPriorityMessages && other.mConversationSenders == mConversationSenders; if (Flags.modesApi()) { - return eq && other.mAllowChannels == mAllowChannels - && other.mUserModifiedFields == mUserModifiedFields; + return eq && other.mAllowChannels == mAllowChannels; } return eq; } @@ -1337,7 +1290,7 @@ public final class ZenPolicy implements Parcelable { public int hashCode() { if (Flags.modesApi()) { return Objects.hash(mPriorityCategories, mVisualEffects, mPriorityCalls, - mPriorityMessages, mConversationSenders, mAllowChannels, mUserModifiedFields); + mPriorityMessages, mConversationSenders, mAllowChannels); } return Objects.hash(mPriorityCategories, mVisualEffects, mPriorityCalls, mPriorityMessages, mConversationSenders); @@ -1389,11 +1342,11 @@ public final class ZenPolicy implements Parcelable { } /** @hide */ - public boolean isCategoryAllowed(@PriorityCategory int category, boolean defaultVal) { - switch (getZenPolicyPriorityCategoryState(category)) { - case ZenPolicy.STATE_ALLOW: + public static boolean stateToBoolean(@State int state, boolean defaultVal) { + switch (state) { + case STATE_ALLOW: return true; - case ZenPolicy.STATE_DISALLOW: + case STATE_DISALLOW: return false; default: return defaultVal; @@ -1401,15 +1354,13 @@ public final class ZenPolicy implements Parcelable { } /** @hide */ + public boolean isCategoryAllowed(@PriorityCategory int category, boolean defaultVal) { + return stateToBoolean(getZenPolicyPriorityCategoryState(category), defaultVal); + } + + /** @hide */ public boolean isVisualEffectAllowed(@VisualEffect int effect, boolean defaultVal) { - switch (getZenPolicyVisualEffectState(effect)) { - case ZenPolicy.STATE_ALLOW: - return true; - case ZenPolicy.STATE_DISALLOW: - return false; - default: - return defaultVal; - } + return stateToBoolean(getZenPolicyVisualEffectState(effect), defaultVal); } /** @@ -1463,8 +1414,8 @@ public final class ZenPolicy implements Parcelable { // apply allowed channels if (Flags.modesApi()) { // if no channels are allowed, can't newly allow them - if (mAllowChannels != CHANNEL_TYPE_NONE - && policyToApply.mAllowChannels != CHANNEL_TYPE_UNSET) { + if (mAllowChannels != CHANNEL_POLICY_NONE + && policyToApply.mAllowChannels != CHANNEL_POLICY_UNSET) { mAllowChannels = policyToApply.mAllowChannels; } } @@ -1530,7 +1481,7 @@ public final class ZenPolicy implements Parcelable { proto.write(DNDPolicyProto.ALLOW_CONVERSATIONS_FROM, getPriorityConversationSenders()); if (Flags.modesApi()) { - proto.write(DNDPolicyProto.ALLOW_CHANNELS, getAllowedChannels()); + proto.write(DNDPolicyProto.ALLOW_CHANNELS, getPriorityChannels()); } proto.flush(); diff --git a/core/java/android/service/voice/VisualQueryDetector.java b/core/java/android/service/voice/VisualQueryDetector.java index adc54f5b5a8c..f2bdbf67e76e 100644 --- a/core/java/android/service/voice/VisualQueryDetector.java +++ b/core/java/android/service/voice/VisualQueryDetector.java @@ -325,7 +325,7 @@ public class VisualQueryDetector { Slog.v(TAG, "BinderCallback#onQueryDetected"); Binder.withCleanCallingIdentity(() -> { synchronized (mLock) { - mCallback.onQueryDetected(partialQuery); + mExecutor.execute(()->mCallback.onQueryDetected(partialQuery)); } }); } @@ -335,7 +335,7 @@ public class VisualQueryDetector { Slog.v(TAG, "BinderCallback#onQueryFinished"); Binder.withCleanCallingIdentity(() -> { synchronized (mLock) { - mCallback.onQueryFinished(); + mExecutor.execute(()->mCallback.onQueryFinished()); } }); } @@ -345,7 +345,7 @@ public class VisualQueryDetector { Slog.v(TAG, "BinderCallback#onQueryRejected"); Binder.withCleanCallingIdentity(() -> { synchronized (mLock) { - mCallback.onQueryRejected(); + mExecutor.execute(()->mCallback.onQueryRejected()); } }); } diff --git a/core/java/android/telephony/PhoneStateListener.java b/core/java/android/telephony/PhoneStateListener.java index cf1156db55e5..fb57921b1529 100644 --- a/core/java/android/telephony/PhoneStateListener.java +++ b/core/java/android/telephony/PhoneStateListener.java @@ -1681,6 +1681,10 @@ public class PhoneStateListener { @EmergencyCallbackModeStopReason int reason) { // not support. Can't override. Use TelephonyCallback. } + + public final void onSimultaneousCallingStateChanged(int[] subIds) { + // not supported on the deprecated interface - Use TelephonyCallback instead + } } private void log(String s) { diff --git a/core/java/android/telephony/TelephonyCallback.java b/core/java/android/telephony/TelephonyCallback.java index 19bcf28d6b83..dc6a035a8176 100644 --- a/core/java/android/telephony/TelephonyCallback.java +++ b/core/java/android/telephony/TelephonyCallback.java @@ -18,6 +18,7 @@ package android.telephony; import android.Manifest; import android.annotation.CallbackExecutor; +import android.annotation.FlaggedApi; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.RequiresPermission; @@ -33,15 +34,19 @@ import android.util.Log; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.telephony.IPhoneStateListener; +import com.android.internal.telephony.flags.Flags; import dalvik.system.VMRuntime; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.ref.WeakReference; +import java.util.Arrays; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.concurrent.Executor; +import java.util.stream.Collectors; /** * A callback class for monitoring changes in specific telephony states @@ -627,6 +632,18 @@ public class TelephonyCallback { public static final int EVENT_EMERGENCY_CALLBACK_MODE_CHANGED = 40; /** + * Event for listening to changes in simultaneous cellular calling subscriptions. + * + * @see SimultaneousCellularCallingSupportListener + * + * @hide + */ + @FlaggedApi(Flags.FLAG_SIMULTANEOUS_CALLING_INDICATIONS) + @RequiresPermission(Manifest.permission.READ_PRIVILEGED_PHONE_STATE) + @SystemApi + public static final int EVENT_SIMULTANEOUS_CELLULAR_CALLING_SUBSCRIPTIONS_CHANGED = 41; + + /** * @hide */ @IntDef(prefix = {"EVENT_"}, value = { @@ -669,7 +686,8 @@ public class TelephonyCallback { EVENT_LINK_CAPACITY_ESTIMATE_CHANGED, EVENT_TRIGGER_NOTIFY_ANBR, EVENT_MEDIA_QUALITY_STATUS_CHANGED, - EVENT_EMERGENCY_CALLBACK_MODE_CHANGED + EVENT_EMERGENCY_CALLBACK_MODE_CHANGED, + EVENT_SIMULTANEOUS_CELLULAR_CALLING_SUBSCRIPTIONS_CHANGED }) @Retention(RetentionPolicy.SOURCE) public @interface TelephonyEvent { @@ -1373,6 +1391,44 @@ public class TelephonyCallback { } /** + * Interface for listening to changes in the simultaneous cellular calling state for active + * cellular subscriptions. + * + * @hide + */ + @FlaggedApi(Flags.FLAG_SIMULTANEOUS_CALLING_INDICATIONS) + @SystemApi + public interface SimultaneousCellularCallingSupportListener { + /** + * Notify the Listener that the subscriptions available for simultaneous <b>cellular</b> + * calling have changed. + * <p> + * If we have an ongoing <b>cellular</b> call on one subscription in this Set, a + * simultaneous incoming or outgoing <b>cellular</b> call is possible on any of the + * subscriptions in this Set. On a traditional Dual Sim Dual Standby device, simultaneous + * calling is not possible between subscriptions, where on a Dual Sim Dual Active device, + * simultaneous calling may be possible between subscriptions in certain network conditions. + * <p> + * Note: This listener only tracks the capability of the modem to perform simultaneous + * cellular calls and does not track the simultaneous calling state of scenarios based on + * multiple IMS registration over multiple transports (WiFi/Internet calling). + * <p> + * Note: This listener fires for all changes to cellular calling subscriptions independent + * of which subscription it is registered on. + * + * @param simultaneousCallingSubscriptionIds The Set of subscription IDs that support + * simultaneous calling. If there is an ongoing call on a subscription in this Set, then a + * simultaneous incoming or outgoing call is only possible for other subscriptions in this + * Set. If there is an ongoing call on a subscription that is not in this Set, then + * simultaneous calling is not possible at the current time. + * + */ + @RequiresPermission(Manifest.permission.READ_PRIVILEGED_PHONE_STATE) + void onSimultaneousCellularCallingSubscriptionsChanged( + @NonNull Set<Integer> simultaneousCallingSubscriptionIds); + } + + /** * Interface for call attributes listener. * * @hide @@ -1976,6 +2032,17 @@ public class TelephonyCallback { allowedNetworkType))); } + public void onSimultaneousCallingStateChanged(int[] subIds) { + SimultaneousCellularCallingSupportListener listener = + (SimultaneousCellularCallingSupportListener) mTelephonyCallbackWeakRef.get(); + if (listener == null) return; + + Binder.withCleanCallingIdentity( + () -> mExecutor.execute( + () -> listener.onSimultaneousCellularCallingSubscriptionsChanged( + Arrays.stream(subIds).boxed().collect(Collectors.toSet())))); + } + public void onLinkCapacityEstimateChanged( List<LinkCapacityEstimate> linkCapacityEstimateList) { LinkCapacityEstimateChangedListener listener = diff --git a/core/java/android/telephony/TelephonyRegistryManager.java b/core/java/android/telephony/TelephonyRegistryManager.java index 886727ea43ef..0de450519646 100644 --- a/core/java/android/telephony/TelephonyRegistryManager.java +++ b/core/java/android/telephony/TelephonyRegistryManager.java @@ -994,6 +994,21 @@ public class TelephonyRegistryManager { } } + /** + * Notify external listeners that the subscriptions supporting simultaneous cellular calling + * have changed. + * @param subIds The new set of subIds supporting simultaneous cellular calling. + */ + public void notifySimultaneousCellularCallingSubscriptionsChanged(Set<Integer> subIds) { + try { + sRegistry.notifySimultaneousCellularCallingSubscriptionsChanged( + subIds.stream().mapToInt(i -> i).toArray()); + } catch (RemoteException ex) { + // system server crash + throw ex.rethrowFromSystemServer(); + } + } + public @NonNull Set<Integer> getEventsFromCallback( @NonNull TelephonyCallback telephonyCallback) { Set<Integer> eventList = new ArraySet<>(); @@ -1135,7 +1150,11 @@ public class TelephonyRegistryManager { eventList.add(TelephonyCallback.EVENT_EMERGENCY_CALLBACK_MODE_CHANGED); } - + if (telephonyCallback + instanceof TelephonyCallback.SimultaneousCellularCallingSupportListener) { + eventList.add( + TelephonyCallback.EVENT_SIMULTANEOUS_CELLULAR_CALLING_SUBSCRIPTIONS_CHANGED); + } return eventList; } diff --git a/core/java/android/text/MeasuredParagraph.java b/core/java/android/text/MeasuredParagraph.java index ac5eb3cbeeaa..b268c2edd9a7 100644 --- a/core/java/android/text/MeasuredParagraph.java +++ b/core/java/android/text/MeasuredParagraph.java @@ -671,9 +671,18 @@ public class MeasuredParagraph { if (mLtrWithoutBidi) { // If the whole text is LTR direction, just apply whole region. if (builder == null) { - mWholeWidth += paint.getTextRunAdvances( - mCopiedBuffer, start, end - start, start, end - start, false /* isRtl */, - mWidths.getRawArray(), start); + // For the compatibility reasons, the letter spacing should not be dropped at the + // left and right edge. + int oldFlag = paint.getFlags(); + paint.setFlags(paint.getFlags() + | (Paint.TEXT_RUN_FLAG_LEFT_EDGE | Paint.TEXT_RUN_FLAG_RIGHT_EDGE)); + try { + mWholeWidth += paint.getTextRunAdvances( + mCopiedBuffer, start, end - start, start, end - start, + false /* isRtl */, mWidths.getRawArray(), start); + } finally { + paint.setFlags(oldFlag); + } } else { builder.appendStyleRun(paint, config, end - start, false /* isRtl */); } @@ -690,9 +699,16 @@ public class MeasuredParagraph { final boolean isRtl = (level & 0x1) != 0; if (builder == null) { final int levelLength = levelEnd - levelStart; - mWholeWidth += paint.getTextRunAdvances( - mCopiedBuffer, levelStart, levelLength, levelStart, levelLength, - isRtl, mWidths.getRawArray(), levelStart); + int oldFlag = paint.getFlags(); + paint.setFlags(paint.getFlags() + | (Paint.TEXT_RUN_FLAG_LEFT_EDGE | Paint.TEXT_RUN_FLAG_RIGHT_EDGE)); + try { + mWholeWidth += paint.getTextRunAdvances( + mCopiedBuffer, levelStart, levelLength, levelStart, levelLength, + isRtl, mWidths.getRawArray(), levelStart); + } finally { + paint.setFlags(oldFlag); + } } else { builder.appendStyleRun(paint, config, levelEnd - levelStart, isRtl); } diff --git a/core/java/android/text/TextLine.java b/core/java/android/text/TextLine.java index 135935cb0632..2175b47e149e 100644 --- a/core/java/android/text/TextLine.java +++ b/core/java/android/text/TextLine.java @@ -291,6 +291,97 @@ public class TextLine { } /** + * Returns the run flag of at the given BiDi run. + * + * @param bidiRunIndex a BiDi run index. + * @return a run flag of the given BiDi run. + */ + @VisibleForTesting + public static int calculateRunFlag(int bidiRunIndex, int bidiRunCount, int lineDirection) { + if (bidiRunCount == 1) { + // Easy case. If there is only single run, it is most left and most right run. + return Paint.TEXT_RUN_FLAG_LEFT_EDGE | Paint.TEXT_RUN_FLAG_RIGHT_EDGE; + } + if (bidiRunIndex != 0 && bidiRunIndex != (bidiRunCount - 1)) { + // Easy case. If the given run is the middle of the line, it is not the most left or + // the most right run. + return 0; + } + + int runFlag = 0; + // For the historical reasons, the BiDi implementation of Android works differently + // from the Java BiDi APIs. The mDirections holds the BiDi runs in visual order, but + // it is reversed order if the paragraph direction is RTL. So, the first BiDi run of + // mDirections is located the most left of the line if the paragraph direction is LTR. + // If the paragraph direction is RTL, the first BiDi run is located the most right of + // the line. + if (bidiRunIndex == 0) { + if (lineDirection == Layout.DIR_LEFT_TO_RIGHT) { + runFlag |= Paint.TEXT_RUN_FLAG_LEFT_EDGE; + } else { + runFlag |= Paint.TEXT_RUN_FLAG_RIGHT_EDGE; + } + } + if (bidiRunIndex == (bidiRunCount - 1)) { + if (lineDirection == Layout.DIR_LEFT_TO_RIGHT) { + runFlag |= Paint.TEXT_RUN_FLAG_RIGHT_EDGE; + } else { + runFlag |= Paint.TEXT_RUN_FLAG_LEFT_EDGE; + } + } + return runFlag; + } + + /** + * Resolve the runFlag for the inline span range. + * + * @param runFlag the runFlag of the current BiDi run. + * @param isRtlRun true for RTL run, false for LTR run. + * @param runStart the inclusive BiDi run start offset. + * @param runEnd the exclusive BiDi run end offset. + * @param spanStart the inclusive span start offset. + * @param spanEnd the exclusive span end offset. + * @return the resolved runFlag. + */ + @VisibleForTesting + public static int resolveRunFlagForSubSequence(int runFlag, boolean isRtlRun, int runStart, + int runEnd, int spanStart, int spanEnd) { + if (runFlag == 0) { + // Easy case. If the run is in the middle of the line, any inline span is also in the + // middle of the line. + return 0; + } + int localRunFlag = runFlag; + if ((runFlag & Paint.TEXT_RUN_FLAG_LEFT_EDGE) != 0) { + if (isRtlRun) { + if (spanEnd != runEnd) { + // In the RTL context, the last run is the most left run. + localRunFlag &= ~Paint.TEXT_RUN_FLAG_LEFT_EDGE; + } + } else { // LTR + if (spanStart != runStart) { + // In the LTR context, the first run is the most left run. + localRunFlag &= ~Paint.TEXT_RUN_FLAG_LEFT_EDGE; + } + } + } + if ((runFlag & Paint.TEXT_RUN_FLAG_RIGHT_EDGE) != 0) { + if (isRtlRun) { + if (spanStart != runStart) { + // In the RTL context, the start of the run is the most right run. + localRunFlag &= ~Paint.TEXT_RUN_FLAG_RIGHT_EDGE; + } + } else { // LTR + if (spanEnd != runEnd) { + // In the LTR context, the last run is the most right position. + localRunFlag &= ~Paint.TEXT_RUN_FLAG_RIGHT_EDGE; + } + } + } + return localRunFlag; + } + + /** * Renders the TextLine. * * @param c the canvas to render on @@ -308,11 +399,13 @@ public class TextLine { final int runLimit = Math.min(runStart + mDirections.getRunLength(runIndex), mLen); final boolean runIsRtl = mDirections.isRunRtl(runIndex); + final int runFlag = calculateRunFlag(runIndex, runCount, mDir); + int segStart = runStart; for (int j = mHasTabs ? runStart : runLimit; j <= runLimit; j++) { if (j == runLimit || charAt(j) == TAB_CHAR) { h += drawRun(c, segStart, j, runIsRtl, x + h, top, y, bottom, - runIndex != (runCount - 1) || j != mLen); + runIndex != (runCount - 1) || j != mLen, runFlag); if (j != runLimit) { // charAt(j) == TAB_CHAR h = mDir * nextTab(h * mDir); @@ -371,11 +464,12 @@ public class TextLine { final int runLimit = Math.min(runStart + mDirections.getRunLength(runIndex), mLen); final boolean runIsRtl = mDirections.isRunRtl(runIndex); + final int runFlag = calculateRunFlag(runIndex, runCount, mDir); int segStart = runStart; for (int j = mHasTabs ? runStart : runLimit; j <= runLimit; j++) { if (j == runLimit || charAt(j) == TAB_CHAR) { horizontal += shapeRun(consumer, segStart, j, runIsRtl, x + horizontal, - runIndex != (runCount - 1) || j != mLen); + runIndex != (runCount - 1) || j != mLen, runFlag); if (j != runLimit) { // charAt(j) == TAB_CHAR horizontal = mDir * nextTab(horizontal * mDir); @@ -441,11 +535,13 @@ public class TextLine { } float h = 0; - for (int runIndex = 0; runIndex < mDirections.getRunCount(); runIndex++) { + final int runCount = mDirections.getRunCount(); + for (int runIndex = 0; runIndex < runCount; runIndex++) { final int runStart = mDirections.getRunStart(runIndex); if (runStart > mLen) break; final int runLimit = Math.min(runStart + mDirections.getRunLength(runIndex), mLen); final boolean runIsRtl = mDirections.isRunRtl(runIndex); + final int runFlag = calculateRunFlag(runIndex, runCount, mDir); int segStart = runStart; for (int j = mHasTabs ? runStart : runLimit; j <= runLimit; j++) { @@ -455,16 +551,16 @@ public class TextLine { if (targetIsInThisSegment && sameDirection) { return h + measureRun(segStart, offset, j, runIsRtl, fmi, drawBounds, null, - 0, h, lineInfo); + 0, h, lineInfo, runFlag); } final float segmentWidth = measureRun(segStart, j, j, runIsRtl, fmi, drawBounds, - null, 0, h, lineInfo); + null, 0, h, lineInfo, runFlag); h += sameDirection ? segmentWidth : -segmentWidth; if (targetIsInThisSegment) { return h + measureRun(segStart, offset, j, runIsRtl, null, null, null, 0, - h, lineInfo); + h, lineInfo, runFlag); } if (j != runLimit) { // charAt(j) == TAB_CHAR @@ -543,20 +639,21 @@ public class TextLine { + "result, needed: " + mLen + " had: " + advances.length); } float h = 0; - for (int runIndex = 0; runIndex < mDirections.getRunCount(); runIndex++) { + final int runCount = mDirections.getRunCount(); + for (int runIndex = 0; runIndex < runCount; runIndex++) { final int runStart = mDirections.getRunStart(runIndex); if (runStart > mLen) break; final int runLimit = Math.min(runStart + mDirections.getRunLength(runIndex), mLen); final boolean runIsRtl = mDirections.isRunRtl(runIndex); + final int runFlag = calculateRunFlag(runIndex, runCount, mDir); int segStart = runStart; for (int j = mHasTabs ? runStart : runLimit; j <= runLimit; j++) { if (j == runLimit || charAt(j) == TAB_CHAR) { final boolean sameDirection = (mDir == Layout.DIR_RIGHT_TO_LEFT) == runIsRtl; - final float segmentWidth = measureRun(segStart, j, j, runIsRtl, null, null, advances, segStart, 0, - null); + null, runFlag); final float oldh = h; h += sameDirection ? segmentWidth : -segmentWidth; @@ -608,11 +705,13 @@ public class TextLine { } float horizontal = 0; - for (int runIndex = 0; runIndex < mDirections.getRunCount(); runIndex++) { + final int runCount = mDirections.getRunCount(); + for (int runIndex = 0; runIndex < runCount; runIndex++) { final int runStart = mDirections.getRunStart(runIndex); if (runStart > mLen) break; final int runLimit = Math.min(runStart + mDirections.getRunLength(runIndex), mLen); final boolean runIsRtl = mDirections.isRunRtl(runIndex); + final int runFlag = calculateRunFlag(runIndex, runCount, mDir); int segStart = runStart; for (int j = mHasTabs ? runStart : runLimit; j <= runLimit; ++j) { @@ -629,7 +728,7 @@ public class TextLine { final float previousSegEndHorizontal = measurement[segStart]; final float width = measureRun(segStart, j, j, runIsRtl, fmi, null, measurement, segStart, - 0, null); + 0, null, runFlag); horizontal += sameDirection ? width : -width; float currHorizontal = sameDirection ? oldHorizontal : horizontal; @@ -686,22 +785,24 @@ public class TextLine { * @param y the baseline * @param bottom the bottom of the line * @param needWidth true if the width value is required. + * @param runFlag the run flag to be applied for this run. * @return the signed width of the run, based on the paragraph direction. * Only valid if needWidth is true. */ private float drawRun(Canvas c, int start, int limit, boolean runIsRtl, float x, int top, int y, int bottom, - boolean needWidth) { + boolean needWidth, int runFlag) { if ((mDir == Layout.DIR_LEFT_TO_RIGHT) == runIsRtl) { - float w = -measureRun(start, limit, limit, runIsRtl, null, null, null, 0, 0, null); + float w = -measureRun(start, limit, limit, runIsRtl, null, null, null, 0, 0, null, + runFlag); handleRun(start, limit, limit, runIsRtl, c, null, x + w, top, - y, bottom, null, null, false, null, 0, null); + y, bottom, null, null, false, null, 0, null, runFlag); return w; } return handleRun(start, limit, limit, runIsRtl, c, null, x, top, - y, bottom, null, null, needWidth, null, 0, null); + y, bottom, null, null, needWidth, null, 0, null, runFlag); } /** @@ -718,19 +819,21 @@ public class TextLine { * @param advancesIndex the start index to fill in the advance information. * @param x horizontal offset of the run. * @param lineInfo an optional output parameter for filling line information. + * @param runFlag the run flag to be applied for this run. * @return the signed width from the start of the run to the leading edge * of the character at offset, based on the run (not paragraph) direction */ private float measureRun(int start, int offset, int limit, boolean runIsRtl, @Nullable FontMetricsInt fmi, @Nullable RectF drawBounds, @Nullable float[] advances, - int advancesIndex, float x, @Nullable LineInfo lineInfo) { + int advancesIndex, float x, @Nullable LineInfo lineInfo, int runFlag) { if (drawBounds != null && (mDir == Layout.DIR_LEFT_TO_RIGHT) == runIsRtl) { - float w = -measureRun(start, offset, limit, runIsRtl, null, null, null, 0, 0, null); + float w = -measureRun(start, offset, limit, runIsRtl, null, null, null, 0, 0, null, + runFlag); return handleRun(start, offset, limit, runIsRtl, null, null, x + w, 0, 0, 0, fmi, - drawBounds, true, advances, advancesIndex, lineInfo); + drawBounds, true, advances, advancesIndex, lineInfo, runFlag); } return handleRun(start, offset, limit, runIsRtl, null, null, x, 0, 0, 0, fmi, drawBounds, - true, advances, advancesIndex, lineInfo); + true, advances, advancesIndex, lineInfo, runFlag); } /** @@ -742,21 +845,23 @@ public class TextLine { * @param runIsRtl true if the run is right-to-left * @param x the position of the run that is closest to the leading margin * @param needWidth true if the width value is required. + * @param runFlag the run flag to be applied for this run. * @return the signed width of the run, based on the paragraph direction. * Only valid if needWidth is true. */ private float shapeRun(TextShaper.GlyphsConsumer consumer, int start, - int limit, boolean runIsRtl, float x, boolean needWidth) { + int limit, boolean runIsRtl, float x, boolean needWidth, int runFlag) { if ((mDir == Layout.DIR_LEFT_TO_RIGHT) == runIsRtl) { - float w = -measureRun(start, limit, limit, runIsRtl, null, null, null, 0, 0, null); + float w = -measureRun(start, limit, limit, runIsRtl, null, null, null, 0, 0, null, + runFlag); handleRun(start, limit, limit, runIsRtl, null, consumer, x + w, 0, 0, 0, null, null, - false, null, 0, null); + false, null, 0, null, runFlag); return w; } return handleRun(start, limit, limit, runIsRtl, null, consumer, x, 0, 0, 0, null, null, - needWidth, null, 0, null); + needWidth, null, 0, null, runFlag); } @@ -1160,6 +1265,7 @@ public class TextLine { * @param advances receives the advance information about the requested run, can be null. * @param advancesIndex the start index to fill in the advance information. * @param lineInfo an optional output parameter for filling line information. + * @param runFlag the run flag to be applied for this run. * @return the signed width of the run based on the run direction; only * valid if needWidth is true */ @@ -1168,8 +1274,8 @@ public class TextLine { Canvas c, TextShaper.GlyphsConsumer consumer, float x, int top, int y, int bottom, FontMetricsInt fmi, RectF drawBounds, boolean needWidth, int offset, @Nullable ArrayList<DecorationInfo> decorations, - @Nullable float[] advances, int advancesIndex, @Nullable LineInfo lineInfo) { - + @Nullable float[] advances, int advancesIndex, @Nullable LineInfo lineInfo, + int runFlag) { if (mIsJustifying) { wp.setWordSpacing(mAddedWidthForJustify); } @@ -1187,7 +1293,16 @@ public class TextLine { } float totalWidth = 0; - + if ((runFlag & Paint.TEXT_RUN_FLAG_LEFT_EDGE) == Paint.TEXT_RUN_FLAG_LEFT_EDGE) { + wp.setFlags(wp.getFlags() | Paint.TEXT_RUN_FLAG_LEFT_EDGE); + } else { + wp.setFlags(wp.getFlags() & ~Paint.TEXT_RUN_FLAG_LEFT_EDGE); + } + if ((runFlag & Paint.TEXT_RUN_FLAG_RIGHT_EDGE) == Paint.TEXT_RUN_FLAG_RIGHT_EDGE) { + wp.setFlags(wp.getFlags() | Paint.TEXT_RUN_FLAG_RIGHT_EDGE); + } else { + wp.setFlags(wp.getFlags() & ~Paint.TEXT_RUN_FLAG_RIGHT_EDGE); + } final int numDecorations = decorations == null ? 0 : decorations.size(); if (needWidth || ((c != null || consumer != null) && (wp.bgColor != 0 || numDecorations != 0 || runIsRtl))) { @@ -1419,6 +1534,7 @@ public class TextLine { * @param advances receives the advance information about the requested run, can be null. * @param advancesIndex the start index to fill in the advance information. * @param lineInfo an optional output parameter for filling line information. + * @param runFlag the run flag to be applied for this run. * @return the signed width of the run based on the run direction; only * valid if needWidth is true */ @@ -1426,7 +1542,8 @@ public class TextLine { int limit, boolean runIsRtl, Canvas c, TextShaper.GlyphsConsumer consumer, float x, int top, int y, int bottom, FontMetricsInt fmi, RectF drawBounds, boolean needWidth, - @Nullable float[] advances, int advancesIndex, @Nullable LineInfo lineInfo) { + @Nullable float[] advances, int advancesIndex, @Nullable LineInfo lineInfo, + int runFlag) { if (measureLimit < start || measureLimit > limit) { throw new IndexOutOfBoundsException("measureLimit (" + measureLimit + ") is out of " @@ -1473,7 +1590,7 @@ public class TextLine { wp.setEndHyphenEdit(adjustEndHyphenEdit(limit, wp.getEndHyphenEdit())); return handleText(wp, start, limit, start, limit, runIsRtl, c, consumer, x, top, y, bottom, fmi, drawBounds, needWidth, measureLimit, null, advances, - advancesIndex, lineInfo); + advancesIndex, lineInfo, runFlag); } // Shaping needs to take into account context up to metric boundaries, @@ -1554,6 +1671,9 @@ public class TextLine { // and use. activePaint.set(wp); } else if (!equalAttributes(wp, activePaint)) { + final int spanRunFlag = resolveRunFlagForSubSequence( + runFlag, runIsRtl, start, measureLimit, activeStart, activeEnd); + // The style of the present chunk of text is substantially different from the // style of the previous chunk. We need to handle the active piece of text // and restart with the present chunk. @@ -1565,7 +1685,7 @@ public class TextLine { consumer, x, top, y, bottom, fmi, drawBounds, needWidth || activeEnd < measureLimit, Math.min(activeEnd, mlimit), mDecorations, - advances, advancesIndex + activeStart - start, lineInfo); + advances, advancesIndex + activeStart - start, lineInfo, spanRunFlag); activeStart = j; activePaint.set(wp); @@ -1585,6 +1705,9 @@ public class TextLine { mDecorations.add(copy); } } + + final int spanRunFlag = resolveRunFlagForSubSequence( + runFlag, runIsRtl, start, measureLimit, activeStart, activeEnd); // Handle the final piece of text. activePaint.setStartHyphenEdit( adjustStartHyphenEdit(activeStart, mPaint.getStartHyphenEdit())); @@ -1593,7 +1716,7 @@ public class TextLine { x += handleText(activePaint, activeStart, activeEnd, i, inext, runIsRtl, c, consumer, x, top, y, bottom, fmi, drawBounds, needWidth || activeEnd < measureLimit, Math.min(activeEnd, mlimit), mDecorations, - advances, advancesIndex + activeStart - start, lineInfo); + advances, advancesIndex + activeStart - start, lineInfo, spanRunFlag); } return x - originalX; @@ -1614,7 +1737,6 @@ public class TextLine { */ private void drawTextRun(Canvas c, TextPaint wp, int start, int end, int contextStart, int contextEnd, boolean runIsRtl, float x, int y) { - if (mCharsValid) { int count = end - start; int contextCount = contextEnd - contextStart; diff --git a/core/java/android/tracing/perfetto/CreateIncrementalStateArgs.java b/core/java/android/tracing/perfetto/CreateIncrementalStateArgs.java new file mode 100644 index 000000000000..819cc8c7beef --- /dev/null +++ b/core/java/android/tracing/perfetto/CreateIncrementalStateArgs.java @@ -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 android.tracing.perfetto; + +import android.annotation.Nullable; + +/** + * @hide + * @param <DataSourceInstanceType> The type of datasource instance this state applied to. + */ +public class CreateIncrementalStateArgs<DataSourceInstanceType extends DataSourceInstance> { + private final DataSource<DataSourceInstanceType, Object, Object> mDataSource; + private final int mInstanceIndex; + + CreateIncrementalStateArgs(DataSource dataSource, int instanceIndex) { + this.mDataSource = dataSource; + this.mInstanceIndex = instanceIndex; + } + + /** + * Gets the datasource instance for this state with a lock. + * releaseDataSourceInstanceLocked must be called before this can be called again. + * @return The data source instance for this state. + * Null if the datasource instance no longer exists. + */ + public @Nullable DataSourceInstanceType getDataSourceInstanceLocked() { + return mDataSource.getDataSourceInstanceLocked(mInstanceIndex); + } +} diff --git a/core/java/android/tracing/perfetto/CreateTlsStateArgs.java b/core/java/android/tracing/perfetto/CreateTlsStateArgs.java new file mode 100644 index 000000000000..3fad2d1b3e53 --- /dev/null +++ b/core/java/android/tracing/perfetto/CreateTlsStateArgs.java @@ -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 android.tracing.perfetto; + +import android.annotation.Nullable; + +/** + * @hide + * @param <DataSourceInstanceType> The type of datasource instance this state applied to. + */ +public class CreateTlsStateArgs<DataSourceInstanceType extends DataSourceInstance> { + private final DataSource<DataSourceInstanceType, Object, Object> mDataSource; + private final int mInstanceIndex; + + CreateTlsStateArgs(DataSource dataSource, int instanceIndex) { + this.mDataSource = dataSource; + this.mInstanceIndex = instanceIndex; + } + + /** + * Gets the datasource instance for this state with a lock. + * releaseDataSourceInstanceLocked must be called before this can be called again. + * @return The data source instance for this state. + * Null if the datasource instance no longer exists. + */ + public @Nullable DataSourceInstanceType getDataSourceInstanceLocked() { + return mDataSource.getDataSourceInstanceLocked(mInstanceIndex); + } +} diff --git a/core/java/android/tracing/perfetto/DataSource.java b/core/java/android/tracing/perfetto/DataSource.java new file mode 100644 index 000000000000..4e08aeef88e6 --- /dev/null +++ b/core/java/android/tracing/perfetto/DataSource.java @@ -0,0 +1,163 @@ +/* + * 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.tracing.perfetto; + +import android.util.proto.ProtoInputStream; + +/** + * Templated base class meant to be derived by embedders to create a custom data + * source. + * + * @param <DataSourceInstanceType> The type for the DataSource instances that will be created from + * this DataSource type. + * @param <TlsStateType> The type of the custom TLS state, if any is used. + * @param <IncrementalStateType> The type of the custom incremental state, if any is used. + * + * @hide + */ +public abstract class DataSource<DataSourceInstanceType extends DataSourceInstance, + TlsStateType, IncrementalStateType> { + protected final long mNativeObj; + + public final String name; + + /** + * A function implemented by each datasource to create a new data source instance. + * + * @param configStream A ProtoInputStream to read the tracing instance's config. + * @return A new data source instance setup with the provided config. + */ + public abstract DataSourceInstanceType createInstance( + ProtoInputStream configStream, int instanceIndex); + + /** + * Constructor for datasource base class. + * + * @param name The fully qualified name of the datasource. + */ + public DataSource(String name) { + this.name = name; + this.mNativeObj = nativeCreate(this, name); + } + + /** + * The main tracing method. Tracing code should call this passing a lambda as + * argument, with the following signature: void(TraceContext). + * <p> + * The lambda will be called synchronously (i.e., always before trace() + * returns) only if tracing is enabled and the data source has been enabled in + * the tracing config. + * <p> + * The lambda can be called more than once per trace() call, in the case of + * concurrent tracing sessions (or even if the data source is instantiated + * twice within the same trace config). + * + * @param fun The tracing lambda that will be called with the tracing contexts of each active + * tracing instance. + */ + public final void trace( + TraceFunction<DataSourceInstanceType, TlsStateType, IncrementalStateType> fun) { + nativeTrace(mNativeObj, fun); + } + + /** + * Flush any trace data from this datasource that has not yet been flushed. + */ + public final void flush() { + nativeFlushAll(mNativeObj); + } + + /** + * Override this method to create a custom TlsState object for your DataSource. A new instance + * will be created per trace instance per thread. + * + * NOTE: Should only be called from native side. + */ + protected TlsStateType createTlsState(CreateTlsStateArgs<DataSourceInstanceType> args) { + return null; + } + + /** + * Override this method to create and use a custom IncrementalState object for your DataSource. + * + * NOTE: Should only be called from native side. + */ + protected IncrementalStateType createIncrementalState( + CreateIncrementalStateArgs<DataSourceInstanceType> args) { + return null; + } + + /** + * Registers the data source on all tracing backends, including ones that + * connect after the registration. Doing so enables the data source to receive + * Setup/Start/Stop notifications and makes the trace() method work when + * tracing is enabled and the data source is selected. + * <p> + * NOTE: Once registered, we cannot unregister the data source. Therefore, we should avoid + * creating and registering data source where not strictly required. This is a fundamental + * limitation of Perfetto itself. + * + * @param params Params to initialize the datasource with. + */ + public void register(DataSourceParams params) { + nativeRegisterDataSource(this.mNativeObj, params.bufferExhaustedPolicy); + } + + /** + * Gets the datasource instance with a specified index. + * IMPORTANT: releaseDataSourceInstance must be called after using the datasource instance. + * @param instanceIndex The index of the datasource to lock and get. + * @return The DataSourceInstance at index instanceIndex. + * Null if the datasource instance at the requested index doesn't exist. + */ + public DataSourceInstanceType getDataSourceInstanceLocked(int instanceIndex) { + return (DataSourceInstanceType) nativeGetPerfettoInstanceLocked(mNativeObj, instanceIndex); + } + + /** + * Unlock the datasource at the specified index. + * @param instanceIndex The index of the datasource to unlock. + */ + protected void releaseDataSourceInstance(int instanceIndex) { + nativeReleasePerfettoInstanceLocked(mNativeObj, instanceIndex); + } + + /** + * Called from native side when a new tracing instance starts. + * + * @param rawConfig byte array of the PerfettoConfig encoded proto. + * @return A new Java DataSourceInstance object. + */ + private DataSourceInstanceType createInstance(byte[] rawConfig, int instanceIndex) { + final ProtoInputStream inputStream = new ProtoInputStream(rawConfig); + return this.createInstance(inputStream, instanceIndex); + } + + private static native void nativeRegisterDataSource( + long dataSourcePtr, int bufferExhaustedPolicy); + + private static native long nativeCreate(DataSource thiz, String name); + private static native void nativeTrace( + long nativeDataSourcePointer, TraceFunction traceFunction); + private static native void nativeFlushAll(long nativeDataSourcePointer); + private static native long nativeGetFinalizer(); + + private static native DataSourceInstance nativeGetPerfettoInstanceLocked( + long dataSourcePtr, int dsInstanceIdx); + private static native void nativeReleasePerfettoInstanceLocked( + long dataSourcePtr, int dsInstanceIdx); +} diff --git a/core/java/android/tracing/perfetto/DataSourceInstance.java b/core/java/android/tracing/perfetto/DataSourceInstance.java new file mode 100644 index 000000000000..49945013ae87 --- /dev/null +++ b/core/java/android/tracing/perfetto/DataSourceInstance.java @@ -0,0 +1,72 @@ +/* + * 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.tracing.perfetto; + +/** + * @hide + */ +public abstract class DataSourceInstance implements AutoCloseable { + private final DataSource mDataSource; + private final int mInstanceIndex; + + public DataSourceInstance(DataSource dataSource, int instanceIndex) { + this.mDataSource = dataSource; + this.mInstanceIndex = instanceIndex; + } + + /** + * Executed when the tracing instance starts running. + * <p> + * NOTE: This callback executes on the Perfetto internal thread and is blocking. + * Anything that is run in this callback should execute quickly. + * + * @param args Start arguments. + */ + protected void onStart(StartCallbackArguments args) {} + + /** + * Executed when a flush is triggered. + * <p> + * NOTE: This callback executes on the Perfetto internal thread and is blocking. + * Anything that is run in this callback should execute quickly. + * @param args Flush arguments. + */ + protected void onFlush(FlushCallbackArguments args) {} + + /** + * Executed when the tracing instance is stopped. + * <p> + * NOTE: This callback executes on the Perfetto internal thread and is blocking. + * Anything that is run in this callback should execute quickly. + * @param args Stop arguments. + */ + protected void onStop(StopCallbackArguments args) {} + + @Override + public final void close() { + this.release(); + } + + /** + * Release the lock on the datasource once you are finished using it. + * Only required to be called when instance was retrieved with + * `DataSource#getDataSourceInstanceLocked`. + */ + public final void release() { + mDataSource.releaseDataSourceInstance(mInstanceIndex); + } +} diff --git a/core/java/android/tracing/perfetto/DataSourceParams.java b/core/java/android/tracing/perfetto/DataSourceParams.java new file mode 100644 index 000000000000..6cd04e3d9a8b --- /dev/null +++ b/core/java/android/tracing/perfetto/DataSourceParams.java @@ -0,0 +1,57 @@ +/* + * 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.tracing.perfetto; + +import android.annotation.IntDef; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * DataSource Parameters + * + * @hide + */ +public class DataSourceParams { + /** + * @hide + */ + @IntDef(value = { + PERFETTO_DS_BUFFER_EXHAUSTED_POLICY_DROP, + PERFETTO_DS_BUFFER_EXHAUSTED_POLICY_STALL_AND_ABORT, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface PerfettoDsBufferExhausted {} + + // If the data source runs out of space when trying to acquire a new chunk, + // it will drop data. + public static final int PERFETTO_DS_BUFFER_EXHAUSTED_POLICY_DROP = 0; + + // If the data source runs out of space when trying to acquire a new chunk, + // it will stall, retry and eventually abort if a free chunk is not acquired + // after a while. + public static final int PERFETTO_DS_BUFFER_EXHAUSTED_POLICY_STALL_AND_ABORT = 1; + + public static DataSourceParams DEFAULTS = + new DataSourceParams(PERFETTO_DS_BUFFER_EXHAUSTED_POLICY_DROP); + + public DataSourceParams(@PerfettoDsBufferExhausted int bufferExhaustedPolicy) { + this.bufferExhaustedPolicy = bufferExhaustedPolicy; + } + + public final @PerfettoDsBufferExhausted int bufferExhaustedPolicy; +} diff --git a/core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.aidl b/core/java/android/tracing/perfetto/FlushCallbackArguments.java index ce92b6d48f0d..ecf6aee9ef50 100644 --- a/core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.aidl +++ b/core/java/android/tracing/perfetto/FlushCallbackArguments.java @@ -13,10 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package android.companion.virtual.camera; + +package android.tracing.perfetto; /** - * The configuration of a single virtual camera stream. * @hide */ -parcelable VirtualCameraStreamConfig;
\ No newline at end of file +public class FlushCallbackArguments { +} diff --git a/core/java/android/tracing/perfetto/InitArguments.java b/core/java/android/tracing/perfetto/InitArguments.java new file mode 100644 index 000000000000..da8c273fd14e --- /dev/null +++ b/core/java/android/tracing/perfetto/InitArguments.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 android.tracing.perfetto; + +import android.annotation.IntDef; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * @hide + */ +public class InitArguments { + public final @PerfettoBackend int backends; + + /** + * @hide + */ + @IntDef(value = { + PERFETTO_BACKEND_IN_PROCESS, + PERFETTO_BACKEND_SYSTEM, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface PerfettoBackend {} + + // The in-process tracing backend. Keeps trace buffers in the process memory. + public static final int PERFETTO_BACKEND_IN_PROCESS = (1 << 0); + + // The system tracing backend. Connects to the system tracing service (e.g. + // on Linux/Android/Mac uses a named UNIX socket). + public static final int PERFETTO_BACKEND_SYSTEM = (1 << 1); + + public static InitArguments DEFAULTS = new InitArguments(PERFETTO_BACKEND_SYSTEM); + + public static InitArguments TESTING = new InitArguments(PERFETTO_BACKEND_IN_PROCESS); + + public InitArguments(@PerfettoBackend int backends) { + this.backends = backends; + } +} diff --git a/core/java/android/tracing/perfetto/Producer.java b/core/java/android/tracing/perfetto/Producer.java new file mode 100644 index 000000000000..a1b3eb754157 --- /dev/null +++ b/core/java/android/tracing/perfetto/Producer.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.tracing.perfetto; + +/** + * @hide + */ +public class Producer { + + /** + * Initializes the global Perfetto producer. + * + * @param args arguments on how to initialize the Perfetto producer. + */ + public static void init(InitArguments args) { + nativePerfettoProducerInit(args.backends); + } + + private static native void nativePerfettoProducerInit(int backends); +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/model/SceneContainerConfigKosmos.kt b/core/java/android/tracing/perfetto/StartCallbackArguments.java index f9cdc1bea309..9739d271a13f 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/model/SceneContainerConfigKosmos.kt +++ b/core/java/android/tracing/perfetto/StartCallbackArguments.java @@ -14,9 +14,10 @@ * limitations under the License. */ -package com.android.systemui.scene.shared.model +package android.tracing.perfetto; -import com.android.systemui.kosmos.Kosmos - -val Kosmos.sceneContainerConfig by - Kosmos.Fixture { FakeSceneContainerConfigModule().sceneContainerConfig } +/** + * @hide + */ +public class StartCallbackArguments { +} diff --git a/core/java/android/tracing/perfetto/StopCallbackArguments.java b/core/java/android/tracing/perfetto/StopCallbackArguments.java new file mode 100644 index 000000000000..0cd1a188fa0c --- /dev/null +++ b/core/java/android/tracing/perfetto/StopCallbackArguments.java @@ -0,0 +1,23 @@ +/* + * 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.tracing.perfetto; + +/** + * @hide + */ +public class StopCallbackArguments { +} diff --git a/core/java/android/tracing/perfetto/TraceFunction.java b/core/java/android/tracing/perfetto/TraceFunction.java new file mode 100644 index 000000000000..62941df70a48 --- /dev/null +++ b/core/java/android/tracing/perfetto/TraceFunction.java @@ -0,0 +1,43 @@ +/* + * 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.tracing.perfetto; + +import java.io.IOException; + +/** + * The interface for the trace function called from native on a trace call with a context. + * + * @param <DataSourceInstanceType> The type of DataSource this tracing context is for. + * @param <TlsStateType> The type of the custom TLS state, if any is used. + * @param <IncrementalStateType> The type of the custom incremental state, if any is used. + * + * @hide + */ +public interface TraceFunction<DataSourceInstanceType extends DataSourceInstance, + TlsStateType, IncrementalStateType> { + + /** + * This function will be called synchronously (i.e., always before trace() returns) only if + * tracing is enabled and the data source has been enabled in the tracing config. + * It can be called more than once per trace() call, in the case of concurrent tracing sessions + * (or even if the data source is instantiated twice within the same trace config). + * + * @param ctx the tracing context to trace for in the trace function. + */ + void trace(TracingContext<DataSourceInstanceType, TlsStateType, IncrementalStateType> ctx) + throws IOException; +} diff --git a/core/java/android/tracing/perfetto/TracingContext.java b/core/java/android/tracing/perfetto/TracingContext.java new file mode 100644 index 000000000000..0bce26e007a1 --- /dev/null +++ b/core/java/android/tracing/perfetto/TracingContext.java @@ -0,0 +1,110 @@ +/* + * 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.tracing.perfetto; + +import android.util.proto.ProtoOutputStream; + +import java.util.ArrayList; +import java.util.List; + +/** + * Argument passed to the lambda function passed to Trace(). + * + * @param <DataSourceInstanceType> The type of the datasource this tracing context is for. + * @param <TlsStateType> The type of the custom TLS state, if any is used. + * @param <IncrementalStateType> The type of the custom incremental state, if any is used. + * + * @hide + */ +public class TracingContext<DataSourceInstanceType extends DataSourceInstance, + TlsStateType, IncrementalStateType> { + + private final long mContextPtr; + private final TlsStateType mTlsState; + private final IncrementalStateType mIncrementalState; + private final List<ProtoOutputStream> mTracePackets = new ArrayList<>(); + + // Should only be created from the native side. + private TracingContext(long contextPtr, TlsStateType tlsState, + IncrementalStateType incrementalState) { + this.mContextPtr = contextPtr; + this.mTlsState = tlsState; + this.mIncrementalState = incrementalState; + } + + /** + * Creates a new output stream to be used to write a trace packet to. The output stream will be + * encoded to the proto binary representation when the callback trace function finishes and + * send over to the native side to be included in the proto buffer. + * + * @return A proto output stream to write a trace packet proto to + */ + public ProtoOutputStream newTracePacket() { + final ProtoOutputStream os = new ProtoOutputStream(0); + mTracePackets.add(os); + return os; + } + + /** + * Forces a commit of the thread-local tracing data written so far to the + * service. This is almost never required (tracing data is periodically + * committed as trace pages are filled up) and has a non-negligible + * performance hit (requires an IPC + refresh of the current thread-local + * chunk). The only case when this should be used is when handling OnStop() + * asynchronously, to ensure sure that the data is committed before the + * Stop timeout expires. + */ + public void flush() { + nativeFlush(this, mContextPtr); + } + + /** + * Can optionally be used to store custom per-sequence + * session data, which is not reset when incremental state is cleared + * (e.g. configuration options). + * + * @return The TlsState instance for the tracing thread and instance. + */ + public TlsStateType getCustomTlsState() { + return this.mTlsState; + } + + /** + * Can optionally be used store custom per-sequence + * incremental data (e.g., interning tables). + * + * @return The current IncrementalState object instance. + */ + public IncrementalStateType getIncrementalState() { + return this.mIncrementalState; + } + + // Called from native to get trace packets + private byte[][] getAndClearAllPendingTracePackets() { + byte[][] res = new byte[mTracePackets.size()][]; + for (int i = 0; i < mTracePackets.size(); i++) { + ProtoOutputStream tracePacket = mTracePackets.get(i); + res[i] = tracePacket.getBytes(); + } + + mTracePackets.clear(); + return res; + } + + // private static native void nativeFlush(long nativeDataSourcePointer); + private static native void nativeFlush(TracingContext thiz, long ctxPointer); +} diff --git a/core/java/android/tracing/transition/TransitionDataSource.java b/core/java/android/tracing/transition/TransitionDataSource.java new file mode 100644 index 000000000000..baece75cbf09 --- /dev/null +++ b/core/java/android/tracing/transition/TransitionDataSource.java @@ -0,0 +1,76 @@ +/* + * 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.tracing.transition; + +import android.tracing.perfetto.CreateTlsStateArgs; +import android.tracing.perfetto.DataSource; +import android.tracing.perfetto.DataSourceInstance; +import android.tracing.perfetto.FlushCallbackArguments; +import android.tracing.perfetto.StartCallbackArguments; +import android.tracing.perfetto.StopCallbackArguments; +import android.util.proto.ProtoInputStream; + +import java.util.HashMap; +import java.util.Map; + +/** + * @hide + */ +public class TransitionDataSource + extends DataSource<DataSourceInstance, TransitionDataSource.TlsState, Void> { + public static String DATA_SOURCE_NAME = "com.android.wm.shell.transition"; + + private final Runnable mOnStartStaticCallback; + private final Runnable mOnFlushStaticCallback; + private final Runnable mOnStopStaticCallback; + + public TransitionDataSource(Runnable onStart, Runnable onFlush, Runnable onStop) { + super(DATA_SOURCE_NAME); + this.mOnStartStaticCallback = onStart; + this.mOnFlushStaticCallback = onFlush; + this.mOnStopStaticCallback = onStop; + } + + @Override + protected TlsState createTlsState(CreateTlsStateArgs<DataSourceInstance> args) { + return new TlsState(); + } + + public class TlsState { + public final Map<String, Integer> handlerMapping = new HashMap<>(); + } + + @Override + public DataSourceInstance createInstance(ProtoInputStream configStream, int instanceIndex) { + return new DataSourceInstance(this, instanceIndex) { + @Override + protected void onStart(StartCallbackArguments args) { + mOnStartStaticCallback.run(); + } + + @Override + protected void onFlush(FlushCallbackArguments args) { + mOnFlushStaticCallback.run(); + } + + @Override + protected void onStop(StopCallbackArguments args) { + mOnStopStaticCallback.run(); + } + }; + } +} diff --git a/core/java/android/view/IWindowManager.aidl b/core/java/android/view/IWindowManager.aidl index 36b74e39072a..7903050e41d4 100644 --- a/core/java/android/view/IWindowManager.aidl +++ b/core/java/android/view/IWindowManager.aidl @@ -69,12 +69,13 @@ import android.view.SurfaceControl; import android.view.displayhash.DisplayHash; import android.view.displayhash.VerifiedDisplayHash; import android.window.AddToSurfaceSyncGroupResult; +import android.window.IScreenRecordingCallback; import android.window.ISurfaceSyncGroupCompletedListener; import android.window.ITaskFpsCallback; -import android.window.ScreenCapture; -import android.window.WindowContextInfo; import android.window.ITrustedPresentationListener; +import android.window.ScreenCapture; import android.window.TrustedPresentationThresholds; +import android.window.WindowContextInfo; /** * System private interface to the window manager. @@ -1083,4 +1084,8 @@ interface IWindowManager void unregisterTrustedPresentationListener(in ITrustedPresentationListener listener, int id); + + boolean registerScreenRecordingCallback(IScreenRecordingCallback callback); + + void unregisterScreenRecordingCallback(IScreenRecordingCallback callback); } diff --git a/core/java/android/view/Surface.java b/core/java/android/view/Surface.java index 785055441d59..ba7874eb2d21 100644 --- a/core/java/android/view/Surface.java +++ b/core/java/android/view/Surface.java @@ -39,8 +39,10 @@ import android.os.Build; import android.os.Parcel; import android.os.Parcelable; import android.util.Log; +import android.view.flags.Flags; import dalvik.system.CloseGuard; +import dalvik.system.VMRuntime; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -101,6 +103,10 @@ public class Surface implements Parcelable { long nativeObject, float frameRate, int compatibility, int changeFrameRateStrategy); private static native void nativeDestroy(long nativeObject); + // 5MB is a wild guess for what the average surface should be. On most new phones, a full-screen + // surface is about 9MB... but not all surfaces are screen size. This should be a nice balance. + private static final long SURFACE_NATIVE_ALLOCATION_SIZE_BYTES = 5_000_000; + public static final @android.annotation.NonNull Parcelable.Creator<Surface> CREATOR = new Parcelable.Creator<Surface>() { @Override @@ -331,6 +337,7 @@ public class Surface implements Parcelable { */ @UnsupportedAppUsage public Surface() { + registerNativeMemoryUsage(); } /** @@ -343,6 +350,7 @@ public class Surface implements Parcelable { */ public Surface(@NonNull SurfaceControl from) { copyFrom(from); + registerNativeMemoryUsage(); } /** @@ -370,6 +378,7 @@ public class Surface implements Parcelable { mName = surfaceTexture.toString(); setNativeObjectLocked(nativeCreateFromSurfaceTexture(surfaceTexture)); } + registerNativeMemoryUsage(); } /* called from android_view_Surface_createFromIGraphicBufferProducer() */ @@ -378,6 +387,7 @@ public class Surface implements Parcelable { synchronized (mLock) { setNativeObjectLocked(nativeObject); } + registerNativeMemoryUsage(); } @Override @@ -389,6 +399,7 @@ public class Surface implements Parcelable { release(); } finally { super.finalize(); + freeNativeMemoryUsage(); } } @@ -1243,4 +1254,16 @@ public class Surface implements Parcelable { return mIsWideColorGamut; } } + + private static void registerNativeMemoryUsage() { + if (Flags.enableSurfaceNativeAllocRegistration()) { + VMRuntime.getRuntime().registerNativeAllocation(SURFACE_NATIVE_ALLOCATION_SIZE_BYTES); + } + } + + private static void freeNativeMemoryUsage() { + if (Flags.enableSurfaceNativeAllocRegistration()) { + VMRuntime.getRuntime().registerNativeFree(SURFACE_NATIVE_ALLOCATION_SIZE_BYTES); + } + } } diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java index 257ecc565c87..c98d1d7ecaea 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -15015,6 +15015,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } /** @hide */ + @Nullable View getSelfOrParentImportantForA11y() { if (isImportantForAccessibility()) return this; ViewParent parent = getParentForAccessibility(); diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index c66f3c8032fd..57174de8a62c 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -26,6 +26,7 @@ import static android.view.Display.INVALID_DISPLAY; 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; @@ -87,6 +88,7 @@ import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_SANDBOXING_VIEW_B import static android.view.WindowManagerGlobal.RELAYOUT_RES_CANCEL_AND_REDRAW; import static android.view.WindowManagerGlobal.RELAYOUT_RES_CONSUME_ALWAYS_SYSTEM_BARS; import static android.view.WindowManagerGlobal.RELAYOUT_RES_SURFACE_CHANGED; +import static android.view.accessibility.Flags.fixMergedContentChangeEvent; import static android.view.accessibility.Flags.forceInvertColor; import static android.view.accessibility.Flags.reduceWindowContentChangedEventThrottle; import static android.view.inputmethod.InputMethodEditorTraceProto.InputMethodClientsTraceProto.ClientSideProto.IME_FOCUS_CONTROLLER; @@ -1009,8 +1011,10 @@ public final class ViewRootImpl implements ViewParent, // 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 touch boosting period. + // Used to check if it is in the frame rate 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; @@ -6425,11 +6429,12 @@ public final class ViewRootImpl implements ViewParent, * 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) { + if (!mHasInvalidation && !mIsFrameRateBoosting && !mIsTouchBoosting) { mPreferredFrameRateCategory = FRAME_RATE_CATEGORY_NO_PREFERENCE; setPreferredFrameRateCategory(mPreferredFrameRateCategory); mHasIdledMessage = false; @@ -7452,7 +7457,7 @@ public final class ViewRootImpl implements ViewParent, // For the variable refresh rate project if (handled && shouldTouchBoost(action, mWindowAttributes.type)) { // set the frame rate to the maximum value. - mIsFrameRateBoosting = true; + mIsTouchBoosting = true; setPreferredFrameRateCategory(mPreferredFrameRateCategory); } /** @@ -11509,6 +11514,15 @@ public final class ViewRootImpl implements ViewParent, event.setContentChangeTypes(mChangeTypes); if (mAction.isPresent()) event.setAction(mAction.getAsInt()); if (AccessibilityEvent.DEBUG_ORIGIN) event.originStackTrace = mOrigin; + + if (fixMergedContentChangeEvent()) { + if ((mChangeTypes & AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE) != 0) { + final View importantParent = source.getSelfOrParentImportantForA11y(); + if (importantParent != null) { + source = importantParent; + } + } + } source.sendAccessibilityEventUnchecked(event); } else { mLastEventTimeMillis = 0; @@ -11543,14 +11557,29 @@ public final class ViewRootImpl implements ViewParent, } if (mSource != null) { - // If there is no common predecessor, then mSource points to - // a removed view, hence in this case always prefer the source. - View predecessor = getCommonPredecessor(mSource, source); - if (predecessor != null) { - predecessor = predecessor.getSelfOrParentImportantForA11y(); + if (fixMergedContentChangeEvent()) { + View newSource = getCommonPredecessor(mSource, source); + if (newSource == null) { + // If there is no common predecessor, then mSource points to + // a removed view, hence in this case always prefer the source. + newSource = source; + } + + mChangeTypes |= changeType; + if (mSource != newSource) { + mChangeTypes |= AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE; + mSource = newSource; + } + } else { + // If there is no common predecessor, then mSource points to + // a removed view, hence in this case always prefer the source. + View predecessor = getCommonPredecessor(mSource, source); + if (predecessor != null) { + predecessor = predecessor.getSelfOrParentImportantForA11y(); + } + mSource = (predecessor != null) ? predecessor : source; + mChangeTypes |= changeType; } - mSource = (predecessor != null) ? predecessor : source; - mChangeTypes |= changeType; final int performingAction = mAccessibilityManager.getPerformingAction(); if (performingAction != 0) { @@ -12177,8 +12206,16 @@ public final class ViewRootImpl implements ViewParent, return; } - int frameRateCategory = mIsFrameRateBoosting || mInsetsAnimationRunning - ? FRAME_RATE_CATEGORY_HIGH : preferredFrameRateCategory; + 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; + } try { if (mLastPreferredFrameRateCategory != frameRateCategory) { diff --git a/core/java/android/view/accessibility/flags/accessibility_flags.aconfig b/core/java/android/view/accessibility/flags/accessibility_flags.aconfig index c7355c144c5f..efae57c9946c 100644 --- a/core/java/android/view/accessibility/flags/accessibility_flags.aconfig +++ b/core/java/android/view/accessibility/flags/accessibility_flags.aconfig @@ -53,6 +53,13 @@ flag { flag { namespace: "accessibility" + name: "fix_merged_content_change_event" + description: "Fixes event type and source of content change event merged in ViewRootImpl" + bug: "277305460" +} + +flag { + namespace: "accessibility" name: "flash_notification_system_api" description: "Makes flash notification APIs as system APIs for calling from mainline module" bug: "303131332" diff --git a/core/java/android/webkit/URLUtil.java b/core/java/android/webkit/URLUtil.java index 828ec265f4c8..c6271d27cb37 100644 --- a/core/java/android/webkit/URLUtil.java +++ b/core/java/android/webkit/URLUtil.java @@ -16,20 +16,40 @@ package android.webkit; +import android.annotation.FlaggedApi; +import android.annotation.NonNull; import android.annotation.Nullable; +import android.compat.Compatibility; +import android.compat.annotation.ChangeId; +import android.compat.annotation.EnabledSince; import android.compat.annotation.UnsupportedAppUsage; import android.net.ParseException; import android.net.Uri; import android.net.WebAddress; +import android.os.Build; import android.util.Log; import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.nio.charset.Charset; import java.util.Locale; import java.util.regex.Matcher; import java.util.regex.Pattern; public final class URLUtil { + /** + * This feature enables parsing of Content-Disposition headers that conform to RFC 6266. In + * particular, this enables parsing of {@code filename*} values which can use a different + * character encoding. + * + * @hide + */ + @ChangeId + @EnabledSince(targetSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM) + @FlaggedApi(android.os.Flags.FLAG_ANDROID_OS_BUILD_VANILLA_ICE_CREAM) + public static final long PARSE_CONTENT_DISPOSITION_USING_RFC_6266 = 319400769L; + private static final String LOGTAG = "webkit"; private static final boolean TRACE = false; @@ -293,21 +313,58 @@ public final class URLUtil { /** * Guesses canonical filename that a download would have, using the URL and contentDisposition. - * File extension, if not defined, is added based on the mimetype + * + * <p>File extension, if not defined, is added based on the mimetype. + * + * <p>The {@code contentDisposition} argument will be treated differently depending on + * targetSdkVersion. + * + * <ul> + * <li>For targetSDK versions < {@code VANILLA_ICE_CREAM} it will be parsed based on RFC + * 2616. + * <li>For targetSDK versions >= {@code VANILLA_ICE_CREAM} it will be parsed based on RFC + * 6266. + * </ul> + * + * In practice, this means that from {@code VANILLA_ICE_CREAM}, this method will be able to + * parse {@code filename*} directives in the {@code contentDisposition} string. + * + * <p>The function also changed in the following ways in {@code VANILLA_ICE_CREAM}: + * + * <ul> + * <li>If the suggested file type extension doesn't match the passed {@code mimeType}, the + * method will append the appropriate extension instead of replacing the current + * extension. + * <li>If the suggested file name contains a path separator ({@code "/"}), the method will + * replace this with the underscore character ({@code "_"}) instead of splitting the + * result and only using the last part. + * </ul> * * @param url Url to the content * @param contentDisposition Content-Disposition HTTP header or {@code null} * @param mimeType Mime-type of the content or {@code null} * @return suggested filename */ - public static final String guessFileName( + public static String guessFileName( + String url, @Nullable String contentDisposition, @Nullable String mimeType) { + if (android.os.Flags.androidOsBuildVanillaIceCream()) { + if (Compatibility.isChangeEnabled(PARSE_CONTENT_DISPOSITION_USING_RFC_6266)) { + return guessFileNameRfc6266(url, contentDisposition, mimeType); + } + } + + return guessFileNameRfc2616(url, contentDisposition, mimeType); + } + + /** Legacy implementation of guessFileName, based on RFC 2616. */ + private static String guessFileNameRfc2616( String url, @Nullable String contentDisposition, @Nullable String mimeType) { String filename = null; String extension = null; // If we couldn't do anything with the hint, move toward the content disposition if (contentDisposition != null) { - filename = parseContentDisposition(contentDisposition); + filename = parseContentDispositionRfc2616(contentDisposition); if (filename != null) { int index = filename.lastIndexOf('/') + 1; if (index > 0) { @@ -384,6 +441,128 @@ public final class URLUtil { return filename + extension; } + /** + * Guesses canonical filename that a download would have, using the URL and contentDisposition. + * Uses RFC 6266 for parsing the contentDisposition header value. + */ + @NonNull + private static String guessFileNameRfc6266( + @NonNull String url, @Nullable String contentDisposition, @Nullable String mimeType) { + String filename = getFilenameSuggestion(url, contentDisposition); + // Split filename between base and extension + // Add an extension if filename does not have one + String extensionFromMimeType = suggestExtensionFromMimeType(mimeType); + + if (filename.indexOf('.') < 0) { + // Filename does not have an extension, use the suggested one. + return filename + extensionFromMimeType; + } + + // Filename already contains at least one dot. + // Compare the last segment of the extension against the mime type. + // If there's a mismatch, add the suggested extension instead. + if (mimeType != null && extensionDifferentFromMimeType(filename, mimeType)) { + return filename + extensionFromMimeType; + } + return filename; + } + + /** + * Get the suggested file name from the {@code contentDisposition} or {@code url}. Will ensure + * that the filename contains no path separators by replacing them with the {@code "_"} + * character. + */ + @NonNull + private static String getFilenameSuggestion(String url, @Nullable String contentDisposition) { + // First attempt to parse the Content-Disposition header if available + if (contentDisposition != null) { + String filename = getFilenameFromContentDispositionRfc6266(contentDisposition); + if (filename != null) { + return replacePathSeparators(filename); + } + } + + // Try to generate a filename based on the URL. + if (url != null) { + Uri parsedUri = Uri.parse(url); + String lastPathSegment = parsedUri.getLastPathSegment(); + if (lastPathSegment != null) { + return replacePathSeparators(lastPathSegment); + } + } + + // Finally, if couldn't get filename from URI, get a generic filename. + return "downloadfile"; + } + + /** + * Replace all instances of {@code "/"} with {@code "_"} to avoid filenames that navigate the + * path. + */ + @NonNull + private static String replacePathSeparators(@NonNull String raw) { + return raw.replaceAll("/", "_"); + } + + /** + * Check if the {@code filename} has an extension that is different from the expected one based + * on the {@code mimeType}. + */ + private static boolean extensionDifferentFromMimeType( + @NonNull String filename, @NonNull String mimeType) { + int lastDotIndex = filename.lastIndexOf('.'); + String typeFromExt = + MimeTypeMap.getSingleton() + .getMimeTypeFromExtension(filename.substring(lastDotIndex + 1)); + return typeFromExt != null && !typeFromExt.equalsIgnoreCase(mimeType); + } + + /** + * Get a candidate file extension (including the {@code .}) for the given mimeType. will return + * {@code ".bin"} if {@code mimeType} is {@code null} + * + * @param mimeType Reported mimetype + * @return A file extension, including the {@code .} + */ + @NonNull + private static String suggestExtensionFromMimeType(@Nullable String mimeType) { + if (mimeType == null) { + return ".bin"; + } + String extensionFromMimeType = + MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType); + if (extensionFromMimeType != null) { + return "." + extensionFromMimeType; + } + if (mimeType.equalsIgnoreCase("text/html")) { + return ".html"; + } else if (mimeType.toLowerCase(Locale.ROOT).startsWith("text/")) { + return ".txt"; + } else { + return ".bin"; + } + } + + /** + * Parse the Content-Disposition HTTP Header. + * + * <p>Behavior depends on targetSdkVersion. + * + * <ul> + * <li>For targetSDK versions < {@code VANILLA_ICE_CREAM} it will parse based on RFC 2616. + * <li>For targetSDK versions >= {@code VANILLA_ICE_CREAM} it will parse based on RFC 6266. + * </ul> + */ + @UnsupportedAppUsage + static String parseContentDisposition(String contentDisposition) { + if (android.os.Flags.androidOsBuildVanillaIceCream()) { + if (Compatibility.isChangeEnabled(PARSE_CONTENT_DISPOSITION_USING_RFC_6266)) { + return getFilenameFromContentDispositionRfc6266(contentDisposition); + } + } + return parseContentDispositionRfc2616(contentDisposition); + } + /** Regex used to parse content-disposition headers */ private static final Pattern CONTENT_DISPOSITION_PATTERN = Pattern.compile( @@ -391,15 +570,14 @@ public final class URLUtil { Pattern.CASE_INSENSITIVE); /** - * Parse the Content-Disposition HTTP Header. The format of the header is defined here: - * http://www.w3.org/Protocols/rfc2616/rfc2616-sec19.html This header provides a filename for - * content that is going to be downloaded to the file system. We only support the attachment - * type. Note that RFC 2616 specifies the filename value must be double-quoted. Unfortunately - * some servers do not quote the value so to maintain consistent behaviour with other browsers, - * we allow unquoted values too. + * Parse the Content-Disposition HTTP Header. The format of the header is defined here: <a + * href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec19.html">rfc2616 Section 19</a>. This + * header provides a filename for content that is going to be downloaded to the file system. We + * only support the attachment type. Note that RFC 2616 specifies the filename value must be + * double-quoted. Unfortunately some servers do not quote the value so to maintain consistent + * behaviour with other browsers, we allow unquoted values too. */ - @UnsupportedAppUsage - static String parseContentDisposition(String contentDisposition) { + private static String parseContentDispositionRfc2616(String contentDisposition) { try { Matcher m = CONTENT_DISPOSITION_PATTERN.matcher(contentDisposition); if (m.find()) { @@ -410,4 +588,136 @@ public final class URLUtil { } return null; } + + /** + * Pattern for parsing individual content disposition key-value pairs. + * + * <p>The pattern will attempt to parse the value as either single-, double-, or unquoted. For + * the single- and double-quoted options, the pattern allows escaped quotes as part of the + * value, as per <a href="https://datatracker.ietf.org/doc/html/rfc2616#section-2.2">rfc2616 + * section-2.2</a> + */ + @SuppressWarnings("RegExpRepeatedSpace") // Spaces are only for readability. + private static final Pattern DISPOSITION_PATTERN = + Pattern.compile( + """ + \\s*(\\S+?) # Group 1: parameter name + \\s*=\\s* # Match equals sign + (?: # non-capturing group of options + '( (?: [^'\\\\] | \\\\. )* )' # Group 2: single-quoted + | "( (?: [^"\\\\] | \\\\. )* )" # Group 3: double-quoted + | ( [^'"][^;\\s]* ) # Group 4: un-quoted parameter + )\\s*;? # Optional end semicolon""", + Pattern.COMMENTS); + + /** + * Extract filename from a {@code Content-Disposition} header value. + * + * <p>This method implements the parsing defined in <a + * href="https://datatracker.ietf.org/doc/html/rfc6266">RFC 6266</a>, supporting both the {@code + * filename} and {@code filename*} disposition parameters. If the passed header value has the + * {@code "inline"} disposition type, this method will return {@code null} to indicate that a + * download was not intended. + * + * <p>If both {@code filename*} and {@code filename} is present, the former will be returned, as + * per the RFC. Invalid encoded values will be ignored. + * + * @param contentDisposition Value of {@code Content-Disposition} header. + * @return The filename suggested by the header or {@code null} if no filename could be parsed + * from the header value. + */ + @Nullable + private static String getFilenameFromContentDispositionRfc6266( + @NonNull String contentDisposition) { + String[] parts = contentDisposition.trim().split(";", 2); + if (parts.length < 2) { + // Need at least 2 parts, the `disposition-type` and at least one `disposition-parm`. + return null; + } + String dispositionType = parts[0].trim(); + if ("inline".equalsIgnoreCase(dispositionType)) { + // "inline" should not result in a download. + // Unknown disposition types should be handles as "attachment" + // https://datatracker.ietf.org/doc/html/rfc6266#section-4.2 + return null; + } + String dispositionParameters = parts[1]; + Matcher matcher = DISPOSITION_PATTERN.matcher(dispositionParameters); + String filename = null; + String filenameExt = null; + while (matcher.find()) { + String parameter = matcher.group(1); + String value; + if (matcher.group(2) != null) { + value = removeSlashEscapes(matcher.group(2)); // Value was single-quoted + } else if (matcher.group(3) != null) { + value = removeSlashEscapes(matcher.group(3)); // Value was double-quoted + } else { + value = matcher.group(4); // Value was un-quoted + } + + if (parameter == null || value == null) { + continue; + } + + if ("filename*".equalsIgnoreCase(parameter)) { + filenameExt = parseExtValueString(value); + } else if ("filename".equalsIgnoreCase(parameter)) { + filename = value; + } + } + + // RFC 6266 dictates the filenameExt should be preferred if present. + if (filenameExt != null) { + return filenameExt; + } + return filename; + } + + /** Replace escapes of the \X form with X. */ + private static String removeSlashEscapes(String raw) { + if (raw == null) { + return null; + } + return raw.replaceAll("\\\\(.)", "$1"); + } + + /** + * Parse an extended value string which can be percent-encoded. Return {@code} null if unable to + * parse the string. + */ + private static String parseExtValueString(String raw) { + String[] parts = raw.split("'", 3); + if (parts.length < 3) { + return null; + } + + String encoding = parts[0]; + // Intentionally ignore parts[1] (language). + String valueChars = parts[2]; + + try { + // The URLDecoder force-decodes + as " " + // so preemptively replace all values with the encoded value to preserve them. + Charset charset = Charset.forName(encoding); + String valueWithEncodedPlus = encodePlusCharacters(valueChars, charset); + return URLDecoder.decode(valueWithEncodedPlus, charset); + } catch (RuntimeException ignored) { + return null; // Ignoring an un-parsable value is within spec. + } + } + + /** + * Replace all instances of {@code "+"} with the percent-encoded equivalent for the given {@code + * charset}. + */ + @NonNull + private static String encodePlusCharacters(@NonNull String valueChars, Charset charset) { + StringBuilder sb = new StringBuilder(); + for (byte b : charset.encode("+").array()) { + // Formatting a byte is not possible with TextUtils.formatSimple + sb.append(String.format("%02x", b)); + } + return valueChars.replaceAll("\\+", sb.toString()); + } } diff --git a/core/java/android/webkit/WebViewFactory.java b/core/java/android/webkit/WebViewFactory.java index 0ef37d14420c..53b047a17f6d 100644 --- a/core/java/android/webkit/WebViewFactory.java +++ b/core/java/android/webkit/WebViewFactory.java @@ -16,6 +16,8 @@ package android.webkit; +import static android.webkit.Flags.updateServiceV2; + import android.annotation.NonNull; import android.annotation.SystemApi; import android.annotation.UptimeMillisLong; @@ -33,6 +35,7 @@ import android.os.RemoteException; import android.os.ServiceManager; import android.os.SystemClock; import android.os.Trace; +import android.text.TextUtils; import android.util.AndroidRuntimeException; import android.util.ArraySet; import android.util.Log; @@ -410,6 +413,21 @@ public final class WebViewFactory { } } + // Returns whether the given package is enabled. + // This state can be changed by the user from Settings->Apps + private static boolean isEnabledPackage(PackageInfo packageInfo) { + if (packageInfo == null) return false; + return packageInfo.applicationInfo.enabled; + } + + // Return {@code true} if the package is installed and not hidden + private static boolean isInstalledPackage(PackageInfo packageInfo) { + if (packageInfo == null) return false; + return (((packageInfo.applicationInfo.flags & ApplicationInfo.FLAG_INSTALLED) != 0) + && ((packageInfo.applicationInfo.privateFlags & ApplicationInfo.PRIVATE_FLAG_HIDDEN) + == 0)); + } + @UnsupportedAppUsage private static Context getWebViewContextAndSetProvider() throws MissingWebViewPackageException { Application initialApplication = AppGlobals.getInitialApplication(); @@ -456,6 +474,21 @@ public final class WebViewFactory { Trace.traceEnd(Trace.TRACE_TAG_WEBVIEW); } + if (updateServiceV2() && !isInstalledPackage(newPackageInfo)) { + throw new MissingWebViewPackageException( + TextUtils.formatSimple( + "Current WebView Package (%s) is not installed for the current " + + "user", + newPackageInfo.packageName)); + } + + if (updateServiceV2() && !isEnabledPackage(newPackageInfo)) { + throw new MissingWebViewPackageException( + TextUtils.formatSimple( + "Current WebView Package (%s) is not enabled for the current user", + newPackageInfo.packageName)); + } + // Validate the newly fetched package info, throws MissingWebViewPackageException on // failure verifyPackageInfo(response.packageInfo, newPackageInfo); diff --git a/core/java/android/window/IScreenRecordingCallback.aidl b/core/java/android/window/IScreenRecordingCallback.aidl new file mode 100644 index 000000000000..560ee75aa365 --- /dev/null +++ b/core/java/android/window/IScreenRecordingCallback.aidl @@ -0,0 +1,24 @@ +/* + * 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 android.window; + +/** + * @hide + */ +oneway interface IScreenRecordingCallback { + void onScreenRecordingStateChanged(boolean visibleInScreenRecording); +} diff --git a/core/java/android/window/TaskFragmentOperation.java b/core/java/android/window/TaskFragmentOperation.java index acc6a749e9b7..7b8cdff8e20b 100644 --- a/core/java/android/window/TaskFragmentOperation.java +++ b/core/java/android/window/TaskFragmentOperation.java @@ -125,6 +125,16 @@ public final class TaskFragmentOperation implements Parcelable { */ public static final int OP_TYPE_SET_DIM_ON_TASK = 16; + /** + * Sets this TaskFragment to move to bottom of the Task if any of the activities below it is + * launched in a mode requiring clear top. + * + * This is only allowed for system organizers. See + * {@link com.android.server.wm.TaskFragmentOrganizerController#registerOrganizer( + * ITaskFragmentOrganizer, boolean)} + */ + public static final int OP_TYPE_SET_MOVE_TO_BOTTOM_IF_CLEAR_WHEN_LAUNCH = 17; + @IntDef(prefix = { "OP_TYPE_" }, value = { OP_TYPE_UNKNOWN, OP_TYPE_CREATE_TASK_FRAGMENT, @@ -144,6 +154,7 @@ public final class TaskFragmentOperation implements Parcelable { OP_TYPE_CREATE_TASK_FRAGMENT_DECOR_SURFACE, OP_TYPE_REMOVE_TASK_FRAGMENT_DECOR_SURFACE, OP_TYPE_SET_DIM_ON_TASK, + OP_TYPE_SET_MOVE_TO_BOTTOM_IF_CLEAR_WHEN_LAUNCH, }) @Retention(RetentionPolicy.SOURCE) public @interface OperationType {} @@ -173,12 +184,14 @@ public final class TaskFragmentOperation implements Parcelable { private final boolean mDimOnTask; + private final boolean mMoveToBottomIfClearWhenLaunch; + private TaskFragmentOperation(@OperationType int opType, @Nullable TaskFragmentCreationParams taskFragmentCreationParams, @Nullable IBinder activityToken, @Nullable Intent activityIntent, @Nullable Bundle bundle, @Nullable IBinder secondaryFragmentToken, @Nullable TaskFragmentAnimationParams animationParams, - boolean isolatedNav, boolean dimOnTask) { + boolean isolatedNav, boolean dimOnTask, boolean moveToBottomIfClearWhenLaunch) { mOpType = opType; mTaskFragmentCreationParams = taskFragmentCreationParams; mActivityToken = activityToken; @@ -188,6 +201,7 @@ public final class TaskFragmentOperation implements Parcelable { mAnimationParams = animationParams; mIsolatedNav = isolatedNav; mDimOnTask = dimOnTask; + mMoveToBottomIfClearWhenLaunch = moveToBottomIfClearWhenLaunch; } private TaskFragmentOperation(Parcel in) { @@ -200,6 +214,7 @@ public final class TaskFragmentOperation implements Parcelable { mAnimationParams = in.readTypedObject(TaskFragmentAnimationParams.CREATOR); mIsolatedNav = in.readBoolean(); mDimOnTask = in.readBoolean(); + mMoveToBottomIfClearWhenLaunch = in.readBoolean(); } @Override @@ -213,6 +228,7 @@ public final class TaskFragmentOperation implements Parcelable { dest.writeTypedObject(mAnimationParams, flags); dest.writeBoolean(mIsolatedNav); dest.writeBoolean(mDimOnTask); + dest.writeBoolean(mMoveToBottomIfClearWhenLaunch); } @NonNull @@ -300,6 +316,14 @@ public final class TaskFragmentOperation implements Parcelable { return mDimOnTask; } + /** + * Returns whether the TaskFragment should move to bottom of task when any activity below it + * is launched in clear top mode. + */ + public boolean isMoveToBottomIfClearWhenLaunch() { + return mMoveToBottomIfClearWhenLaunch; + } + @Override public String toString() { final StringBuilder sb = new StringBuilder(); @@ -324,6 +348,7 @@ public final class TaskFragmentOperation implements Parcelable { } sb.append(", isolatedNav=").append(mIsolatedNav); sb.append(", dimOnTask=").append(mDimOnTask); + sb.append(", moveToBottomIfClearWhenLaunch=").append(mMoveToBottomIfClearWhenLaunch); sb.append('}'); return sb.toString(); @@ -332,7 +357,8 @@ public final class TaskFragmentOperation implements Parcelable { @Override public int hashCode() { return Objects.hash(mOpType, mTaskFragmentCreationParams, mActivityToken, mActivityIntent, - mBundle, mSecondaryFragmentToken, mAnimationParams, mIsolatedNav, mDimOnTask); + mBundle, mSecondaryFragmentToken, mAnimationParams, mIsolatedNav, mDimOnTask, + mMoveToBottomIfClearWhenLaunch); } @Override @@ -349,7 +375,8 @@ public final class TaskFragmentOperation implements Parcelable { && Objects.equals(mSecondaryFragmentToken, other.mSecondaryFragmentToken) && Objects.equals(mAnimationParams, other.mAnimationParams) && mIsolatedNav == other.mIsolatedNav - && mDimOnTask == other.mDimOnTask; + && mDimOnTask == other.mDimOnTask + && mMoveToBottomIfClearWhenLaunch == other.mMoveToBottomIfClearWhenLaunch; } @Override @@ -385,6 +412,8 @@ public final class TaskFragmentOperation implements Parcelable { private boolean mDimOnTask; + private boolean mMoveToBottomIfClearWhenLaunch; + /** * @param opType the {@link OperationType} of this {@link TaskFragmentOperation}. */ @@ -466,13 +495,23 @@ public final class TaskFragmentOperation implements Parcelable { } /** + * Sets whether the TaskFragment should move to bottom of task when any activity below it + * is launched in clear top mode. + */ + @NonNull + public Builder setMoveToBottomIfClearWhenLaunch(boolean moveToBottomIfClearWhenLaunch) { + mMoveToBottomIfClearWhenLaunch = moveToBottomIfClearWhenLaunch; + return this; + } + + /** * Constructs the {@link TaskFragmentOperation}. */ @NonNull public TaskFragmentOperation build() { return new TaskFragmentOperation(mOpType, mTaskFragmentCreationParams, mActivityToken, mActivityIntent, mBundle, mSecondaryFragmentToken, mAnimationParams, - mIsolatedNav, mDimOnTask); + mIsolatedNav, mDimOnTask, mMoveToBottomIfClearWhenLaunch); } } } diff --git a/core/java/android/window/TransitionInfo.java b/core/java/android/window/TransitionInfo.java index bceb8726b1cb..feae173f3e61 100644 --- a/core/java/android/window/TransitionInfo.java +++ b/core/java/android/window/TransitionInfo.java @@ -421,8 +421,11 @@ public final class TransitionInfo implements Parcelable { final String perChangeLineStart = shouldPrettyPrint ? "\n" + innerPrefix : ""; StringBuilder sb = new StringBuilder(); sb.append("{id=").append(mDebugId).append(" t=").append(transitTypeToString(mType)) - .append(" f=0x").append(Integer.toHexString(mFlags)).append(" trk=").append(mTrack) - .append(" r=["); + .append(" f=0x").append(Integer.toHexString(mFlags)).append(" trk=").append(mTrack); + if (mOptions != null) { + sb.append(" opt=").append(mOptions); + } + sb.append(" r=["); for (int i = 0; i < mRoots.size(); ++i) { if (i > 0) { sb.append(','); @@ -1211,21 +1214,31 @@ public final class TransitionInfo implements Parcelable { @NonNull private static String typeToString(int mode) { - switch(mode) { - case ANIM_CUSTOM: return "ANIM_CUSTOM"; - case ANIM_CLIP_REVEAL: return "ANIM_CLIP_REVEAL"; - case ANIM_SCALE_UP: return "ANIM_SCALE_UP"; - case ANIM_THUMBNAIL_SCALE_UP: return "ANIM_THUMBNAIL_SCALE_UP"; - case ANIM_THUMBNAIL_SCALE_DOWN: return "ANIM_THUMBNAIL_SCALE_DOWN"; - case ANIM_OPEN_CROSS_PROFILE_APPS: return "ANIM_OPEN_CROSS_PROFILE_APPS"; - default: return "<unknown:" + mode + ">"; - } + return switch (mode) { + case ANIM_CUSTOM -> "CUSTOM"; + case ANIM_SCALE_UP -> "SCALE_UP"; + case ANIM_THUMBNAIL_SCALE_UP -> "THUMBNAIL_SCALE_UP"; + case ANIM_THUMBNAIL_SCALE_DOWN -> "THUMBNAIL_SCALE_DOWN"; + case ANIM_SCENE_TRANSITION -> "SCENE_TRANSITION"; + case ANIM_CLIP_REVEAL -> "CLIP_REVEAL"; + case ANIM_OPEN_CROSS_PROFILE_APPS -> "OPEN_CROSS_PROFILE_APPS"; + case ANIM_FROM_STYLE -> "FROM_STYLE"; + default -> "<" + mode + ">"; + }; } @Override public String toString() { - return "{ AnimationOptions type= " + typeToString(mType) + " package=" + mPackageName - + " override=" + mOverrideTaskTransition + " b=" + mTransitionBounds + "}"; + final StringBuilder sb = new StringBuilder(32); + sb.append("{t=").append(typeToString(mType)); + if (mOverrideTaskTransition) { + sb.append(" overrideTask=true"); + } + if (!mTransitionBounds.isEmpty()) { + sb.append(" bounds=").append(mTransitionBounds); + } + sb.append('}'); + return sb.toString(); } /** Customized activity transition. */ 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 4b5595feb966..cbf6367b3d60 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 @@ -43,3 +43,10 @@ flag { description: "Whether the API PackageManager.USER_MIN_ASPECT_RATIO_APP_DEFAULT is available" bug: "310816437" } + +flag { + name: "allow_hide_scm_button" + namespace: "large_screen_experiences_app_compat" + description: "Whether we should allow hiding the size compat restart button" + bug: "318840081" +} diff --git a/core/java/android/window/flags/window_surfaces.aconfig b/core/java/android/window/flags/window_surfaces.aconfig index 3c3c8469b3a4..751c1a8dd0ff 100644 --- a/core/java/android/window/flags/window_surfaces.aconfig +++ b/core/java/android/window/flags/window_surfaces.aconfig @@ -80,3 +80,11 @@ flag { is_fixed_read_only: true bug: "278757236" } + +flag { + namespace: "window_surfaces" + name: "screen_recording_callbacks" + description: "Enable screen recording callbacks public API" + is_fixed_read_only: true + bug: "304574518" +} diff --git a/core/java/android/window/flags/windowing_frontend.aconfig b/core/java/android/window/flags/windowing_frontend.aconfig index bb16ad2cb8de..2c5fbd7f3725 100644 --- a/core/java/android/window/flags/windowing_frontend.aconfig +++ b/core/java/android/window/flags/windowing_frontend.aconfig @@ -69,11 +69,10 @@ flag { } flag { - name: "predictive_back_system_animations" + name: "predictive_back_system_anims" namespace: "systemui" description: "Predictive back for system animations" - bug: "319421778" - is_fixed_read_only: true + bug: "320510464" } flag { @@ -90,4 +89,12 @@ flag { description: "Feature flag to enable a multi-instance system ui component property." bug: "262864589" is_fixed_read_only: true +} + +flag { + name: "insets_decoupled_configuration" + namespace: "windowing_frontend" + description: "Configuration decoupled from insets" + bug: "151861875" + is_fixed_read_only: true }
\ No newline at end of file diff --git a/core/java/com/android/internal/os/MonotonicClock.java b/core/java/com/android/internal/os/MonotonicClock.java index c3bcfa6b2cc2..eca6f58dd50f 100644 --- a/core/java/com/android/internal/os/MonotonicClock.java +++ b/core/java/com/android/internal/os/MonotonicClock.java @@ -17,11 +17,11 @@ package com.android.internal.os; import android.annotation.NonNull; +import android.annotation.Nullable; import android.util.AtomicFile; import android.util.Log; import android.util.Xml; -import com.android.internal.annotations.VisibleForTesting; import com.android.modules.utils.TypedXmlPullParser; import com.android.modules.utils.TypedXmlSerializer; @@ -49,20 +49,27 @@ public class MonotonicClock { private final AtomicFile mFile; private final Clock mClock; - private long mTimeshift; + private final long mTimeshift; public static final long UNDEFINED = -1; public MonotonicClock(File file) { - mFile = new AtomicFile(file); - mClock = Clock.SYSTEM_CLOCK; - read(); + this (file, Clock.SYSTEM_CLOCK.elapsedRealtime(), Clock.SYSTEM_CLOCK); } public MonotonicClock(long monotonicTime, @NonNull Clock clock) { + this(null, monotonicTime, clock); + } + + public MonotonicClock(@Nullable File file, long monotonicTime, @NonNull Clock clock) { mClock = clock; - mFile = null; - mTimeshift = monotonicTime - mClock.elapsedRealtime(); + if (file != null) { + mFile = new AtomicFile(file); + mTimeshift = read(monotonicTime - mClock.elapsedRealtime()); + } else { + mFile = null; + mTimeshift = monotonicTime - mClock.elapsedRealtime(); + } } /** @@ -81,15 +88,16 @@ public class MonotonicClock { return mTimeshift + elapsedRealtimeMs; } - private void read() { + private long read(long defaultTimeshift) { if (!mFile.exists()) { - return; + return defaultTimeshift; } try { - readXml(new ByteArrayInputStream(mFile.readFully()), Xml.newBinaryPullParser()); + return readXml(new ByteArrayInputStream(mFile.readFully()), Xml.newBinaryPullParser()); } catch (IOException e) { Log.e(TAG, "Cannot load monotonic clock from " + mFile.getBaseFile(), e); + return defaultTimeshift; } } @@ -102,18 +110,21 @@ public class MonotonicClock { return; } - try (FileOutputStream out = mFile.startWrite()) { + FileOutputStream out = null; + try { + out = mFile.startWrite(); writeXml(out, Xml.newBinarySerializer()); + mFile.finishWrite(out); } catch (IOException e) { Log.e(TAG, "Cannot write monotonic clock to " + mFile.getBaseFile(), e); + mFile.failWrite(out); } } /** * Parses an XML file containing the persistent state of the monotonic clock. */ - @VisibleForTesting - public void readXml(InputStream inputStream, TypedXmlPullParser parser) throws IOException { + private long readXml(InputStream inputStream, TypedXmlPullParser parser) throws IOException { long savedTimeshift = 0; try { parser.setInput(inputStream, StandardCharsets.UTF_8.name()); @@ -128,14 +139,13 @@ public class MonotonicClock { } catch (XmlPullParserException e) { throw new IOException(e); } - mTimeshift = savedTimeshift - mClock.elapsedRealtime(); + return savedTimeshift - mClock.elapsedRealtime(); } /** * Creates an XML file containing the persistent state of the monotonic clock. */ - @VisibleForTesting - public void writeXml(OutputStream out, TypedXmlSerializer serializer) throws IOException { + private void writeXml(OutputStream out, TypedXmlSerializer serializer) throws IOException { serializer.setOutput(out, StandardCharsets.UTF_8.name()); serializer.startDocument(null, true); serializer.startTag(null, XML_TAG_MONOTONIC_TIME); diff --git a/core/java/com/android/internal/pm/pkg/component/ParsedAttributionImpl.java b/core/java/com/android/internal/pm/pkg/component/ParsedAttributionImpl.java index e3bfb38802db..e419d13730c2 100644 --- a/core/java/com/android/internal/pm/pkg/component/ParsedAttributionImpl.java +++ b/core/java/com/android/internal/pm/pkg/component/ParsedAttributionImpl.java @@ -38,7 +38,7 @@ import java.util.List; public class ParsedAttributionImpl implements ParsedAttribution, Parcelable { /** Maximum amount of attributions per package */ - static final int MAX_NUM_ATTRIBUTIONS = 10000; + static final int MAX_NUM_ATTRIBUTIONS = 400; /** Tag of the attribution */ private @NonNull String tag; 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 12aff1c6669f..5d82d0469d56 100644 --- a/core/java/com/android/internal/pm/pkg/component/ParsedProviderUtils.java +++ b/core/java/com/android/internal/pm/pkg/component/ParsedProviderUtils.java @@ -29,7 +29,6 @@ import android.content.pm.parsing.result.ParseResult; 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; @@ -127,10 +126,6 @@ public class ParsedProviderUtils { .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 4ac542f84226..a1dd19a3bc90 100644 --- a/core/java/com/android/internal/pm/pkg/component/ParsedServiceUtils.java +++ b/core/java/com/android/internal/pm/pkg/component/ParsedServiceUtils.java @@ -29,7 +29,6 @@ import android.content.pm.parsing.result.ParseResult; 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; @@ -106,11 +105,6 @@ public class ParsedServiceUtils { | 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/telephony/IPhoneStateListener.aidl b/core/java/com/android/internal/telephony/IPhoneStateListener.aidl index 03cfd4f2ab91..969f95db002d 100644 --- a/core/java/com/android/internal/telephony/IPhoneStateListener.aidl +++ b/core/java/com/android/internal/telephony/IPhoneStateListener.aidl @@ -80,4 +80,5 @@ oneway interface IPhoneStateListener { void onMediaQualityStatusChanged(in MediaQualityStatus mediaQualityStatus); void onCallBackModeStarted(int type); void onCallBackModeStopped(int type, int reason); + void onSimultaneousCallingStateChanged(in int[] subIds); } diff --git a/core/java/com/android/internal/telephony/ITelephonyRegistry.aidl b/core/java/com/android/internal/telephony/ITelephonyRegistry.aidl index aab22421b334..0203ea49f252 100644 --- a/core/java/com/android/internal/telephony/ITelephonyRegistry.aidl +++ b/core/java/com/android/internal/telephony/ITelephonyRegistry.aidl @@ -104,6 +104,7 @@ interface ITelephonyRegistry { void notifyAllowedNetworkTypesChanged(in int phoneId, in int subId, in int reason, in long allowedNetworkType); void notifyLinkCapacityEstimateChanged(in int phoneId, in int subId, in List<LinkCapacityEstimate> linkCapacityEstimateList); + void notifySimultaneousCellularCallingSubscriptionsChanged(in int[] subIds); void addCarrierPrivilegesCallback( int phoneId, ICarrierPrivilegesCallback callback, String pkg, String featureId); diff --git a/core/java/com/android/internal/util/CollectionUtils.java b/core/java/com/android/internal/util/CollectionUtils.java index b87027cbb6e7..419193688268 100644 --- a/core/java/com/android/internal/util/CollectionUtils.java +++ b/core/java/com/android/internal/util/CollectionUtils.java @@ -120,8 +120,9 @@ public class CollectionUtils { public static @NonNull <I, O> List<O> map(@Nullable List<I> cur, Function<? super I, ? extends O> f) { if (isEmpty(cur)) return Collections.emptyList(); - final ArrayList<O> result = new ArrayList<>(); - for (int i = 0; i < cur.size(); i++) { + final int size = cur.size(); + final ArrayList<O> result = new ArrayList<>(size); + for (int i = 0; i < size; i++) { result.add(f.apply(cur.get(i))); } return result; @@ -133,7 +134,7 @@ public class CollectionUtils { public static @NonNull <I, O> Set<O> map(@Nullable Set<I> cur, Function<? super I, ? extends O> f) { if (isEmpty(cur)) return emptySet(); - ArraySet<O> result = new ArraySet<>(); + ArraySet<O> result = new ArraySet<>(cur.size()); if (cur instanceof ArraySet) { ArraySet<I> arraySet = (ArraySet<I>) cur; int size = arraySet.size(); @@ -163,7 +164,7 @@ public class CollectionUtils { Function<? super I, ? extends O> f) { if (isEmpty(cur)) return Collections.emptyList(); List<O> result = null; - for (int i = 0; i < cur.size(); i++) { + for (int i = 0, size = cur.size(); i < size; i++) { O transformed = f.apply(cur.get(i)); if (transformed != null) { result = add(result, transformed); @@ -239,7 +240,7 @@ public class CollectionUtils { public static @NonNull <T> List<T> filter(@Nullable List<?> list, Class<T> c) { if (isEmpty(list)) return Collections.emptyList(); ArrayList<T> result = null; - for (int i = 0; i < list.size(); i++) { + for (int i = 0, size = list.size(); i < size; i++) { final Object item = list.get(i); if (c.isInstance(item)) { result = ArrayUtils.add(result, (T) item); @@ -273,7 +274,7 @@ public class CollectionUtils { public static @Nullable <T> T find(@Nullable List<T> items, java.util.function.Predicate<T> predicate) { if (isEmpty(items)) return null; - for (int i = 0; i < items.size(); i++) { + for (int i = 0, size = items.size(); i < size; i++) { final T item = items.get(i); if (predicate.test(item)) return item; } diff --git a/core/java/com/android/internal/util/LatencyTracker.java b/core/java/com/android/internal/util/LatencyTracker.java index 58ee2b21296b..13efaf06aeda 100644 --- a/core/java/com/android/internal/util/LatencyTracker.java +++ b/core/java/com/android/internal/util/LatencyTracker.java @@ -26,6 +26,8 @@ import static com.android.internal.util.FrameworkStatsLog.UIACTION_LATENCY_REPOR import static com.android.internal.util.FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_FACE_WAKE_AND_UNLOCK; import static com.android.internal.util.FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_FINGERPRINT_WAKE_AND_UNLOCK; import static com.android.internal.util.FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_FOLD_TO_AOD; +import static com.android.internal.util.FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_NOTIFICATIONS_HIDDEN_FOR_MEASURE; +import static com.android.internal.util.FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_NOTIFICATIONS_HIDDEN_FOR_MEASURE_WITH_SHADE_OPEN; import static com.android.internal.util.FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_KEYGUARD_FPS_UNLOCK_TO_HOME; import static com.android.internal.util.FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_LOAD_SHARE_SHEET; import static com.android.internal.util.FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_LOCKSCREEN_UNLOCK; @@ -234,6 +236,19 @@ public class LatencyTracker { */ public static final int ACTION_BACK_SYSTEM_ANIMATION = 25; + /** + * Time notifications spent in hidden state for performance reasons. We might temporary + * hide notifications after display size changes (e.g. fold/unfold of a foldable device) + * and measure them while they are hidden to unblock rendering of the rest of the UI. + */ + public static final int ACTION_NOTIFICATIONS_HIDDEN_FOR_MEASURE = 26; + + /** + * The same as {@link ACTION_NOTIFICATIONS_HIDDEN_FOR_MEASURE} but tracks time only + * when the notifications are hidden and when the shade is open or keyguard is visible. + */ + public static final int ACTION_NOTIFICATIONS_HIDDEN_FOR_MEASURE_WITH_SHADE_OPEN = 27; + private static final int[] ACTIONS_ALL = { ACTION_EXPAND_PANEL, ACTION_TOGGLE_RECENTS, @@ -261,6 +276,8 @@ public class LatencyTracker { ACTION_NOTIFICATION_BIG_PICTURE_LOADED, ACTION_KEYGUARD_FPS_UNLOCK_TO_HOME, ACTION_BACK_SYSTEM_ANIMATION, + ACTION_NOTIFICATIONS_HIDDEN_FOR_MEASURE, + ACTION_NOTIFICATIONS_HIDDEN_FOR_MEASURE_WITH_SHADE_OPEN, }; /** @hide */ @@ -291,6 +308,8 @@ public class LatencyTracker { ACTION_NOTIFICATION_BIG_PICTURE_LOADED, ACTION_KEYGUARD_FPS_UNLOCK_TO_HOME, ACTION_BACK_SYSTEM_ANIMATION, + ACTION_NOTIFICATIONS_HIDDEN_FOR_MEASURE, + ACTION_NOTIFICATIONS_HIDDEN_FOR_MEASURE_WITH_SHADE_OPEN, }) @Retention(RetentionPolicy.SOURCE) public @interface Action { @@ -324,6 +343,8 @@ public class LatencyTracker { UIACTION_LATENCY_REPORTED__ACTION__ACTION_NOTIFICATION_BIG_PICTURE_LOADED, UIACTION_LATENCY_REPORTED__ACTION__ACTION_KEYGUARD_FPS_UNLOCK_TO_HOME, UIACTION_LATENCY_REPORTED__ACTION__ACTION_BACK_SYSTEM_ANIMATION, + UIACTION_LATENCY_REPORTED__ACTION__ACTION_NOTIFICATIONS_HIDDEN_FOR_MEASURE, + UIACTION_LATENCY_REPORTED__ACTION__ACTION_NOTIFICATIONS_HIDDEN_FOR_MEASURE_WITH_SHADE_OPEN, }; private final Object mLock = new Object(); @@ -514,6 +535,10 @@ public class LatencyTracker { return "ACTION_KEYGUARD_FPS_UNLOCK_TO_HOME"; case UIACTION_LATENCY_REPORTED__ACTION__ACTION_BACK_SYSTEM_ANIMATION: return "ACTION_BACK_SYSTEM_ANIMATION"; + case UIACTION_LATENCY_REPORTED__ACTION__ACTION_NOTIFICATIONS_HIDDEN_FOR_MEASURE: + return "ACTION_NOTIFICATIONS_HIDDEN_FOR_MEASURE"; + case UIACTION_LATENCY_REPORTED__ACTION__ACTION_NOTIFICATIONS_HIDDEN_FOR_MEASURE_WITH_SHADE_OPEN: + return "ACTION_NOTIFICATIONS_HIDDEN_FOR_MEASURE_WITH_SHADE_OPEN"; default: throw new IllegalArgumentException("Invalid action"); } diff --git a/core/java/com/android/internal/util/RingBuffer.java b/core/java/com/android/internal/util/RingBuffer.java index 04886598de35..342ba1b67e4d 100644 --- a/core/java/com/android/internal/util/RingBuffer.java +++ b/core/java/com/android/internal/util/RingBuffer.java @@ -19,7 +19,10 @@ package com.android.internal.util; import static com.android.internal.util.Preconditions.checkArgumentPositive; import java.lang.reflect.Array; +import java.lang.reflect.InvocationTargetException; import java.util.Arrays; +import java.util.function.IntFunction; +import java.util.function.Supplier; /** * A simple ring buffer structure with bounded capacity backed by an array. @@ -30,16 +33,35 @@ import java.util.Arrays; @android.ravenwood.annotation.RavenwoodKeepWholeClass public class RingBuffer<T> { + private final Supplier<T> mNewItem; // Array for storing events. private final T[] mBuffer; // Cursor keeping track of the logical end of the array. This cursor never // wraps and instead keeps track of the total number of append() operations. private long mCursor = 0; + /** + * @deprecated This uses reflection to create new instances. + * Use {@link #RingBuffer(Supplier, IntFunction, int)}} instead. + */ + @Deprecated public RingBuffer(Class<T> c, int capacity) { + this(() -> (T) createNewItem(c), cap -> (T[]) Array.newInstance(c, cap), capacity); + } + + private static Object createNewItem(Class c) { + try { + return c.getDeclaredConstructor().newInstance(); + } catch (IllegalAccessException | InstantiationException | NoSuchMethodException + | InvocationTargetException e) { + return null; + } + } + + public RingBuffer(Supplier<T> newItem, IntFunction<T[]> newBacking, int capacity) { checkArgumentPositive(capacity, "A RingBuffer cannot have 0 capacity"); - // Java cannot create generic arrays without a runtime hint. - mBuffer = (T[]) Array.newInstance(c, capacity); + mBuffer = newBacking.apply(capacity); + mNewItem = newItem; } public int size() { @@ -69,22 +91,11 @@ public class RingBuffer<T> { public T getNextSlot() { final int nextSlotIdx = indexOf(mCursor++); if (mBuffer[nextSlotIdx] == null) { - mBuffer[nextSlotIdx] = createNewItem(); + mBuffer[nextSlotIdx] = mNewItem.get(); } return mBuffer[nextSlotIdx]; } - /** - * @return a new object of type <T> or null if a new object could not be created. - */ - protected T createNewItem() { - try { - return (T) mBuffer.getClass().getComponentType().newInstance(); - } catch (IllegalAccessException | InstantiationException e) { - return null; - } - } - public T[] toArray() { // Only generic way to create a T[] from another T[] T[] out = Arrays.copyOf(mBuffer, size(), (Class<T[]>) mBuffer.getClass()); diff --git a/core/jni/Android.bp b/core/jni/Android.bp index 2a744e343ccd..c8fd246a255b 100644 --- a/core/jni/Android.bp +++ b/core/jni/Android.bp @@ -269,6 +269,9 @@ cc_library_shared_for_libandroid_runtime { "android_window_WindowInfosListener.cpp", "android_window_ScreenCapture.cpp", "jni_common.cpp", + "android_tracing_PerfettoDataSource.cpp", + "android_tracing_PerfettoDataSourceInstance.cpp", + "android_tracing_PerfettoProducer.cpp", ], static_libs: [ @@ -282,6 +285,7 @@ cc_library_shared_for_libandroid_runtime { "libscrypt_static", "libstatssocket_lazy", "libskia", + "libperfetto_client_experimental", ], shared_libs: [ @@ -355,6 +359,7 @@ cc_library_shared_for_libandroid_runtime { "server_configurable_flags", "libimage_io", "libultrahdr", + "libperfetto_c", ], export_shared_lib_headers: [ // our headers include libnativewindow's public headers diff --git a/core/jni/AndroidRuntime.cpp b/core/jni/AndroidRuntime.cpp index 17aad43edb6b..7a16318f3276 100644 --- a/core/jni/AndroidRuntime.cpp +++ b/core/jni/AndroidRuntime.cpp @@ -220,6 +220,9 @@ extern int register_com_android_internal_util_VirtualRefBasePtr(JNIEnv *env); extern int register_android_window_WindowInfosListener(JNIEnv* env); extern int register_android_window_ScreenCapture(JNIEnv* env); extern int register_jni_common(JNIEnv* env); +extern int register_android_tracing_PerfettoDataSource(JNIEnv* env); +extern int register_android_tracing_PerfettoDataSourceInstance(JNIEnv* env); +extern int register_android_tracing_PerfettoProducer(JNIEnv* env); // Namespace for Android Runtime flags applied during boot time. static const char* RUNTIME_NATIVE_BOOT_NAMESPACE = "runtime_native_boot"; @@ -1675,6 +1678,10 @@ static const RegJNIRec gRegJNI[] = { REG_JNI(register_android_window_WindowInfosListener), REG_JNI(register_android_window_ScreenCapture), REG_JNI(register_jni_common), + + REG_JNI(register_android_tracing_PerfettoDataSource), + REG_JNI(register_android_tracing_PerfettoDataSourceInstance), + REG_JNI(register_android_tracing_PerfettoProducer), }; /* diff --git a/core/jni/android_hardware_OverlayProperties.cpp b/core/jni/android_hardware_OverlayProperties.cpp index 5b95ee79f330..f6fe3dd842da 100644 --- a/core/jni/android_hardware_OverlayProperties.cpp +++ b/core/jni/android_hardware_OverlayProperties.cpp @@ -61,13 +61,12 @@ static jboolean android_hardware_OverlayProperties_supportFp16ForHdr(JNIEnv* env static_cast<int32_t>(HAL_PIXEL_FORMAT_RGBA_FP16)) != i.pixelFormats.end() && std::find(i.standards.begin(), i.standards.end(), - static_cast<int32_t>(HAL_DATASPACE_STANDARD_BT2020)) != + static_cast<int32_t>(HAL_DATASPACE_STANDARD_BT709)) != i.standards.end() && std::find(i.transfers.begin(), i.transfers.end(), - static_cast<int32_t>(HAL_DATASPACE_TRANSFER_ST2084)) != - i.transfers.end() && + static_cast<int32_t>(HAL_DATASPACE_TRANSFER_SRGB)) != i.transfers.end() && std::find(i.ranges.begin(), i.ranges.end(), - static_cast<int32_t>(HAL_DATASPACE_RANGE_FULL)) != i.ranges.end()) { + static_cast<int32_t>(HAL_DATASPACE_RANGE_EXTENDED)) != i.ranges.end()) { return true; } } diff --git a/core/jni/android_tracing_PerfettoDataSource.cpp b/core/jni/android_tracing_PerfettoDataSource.cpp new file mode 100644 index 000000000000..d71069866d89 --- /dev/null +++ b/core/jni/android_tracing_PerfettoDataSource.cpp @@ -0,0 +1,437 @@ +/* + * Copyright 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. + */ + +#define LOG_TAG "Perfetto" + +#include "android_tracing_PerfettoDataSource.h" + +#include <android_runtime/AndroidRuntime.h> +#include <android_runtime/Log.h> +#include <nativehelper/JNIHelp.h> +#include <perfetto/public/data_source.h> +#include <perfetto/public/producer.h> +#include <perfetto/public/protos/trace/test_event.pzc.h> +#include <perfetto/public/protos/trace/trace_packet.pzc.h> +#include <perfetto/tracing.h> +#include <utils/Log.h> +#include <utils/RefBase.h> + +#include <sstream> +#include <thread> + +#include "core_jni_helpers.h" + +namespace android { + +static struct { + jclass clazz; + jmethodID createInstance; + jmethodID createTlsState; + jmethodID createIncrementalState; +} gPerfettoDataSourceClassInfo; + +static struct { + jclass clazz; + jmethodID init; + jmethodID getAndClearAllPendingTracePackets; +} gTracingContextClassInfo; + +static struct { + jclass clazz; + jmethodID init; +} gCreateTlsStateArgsClassInfo; + +static struct { + jclass clazz; + jmethodID init; +} gCreateIncrementalStateArgsClassInfo; + +static JavaVM* gVm; + +struct TlsState { + jobject jobj; +}; + +struct IncrementalState { + jobject jobj; +}; + +static void traceAllPendingPackets(JNIEnv* env, jobject jCtx, PerfettoDsTracerIterator* ctx) { + jobjectArray packets = + (jobjectArray)env + ->CallObjectMethod(jCtx, + gTracingContextClassInfo.getAndClearAllPendingTracePackets); + if (env->ExceptionOccurred()) { + env->ExceptionDescribe(); + env->ExceptionClear(); + + LOG_ALWAYS_FATAL("Failed to call java context finalize method"); + } + + int packets_count = env->GetArrayLength(packets); + for (int i = 0; i < packets_count; i++) { + jbyteArray packet_proto_buffer = (jbyteArray)env->GetObjectArrayElement(packets, i); + + jbyte* raw_proto_buffer = env->GetByteArrayElements(packet_proto_buffer, 0); + int buffer_size = env->GetArrayLength(packet_proto_buffer); + + struct PerfettoDsRootTracePacket trace_packet; + PerfettoDsTracerPacketBegin(ctx, &trace_packet); + PerfettoPbMsgAppendBytes(&trace_packet.msg.msg, (const uint8_t*)raw_proto_buffer, + buffer_size); + PerfettoDsTracerPacketEnd(ctx, &trace_packet); + } +} + +PerfettoDataSource::PerfettoDataSource(JNIEnv* env, jobject javaDataSource, + std::string dataSourceName) + : dataSourceName(std::move(dataSourceName)), + mJavaDataSource(env->NewGlobalRef(javaDataSource)) {} + +jobject PerfettoDataSource::newInstance(JNIEnv* env, void* ds_config, size_t ds_config_size, + PerfettoDsInstanceIndex inst_id) { + jbyteArray configArray = env->NewByteArray(ds_config_size); + + void* temp = env->GetPrimitiveArrayCritical((jarray)configArray, 0); + memcpy(temp, ds_config, ds_config_size); + env->ReleasePrimitiveArrayCritical(configArray, temp, 0); + + jobject instance = + env->CallObjectMethod(mJavaDataSource, gPerfettoDataSourceClassInfo.createInstance, + configArray, inst_id); + + if (env->ExceptionCheck()) { + LOGE_EX(env); + env->ExceptionClear(); + LOG_ALWAYS_FATAL("Failed to create new Java Perfetto datasource instance"); + } + + return instance; +} + +jobject PerfettoDataSource::createTlsStateGlobalRef(JNIEnv* env, PerfettoDsInstanceIndex inst_id) { + ScopedLocalRef<jobject> args(env, + env->NewObject(gCreateTlsStateArgsClassInfo.clazz, + gCreateTlsStateArgsClassInfo.init, mJavaDataSource, + inst_id)); + + ScopedLocalRef<jobject> tslState(env, + env->CallObjectMethod(mJavaDataSource, + gPerfettoDataSourceClassInfo + .createTlsState, + args.get())); + + if (env->ExceptionCheck()) { + LOGE_EX(env); + env->ExceptionClear(); + LOG_ALWAYS_FATAL("Failed to create new Java Perfetto incremental state"); + } + + return env->NewGlobalRef(tslState.get()); +} + +jobject PerfettoDataSource::createIncrementalStateGlobalRef(JNIEnv* env, + PerfettoDsInstanceIndex inst_id) { + ScopedLocalRef<jobject> args(env, + env->NewObject(gCreateIncrementalStateArgsClassInfo.clazz, + gCreateIncrementalStateArgsClassInfo.init, + mJavaDataSource, inst_id)); + + ScopedLocalRef<jobject> incrementalState(env, + env->CallObjectMethod(mJavaDataSource, + gPerfettoDataSourceClassInfo + .createIncrementalState, + args.get())); + + if (env->ExceptionCheck()) { + LOGE_EX(env); + env->ExceptionClear(); + LOG_ALWAYS_FATAL("Failed to create Java Perfetto incremental state"); + } + + return env->NewGlobalRef(incrementalState.get()); +} + +void PerfettoDataSource::trace(JNIEnv* env, jobject traceFunction) { + PERFETTO_DS_TRACE(dataSource, ctx) { + TlsState* tls_state = + reinterpret_cast<TlsState*>(PerfettoDsGetCustomTls(&dataSource, &ctx)); + IncrementalState* incr_state = reinterpret_cast<IncrementalState*>( + PerfettoDsGetIncrementalState(&dataSource, &ctx)); + + ScopedLocalRef<jobject> jCtx(env, + env->NewObject(gTracingContextClassInfo.clazz, + gTracingContextClassInfo.init, &ctx, + tls_state->jobj, incr_state->jobj)); + + jclass objclass = env->GetObjectClass(traceFunction); + jmethodID method = + env->GetMethodID(objclass, "trace", "(Landroid/tracing/perfetto/TracingContext;)V"); + if (method == 0) { + LOG_ALWAYS_FATAL("Failed to get method id"); + } + + env->ExceptionClear(); + + env->CallVoidMethod(traceFunction, method, jCtx.get()); + if (env->ExceptionOccurred()) { + env->ExceptionDescribe(); + env->ExceptionClear(); + LOG_ALWAYS_FATAL("Failed to call java trace method"); + } + + traceAllPendingPackets(env, jCtx.get(), &ctx); + } +} + +void PerfettoDataSource::flushAll() { + PERFETTO_DS_TRACE(dataSource, ctx) { + PerfettoDsTracerFlush(&ctx, nullptr, nullptr); + } +} + +PerfettoDataSource::~PerfettoDataSource() { + JNIEnv* env = AndroidRuntime::getJNIEnv(); + env->DeleteWeakGlobalRef(mJavaDataSource); +} + +jlong nativeCreate(JNIEnv* env, jclass clazz, jobject javaDataSource, jstring name) { + const char* nativeString = env->GetStringUTFChars(name, 0); + PerfettoDataSource* dataSource = new PerfettoDataSource(env, javaDataSource, nativeString); + env->ReleaseStringUTFChars(name, nativeString); + + dataSource->incStrong((void*)nativeCreate); + + return reinterpret_cast<jlong>(dataSource); +} + +void nativeDestroy(void* ptr) { + PerfettoDataSource* dataSource = reinterpret_cast<PerfettoDataSource*>(ptr); + dataSource->decStrong((void*)nativeCreate); +} + +static jlong nativeGetFinalizer(JNIEnv* /* env */, jclass /* clazz */) { + return static_cast<jlong>(reinterpret_cast<uintptr_t>(&nativeDestroy)); +} + +void nativeTrace(JNIEnv* env, jclass clazz, jlong dataSourcePtr, jobject traceFunctionInterface) { + sp<PerfettoDataSource> datasource = reinterpret_cast<PerfettoDataSource*>(dataSourcePtr); + + datasource->trace(env, traceFunctionInterface); +} + +void nativeFlush(JNIEnv* env, jclass clazz, jobject jCtx, jlong ctxPtr) { + auto* ctx = reinterpret_cast<struct PerfettoDsTracerIterator*>(ctxPtr); + traceAllPendingPackets(env, jCtx, ctx); + PerfettoDsTracerFlush(ctx, nullptr, nullptr); +} + +void nativeFlushAll(JNIEnv* env, jclass clazz, jlong ptr) { + sp<PerfettoDataSource> datasource = reinterpret_cast<PerfettoDataSource*>(ptr); + datasource->flushAll(); +} + +void nativeRegisterDataSource(JNIEnv* env, jclass clazz, jlong datasource_ptr, + int buffer_exhausted_policy) { + sp<PerfettoDataSource> datasource = reinterpret_cast<PerfettoDataSource*>(datasource_ptr); + + struct PerfettoDsParams params = PerfettoDsParamsDefault(); + params.buffer_exhausted_policy = (PerfettoDsBufferExhaustedPolicy)buffer_exhausted_policy; + + params.user_arg = reinterpret_cast<void*>(datasource.get()); + + params.on_setup_cb = [](struct PerfettoDsImpl*, PerfettoDsInstanceIndex inst_id, + void* ds_config, size_t ds_config_size, void* user_arg, + struct PerfettoDsOnSetupArgs*) -> void* { + JNIEnv* env = GetOrAttachJNIEnvironment(gVm, JNI_VERSION_1_6); + + auto* datasource = reinterpret_cast<PerfettoDataSource*>(user_arg); + + ScopedLocalRef<jobject> java_data_source_instance(env, + datasource->newInstance(env, ds_config, + ds_config_size, + inst_id)); + + auto* datasource_instance = + new PerfettoDataSourceInstance(env, java_data_source_instance.get(), inst_id); + + return static_cast<void*>(datasource_instance); + }; + + params.on_create_tls_cb = [](struct PerfettoDsImpl* ds_impl, PerfettoDsInstanceIndex inst_id, + struct PerfettoDsTracerImpl* tracer, void* user_arg) -> void* { + JNIEnv* env = GetOrAttachJNIEnvironment(gVm, JNI_VERSION_1_6); + + auto* datasource = reinterpret_cast<PerfettoDataSource*>(user_arg); + + jobject java_tls_state = datasource->createTlsStateGlobalRef(env, inst_id); + + auto* tls_state = new TlsState(java_tls_state); + + return static_cast<void*>(tls_state); + }; + + params.on_delete_tls_cb = [](void* ptr) { + JNIEnv* env = GetOrAttachJNIEnvironment(gVm, JNI_VERSION_1_6); + + TlsState* tls_state = reinterpret_cast<TlsState*>(ptr); + env->DeleteGlobalRef(tls_state->jobj); + delete tls_state; + }; + + params.on_create_incr_cb = [](struct PerfettoDsImpl* ds_impl, PerfettoDsInstanceIndex inst_id, + struct PerfettoDsTracerImpl* tracer, void* user_arg) -> void* { + JNIEnv* env = GetOrAttachJNIEnvironment(gVm, JNI_VERSION_1_6); + + auto* datasource = reinterpret_cast<PerfettoDataSource*>(user_arg); + jobject java_incr_state = datasource->createIncrementalStateGlobalRef(env, inst_id); + + auto* incr_state = new IncrementalState(java_incr_state); + return static_cast<void*>(incr_state); + }; + + params.on_delete_incr_cb = [](void* ptr) { + JNIEnv* env = GetOrAttachJNIEnvironment(gVm, JNI_VERSION_1_6); + + IncrementalState* incr_state = reinterpret_cast<IncrementalState*>(ptr); + env->DeleteGlobalRef(incr_state->jobj); + delete incr_state; + }; + + params.on_start_cb = [](struct PerfettoDsImpl*, PerfettoDsInstanceIndex, void*, void* inst_ctx, + struct PerfettoDsOnStartArgs*) { + JNIEnv* env = GetOrAttachJNIEnvironment(gVm, JNI_VERSION_1_6); + + auto* datasource_instance = static_cast<PerfettoDataSourceInstance*>(inst_ctx); + datasource_instance->onStart(env); + }; + + params.on_flush_cb = [](struct PerfettoDsImpl*, PerfettoDsInstanceIndex, void*, void* inst_ctx, + struct PerfettoDsOnFlushArgs*) { + JNIEnv* env = GetOrAttachJNIEnvironment(gVm, JNI_VERSION_1_6); + + auto* datasource_instance = static_cast<PerfettoDataSourceInstance*>(inst_ctx); + datasource_instance->onFlush(env); + }; + + params.on_stop_cb = [](struct PerfettoDsImpl*, PerfettoDsInstanceIndex inst_id, void* user_arg, + void* inst_ctx, struct PerfettoDsOnStopArgs*) { + JNIEnv* env = GetOrAttachJNIEnvironment(gVm, JNI_VERSION_1_6); + + auto* datasource_instance = static_cast<PerfettoDataSourceInstance*>(inst_ctx); + datasource_instance->onStop(env); + }; + + params.on_destroy_cb = [](struct PerfettoDsImpl* ds_impl, void* user_arg, + void* inst_ctx) -> void { + auto* datasource_instance = static_cast<PerfettoDataSourceInstance*>(inst_ctx); + delete datasource_instance; + }; + + PerfettoDsRegister(&datasource->dataSource, datasource->dataSourceName.c_str(), params); +} + +jobject nativeGetPerfettoInstanceLocked(JNIEnv* env, jclass clazz, jlong dataSourcePtr, + PerfettoDsInstanceIndex instance_idx) { + sp<PerfettoDataSource> datasource = reinterpret_cast<PerfettoDataSource*>(dataSourcePtr); + auto* datasource_instance = static_cast<PerfettoDataSourceInstance*>( + PerfettoDsImplGetInstanceLocked(datasource->dataSource.impl, instance_idx)); + + if (datasource_instance == nullptr) { + // datasource instance doesn't exist + return nullptr; + } + + return datasource_instance->GetJavaDataSourceInstance(); +} + +void nativeReleasePerfettoInstanceLocked(JNIEnv* env, jclass clazz, jlong dataSourcePtr, + PerfettoDsInstanceIndex instance_idx) { + sp<PerfettoDataSource> datasource = reinterpret_cast<PerfettoDataSource*>(dataSourcePtr); + PerfettoDsImplReleaseInstanceLocked(datasource->dataSource.impl, instance_idx); +} + +const JNINativeMethod gMethods[] = { + /* name, signature, funcPtr */ + {"nativeCreate", "(Landroid/tracing/perfetto/DataSource;Ljava/lang/String;)J", + (void*)nativeCreate}, + {"nativeTrace", "(JLandroid/tracing/perfetto/TraceFunction;)V", (void*)nativeTrace}, + {"nativeFlushAll", "(J)V", (void*)nativeFlushAll}, + {"nativeGetFinalizer", "()J", (void*)nativeGetFinalizer}, + {"nativeRegisterDataSource", "(JI)V", (void*)nativeRegisterDataSource}, + {"nativeGetPerfettoInstanceLocked", "(JI)Landroid/tracing/perfetto/DataSourceInstance;", + (void*)nativeGetPerfettoInstanceLocked}, + {"nativeReleasePerfettoInstanceLocked", "(JI)V", + (void*)nativeReleasePerfettoInstanceLocked}, +}; + +const JNINativeMethod gMethodsTracingContext[] = { + /* name, signature, funcPtr */ + {"nativeFlush", "(Landroid/tracing/perfetto/TracingContext;J)V", (void*)nativeFlush}, +}; + +int register_android_tracing_PerfettoDataSource(JNIEnv* env) { + int res = jniRegisterNativeMethods(env, "android/tracing/perfetto/DataSource", gMethods, + NELEM(gMethods)); + + LOG_ALWAYS_FATAL_IF(res < 0, "Unable to register native methods."); + + res = jniRegisterNativeMethods(env, "android/tracing/perfetto/TracingContext", + gMethodsTracingContext, NELEM(gMethodsTracingContext)); + + LOG_ALWAYS_FATAL_IF(res < 0, "Unable to register native methods."); + + if (env->GetJavaVM(&gVm) != JNI_OK) { + LOG_ALWAYS_FATAL("Failed to get JavaVM from JNIEnv: %p", env); + } + + jclass clazz = env->FindClass("android/tracing/perfetto/DataSource"); + gPerfettoDataSourceClassInfo.clazz = MakeGlobalRefOrDie(env, clazz); + gPerfettoDataSourceClassInfo.createInstance = + env->GetMethodID(gPerfettoDataSourceClassInfo.clazz, "createInstance", + "([BI)Landroid/tracing/perfetto/DataSourceInstance;"); + gPerfettoDataSourceClassInfo.createTlsState = + env->GetMethodID(gPerfettoDataSourceClassInfo.clazz, "createTlsState", + "(Landroid/tracing/perfetto/CreateTlsStateArgs;)Ljava/lang/Object;"); + gPerfettoDataSourceClassInfo.createIncrementalState = + env->GetMethodID(gPerfettoDataSourceClassInfo.clazz, "createIncrementalState", + "(Landroid/tracing/perfetto/CreateIncrementalStateArgs;)Ljava/lang/" + "Object;"); + + clazz = env->FindClass("android/tracing/perfetto/TracingContext"); + gTracingContextClassInfo.clazz = MakeGlobalRefOrDie(env, clazz); + gTracingContextClassInfo.init = env->GetMethodID(gTracingContextClassInfo.clazz, "<init>", + "(JLjava/lang/Object;Ljava/lang/Object;)V"); + gTracingContextClassInfo.getAndClearAllPendingTracePackets = + env->GetMethodID(gTracingContextClassInfo.clazz, "getAndClearAllPendingTracePackets", + "()[[B"); + + clazz = env->FindClass("android/tracing/perfetto/CreateTlsStateArgs"); + gCreateTlsStateArgsClassInfo.clazz = MakeGlobalRefOrDie(env, clazz); + gCreateTlsStateArgsClassInfo.init = + env->GetMethodID(gCreateTlsStateArgsClassInfo.clazz, "<init>", + "(Landroid/tracing/perfetto/DataSource;I)V"); + + clazz = env->FindClass("android/tracing/perfetto/CreateIncrementalStateArgs"); + gCreateIncrementalStateArgsClassInfo.clazz = MakeGlobalRefOrDie(env, clazz); + gCreateIncrementalStateArgsClassInfo.init = + env->GetMethodID(gCreateIncrementalStateArgsClassInfo.clazz, "<init>", + "(Landroid/tracing/perfetto/DataSource;I)V"); + + return 0; +} + +} // namespace android
\ No newline at end of file diff --git a/core/jni/android_tracing_PerfettoDataSource.h b/core/jni/android_tracing_PerfettoDataSource.h new file mode 100644 index 000000000000..4ddf1d8d4512 --- /dev/null +++ b/core/jni/android_tracing_PerfettoDataSource.h @@ -0,0 +1,59 @@ +/* + * Copyright 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. + */ + +#define LOG_TAG "Perfetto" + +#include <android_runtime/AndroidRuntime.h> +#include <android_runtime/Log.h> +#include <nativehelper/JNIHelp.h> +#include <perfetto/public/data_source.h> +#include <perfetto/public/producer.h> +#include <perfetto/public/protos/trace/test_event.pzc.h> +#include <perfetto/public/protos/trace/trace_packet.pzc.h> +#include <perfetto/tracing.h> +#include <utils/Log.h> +#include <utils/RefBase.h> + +#include <sstream> +#include <thread> + +#include "android_tracing_PerfettoDataSourceInstance.h" +#include "core_jni_helpers.h" + +namespace android { + +class PerfettoDataSource : public virtual RefBase { +public: + const std::string dataSourceName; + struct PerfettoDs dataSource = PERFETTO_DS_INIT(); + + PerfettoDataSource(JNIEnv* env, jobject java_data_source, std::string data_source_name); + ~PerfettoDataSource(); + + jobject newInstance(JNIEnv* env, void* ds_config, size_t ds_config_size, + PerfettoDsInstanceIndex inst_id); + + jobject createTlsStateGlobalRef(JNIEnv* env, PerfettoDsInstanceIndex inst_id); + jobject createIncrementalStateGlobalRef(JNIEnv* env, PerfettoDsInstanceIndex inst_id); + void trace(JNIEnv* env, jobject trace_function); + void flushAll(); + +private: + jobject mJavaDataSource; + std::map<PerfettoDsInstanceIndex, PerfettoDataSourceInstance*> mInstances; +}; + +} // namespace android
\ No newline at end of file diff --git a/core/jni/android_tracing_PerfettoDataSourceInstance.cpp b/core/jni/android_tracing_PerfettoDataSourceInstance.cpp new file mode 100644 index 000000000000..e659bf1c55e9 --- /dev/null +++ b/core/jni/android_tracing_PerfettoDataSourceInstance.cpp @@ -0,0 +1,138 @@ +/* + * Copyright 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. + */ + +#define LOG_TAG "Perfetto" + +#include "android_tracing_PerfettoDataSourceInstance.h" + +#include <android_runtime/AndroidRuntime.h> +#include <android_runtime/Log.h> +#include <nativehelper/JNIHelp.h> +#include <perfetto/public/data_source.h> +#include <perfetto/public/producer.h> +#include <perfetto/public/protos/trace/test_event.pzc.h> +#include <perfetto/public/protos/trace/trace_packet.pzc.h> +#include <perfetto/tracing.h> +#include <utils/Log.h> +#include <utils/RefBase.h> + +#include <sstream> +#include <thread> + +#include "core_jni_helpers.h" + +namespace android { + +static struct { + jclass clazz; + jmethodID init; +} gStartCallbackArgumentsClassInfo; + +static struct { + jclass clazz; + jmethodID init; +} gFlushCallbackArgumentsClassInfo; + +static struct { + jclass clazz; + jmethodID init; +} gStopCallbackArgumentsClassInfo; + +static JavaVM* gVm; + +void callJavaMethodWithArgsObject(JNIEnv* env, jobject classRef, jmethodID method, jobject args) { + ScopedLocalRef<jobject> localClassRef(env, env->NewLocalRef(classRef)); + + if (localClassRef == nullptr) { + ALOGE("Weak reference went out of scope"); + return; + } + + env->CallVoidMethod(localClassRef.get(), method, args); + + if (env->ExceptionCheck()) { + env->ExceptionDescribe(); + LOGE_EX(env); + env->ExceptionClear(); + } +} + +PerfettoDataSourceInstance::PerfettoDataSourceInstance(JNIEnv* env, jobject javaDataSourceInstance, + PerfettoDsInstanceIndex inst_idx) + : inst_idx(inst_idx), mJavaDataSourceInstance(env->NewGlobalRef(javaDataSourceInstance)) {} + +PerfettoDataSourceInstance::~PerfettoDataSourceInstance() { + JNIEnv* env = GetOrAttachJNIEnvironment(gVm, JNI_VERSION_1_6); + env->DeleteGlobalRef(mJavaDataSourceInstance); +} + +void PerfettoDataSourceInstance::onStart(JNIEnv* env) { + ScopedLocalRef<jobject> args(env, + env->NewObject(gStartCallbackArgumentsClassInfo.clazz, + gStartCallbackArgumentsClassInfo.init)); + jclass cls = env->GetObjectClass(mJavaDataSourceInstance); + jmethodID mid = env->GetMethodID(cls, "onStart", + "(Landroid/tracing/perfetto/StartCallbackArguments;)V"); + + callJavaMethodWithArgsObject(env, mJavaDataSourceInstance, mid, args.get()); +} + +void PerfettoDataSourceInstance::onFlush(JNIEnv* env) { + ScopedLocalRef<jobject> args(env, + env->NewObject(gFlushCallbackArgumentsClassInfo.clazz, + gFlushCallbackArgumentsClassInfo.init)); + jclass cls = env->GetObjectClass(mJavaDataSourceInstance); + jmethodID mid = env->GetMethodID(cls, "onFlush", + "(Landroid/tracing/perfetto/FlushCallbackArguments;)V"); + + callJavaMethodWithArgsObject(env, mJavaDataSourceInstance, mid, args.get()); +} + +void PerfettoDataSourceInstance::onStop(JNIEnv* env) { + ScopedLocalRef<jobject> args(env, + env->NewObject(gStopCallbackArgumentsClassInfo.clazz, + gStopCallbackArgumentsClassInfo.init)); + jclass cls = env->GetObjectClass(mJavaDataSourceInstance); + jmethodID mid = + env->GetMethodID(cls, "onStop", "(Landroid/tracing/perfetto/StopCallbackArguments;)V"); + + callJavaMethodWithArgsObject(env, mJavaDataSourceInstance, mid, args.get()); +} + +int register_android_tracing_PerfettoDataSourceInstance(JNIEnv* env) { + if (env->GetJavaVM(&gVm) != JNI_OK) { + LOG_ALWAYS_FATAL("Failed to get JavaVM from JNIEnv: %p", env); + } + + jclass clazz = env->FindClass("android/tracing/perfetto/StartCallbackArguments"); + gStartCallbackArgumentsClassInfo.clazz = MakeGlobalRefOrDie(env, clazz); + gStartCallbackArgumentsClassInfo.init = + env->GetMethodID(gStartCallbackArgumentsClassInfo.clazz, "<init>", "()V"); + + clazz = env->FindClass("android/tracing/perfetto/FlushCallbackArguments"); + gFlushCallbackArgumentsClassInfo.clazz = MakeGlobalRefOrDie(env, clazz); + gFlushCallbackArgumentsClassInfo.init = + env->GetMethodID(gFlushCallbackArgumentsClassInfo.clazz, "<init>", "()V"); + + clazz = env->FindClass("android/tracing/perfetto/StopCallbackArguments"); + gStopCallbackArgumentsClassInfo.clazz = MakeGlobalRefOrDie(env, clazz); + gStopCallbackArgumentsClassInfo.init = + env->GetMethodID(gStopCallbackArgumentsClassInfo.clazz, "<init>", "()V"); + + return 0; +} + +} // namespace android
\ No newline at end of file diff --git a/core/jni/android_tracing_PerfettoDataSourceInstance.h b/core/jni/android_tracing_PerfettoDataSourceInstance.h new file mode 100644 index 000000000000..d57765565d8a --- /dev/null +++ b/core/jni/android_tracing_PerfettoDataSourceInstance.h @@ -0,0 +1,59 @@ +/* + * Copyright 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. + */ + +#define LOG_TAG "Perfetto" + +#include <android_runtime/AndroidRuntime.h> +#include <android_runtime/Log.h> +#include <nativehelper/JNIHelp.h> +#include <perfetto/public/data_source.h> +#include <perfetto/public/producer.h> +#include <perfetto/public/protos/trace/test_event.pzc.h> +#include <perfetto/public/protos/trace/trace_packet.pzc.h> +#include <perfetto/tracing.h> +#include <utils/Log.h> +#include <utils/RefBase.h> + +#include <sstream> +#include <thread> + +#include "core_jni_helpers.h" + +namespace android { + +class PerfettoDataSourceInstance { +public: + PerfettoDataSourceInstance(JNIEnv* env, jobject javaDataSourceInstance, + PerfettoDsInstanceIndex inst_idx); + ~PerfettoDataSourceInstance(); + + void onStart(JNIEnv* env); + void onFlush(JNIEnv* env); + void onStop(JNIEnv* env); + + jobject GetJavaDataSourceInstance() { + return mJavaDataSourceInstance; + } + + PerfettoDsInstanceIndex getIndex() { + return inst_idx; + } + +private: + PerfettoDsInstanceIndex inst_idx; + jobject mJavaDataSourceInstance; +}; +} // namespace android
\ No newline at end of file diff --git a/core/jni/android_tracing_PerfettoProducer.cpp b/core/jni/android_tracing_PerfettoProducer.cpp new file mode 100644 index 000000000000..ce72f5893c19 --- /dev/null +++ b/core/jni/android_tracing_PerfettoProducer.cpp @@ -0,0 +1,58 @@ +/* + * Copyright 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. + */ + +#define LOG_TAG "Perfetto" + +#include <android_runtime/AndroidRuntime.h> +#include <android_runtime/Log.h> +#include <nativehelper/JNIHelp.h> +#include <perfetto/public/data_source.h> +#include <perfetto/public/producer.h> +#include <perfetto/public/protos/trace/test_event.pzc.h> +#include <perfetto/public/protos/trace/trace_packet.pzc.h> +#include <perfetto/tracing.h> +#include <utils/Log.h> +#include <utils/RefBase.h> + +#include <sstream> +#include <thread> + +#include "android_tracing_PerfettoDataSource.h" +#include "core_jni_helpers.h" + +namespace android { + +void perfettoProducerInit(JNIEnv* env, jclass clazz, int backends) { + struct PerfettoProducerInitArgs args = PERFETTO_PRODUCER_INIT_ARGS_INIT(); + args.backends = (PerfettoBackendTypes)backends; + PerfettoProducerInit(args); +} + +const JNINativeMethod gMethods[] = { + /* name, signature, funcPtr */ + {"nativePerfettoProducerInit", "(I)V", (void*)perfettoProducerInit}, +}; + +int register_android_tracing_PerfettoProducer(JNIEnv* env) { + int res = jniRegisterNativeMethods(env, "android/tracing/perfetto/Producer", gMethods, + NELEM(gMethods)); + + LOG_ALWAYS_FATAL_IF(res < 0, "Unable to register native methods."); + + return 0; +} + +} // namespace android
\ No newline at end of file diff --git a/core/proto/android/service/notification.proto b/core/proto/android/service/notification.proto index a2978bec16b5..ad36b1c235fe 100644 --- a/core/proto/android/service/notification.proto +++ b/core/proto/android/service/notification.proto @@ -360,7 +360,7 @@ message DNDPolicyProto { optional ConversationType allow_conversations_from = 19; - optional ChannelType allow_channels = 20; + optional ChannelPolicy allow_channels = 20; } // Enum identifying the type of rule that changed; values set to match ones used in the @@ -373,8 +373,8 @@ enum RuleType { // Enum used in DNDPolicyProto to indicate the type of channels permitted to // break through DND. Mirrors values in ZenPolicy. -enum ChannelType { - CHANNEL_TYPE_UNSET = 0; - CHANNEL_TYPE_PRIORITY = 1; - CHANNEL_TYPE_NONE = 2; +enum ChannelPolicy { + CHANNEL_POLICY_UNSET = 0; + CHANNEL_POLICY_PRIORITY = 1; + CHANNEL_POLICY_NONE = 2; } diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index 5f3f6419418a..9c455d5281d8 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -7896,6 +7896,26 @@ <permission android:name="android.permission.RECEIVE_SENSITIVE_NOTIFICATIONS" android:protectionLevel="signature|role" /> + <!-- @SystemApi + @FlaggedApi("android.app.bic_client") + Allows app to call BackgroundInstallControlManager API to retrieve silently installed apps + for all users on device. + <p>Apps with a BackgroundInstallControlManager client will not be able to call any API without + this permission. + <p>Protection level: signature|role + @hide + --> + <permission android:name="android.permission.GET_BACKGROUND_INSTALLED_PACKAGES" + android:protectionLevel="signature|role" /> + + <!-- @SystemApi Allows an application to read the system grammatical gender. + @FlaggedApi("android.app.system_terms_of_address_enabled") + <p>Protection level: signature|privileged|appop + @hide + --> + <permission android:name="android.permission.READ_SYSTEM_GRAMMATICAL_GENDER" + android:protectionLevel="signature|privileged|appop"/> + <!-- Attribution for Geofencing service. --> <attribution android:tag="GeofencingService" android:label="@string/geofencing_service"/> <!-- Attribution for Country Detector. --> diff --git a/core/res/res/drawable-nodpi/platlogo.xml b/core/res/res/drawable-nodpi/platlogo.xml index f3acab063bbf..fe12f6ee7836 100644 --- a/core/res/res/drawable-nodpi/platlogo.xml +++ b/core/res/res/drawable-nodpi/platlogo.xml @@ -1,5 +1,5 @@ <!-- -Copyright (C) 2021 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. @@ -20,179 +20,103 @@ Copyright (C) 2021 The Android Open Source Project android:viewportWidth="512" android:viewportHeight="512"> <path - android:pathData="M256,256m-200,0a200,200 0,1 1,400 0a200,200 0,1 1,-400 0"> + android:pathData="M256.22,437.7c-26.38,0 -43.81,-18.3 -61.2,-48.42 -17.39,-30.12 -103.26,-178.86 -120.65,-208.98s-24.52,-54.37 -11.33,-77.21c13.19,-22.84 37.75,-28.79 72.53,-28.79 34.78,0 206.53,0 241.31,0 34.78,0 59.35,5.95 72.53,28.79 13.19,22.84 6.06,47.09 -11.33,77.21s-103.26,178.86 -120.65,208.98c-17.39,30.12 -34.83,48.42 -61.2,48.42Z" + android:strokeWidth="0"> <aapt:attr name="android:fillColor"> <gradient - android:startX="256" - android:startY="21.81" - android:endX="256" - android:endY="350.42" + android:startX="56.22" + android:startY="256" + android:endX="456.22" + android:endY="256" android:type="linear"> <item android:offset="0" android:color="#FF073042"/> <item android:offset="1" android:color="#FF073042"/> </gradient> </aapt:attr> </path> - <group> - <clip-path - android:pathData="M256,256m-200,0a200,200 0,1 1,400 0a200,200 0,1 1,-400 0"/> - <path - android:pathData="m195.27,187.64l2.25,-6.69c13.91,78.13 50.84,284.39 50.84,50.33 0,-0.97 0.72,-1.81 1.62,-1.81h12.69c0.9,0 1.62,0.83 1.62,1.8 -0.2,409.91 -69.03,-43.64 -69.03,-43.64Z" - android:fillColor="#3ddc84"/> - </group> - <group> - <clip-path - android:pathData="M256,256m-200,0a200,200 0,1 1,400 0a200,200 0,1 1,-400 0"/> - <path - android:pathData="m158.77,180.68l-33.17,57.45c-1.9,3.3 -0.77,7.52 2.53,9.42 3.3,1.9 7.52,0.77 9.42,-2.53l33.59,-58.17c54.27,24.33 116.34,24.33 170.61,0l33.59,58.17c1.97,3.26 6.21,4.3 9.47,2.33 3.17,-1.91 4.26,-5.99 2.47,-9.23l-33.16,-57.45c56.95,-30.97 95.91,-88.64 101.61,-156.76H57.17c5.7,68.12 44.65,125.79 101.61,156.76Z" - android:fillColor="#fff"/> - </group> - <group> - <clip-path - android:pathData="M256,256m-200,0a200,200 0,1 1,400 0a200,200 0,1 1,-400 0"/> - <path - android:pathData="M250.26,187.66L262.17,187.66A2.13,2.13 0,0 1,264.3 189.78L264.3,217.85A2.13,2.13 0,0 1,262.17 219.98L250.26,219.98A2.13,2.13 0,0 1,248.14 217.85L248.14,189.78A2.13,2.13 0,0 1,250.26 187.66z" - android:fillColor="#3ddc84"/> - </group> - <group> - <clip-path - android:pathData="M256,256m-200,0a200,200 0,1 1,400 0a200,200 0,1 1,-400 0"/> - <path - android:pathData="M250.12,170.29L262.32,170.29A1.98,1.98 0,0 1,264.3 172.26L264.3,176.39A1.98,1.98 0,0 1,262.32 178.37L250.12,178.37A1.98,1.98 0,0 1,248.14 176.39L248.14,172.26A1.98,1.98 0,0 1,250.12 170.29z" - android:fillColor="#3ddc84"/> - </group> - <group> - <clip-path - android:pathData="M256,256m-200,0a200,200 0,1 1,400 0a200,200 0,1 1,-400 0"/> - <path - android:pathData="M171.92,216.82h4.04v4.04h-4.04z" - android:fillColor="#fff"/> - </group> - <group> - <clip-path - android:pathData="M256,256m-200,0a200,200 0,1 1,400 0a200,200 0,1 1,-400 0"/> - <path - android:pathData="M188.8,275.73h4.04v4.04h-4.04z" - android:fillColor="#fff"/> - </group> - <group> - <clip-path - android:pathData="M256,256m-200,0a200,200 0,1 1,400 0a200,200 0,1 1,-400 0"/> - <path - android:pathData="M369.04,337.63h4.04v4.04h-4.04z" - android:fillColor="#fff"/> - </group> - <group> - <clip-path - android:pathData="M256,256m-200,0a200,200 0,1 1,400 0a200,200 0,1 1,-400 0"/> - <path - android:pathData="M285.93,252.22h4.04v4.04h-4.04z" - android:fillColor="#fff"/> - </group> - <group> - <clip-path - android:pathData="M256,256m-200,0a200,200 0,1 1,400 0a200,200 0,1 1,-400 0"/> - <path - android:pathData="M318.96,218.84h4.04v4.04h-4.04z" - android:fillColor="#fff"/> - </group> - <group> - <clip-path - android:pathData="M256,256m-200,0a200,200 0,1 1,400 0a200,200 0,1 1,-400 0"/> - <path - android:pathData="M294.05,288.55h4.04v4.04h-4.04z" - android:fillColor="#fff"/> - </group> - <group> - <clip-path - android:pathData="M256,256m-200,0a200,200 0,1 1,400 0a200,200 0,1 1,-400 0"/> - <path - android:pathData="M330.82,273.31h8.08v8.08h-8.08z" - android:fillColor="#fff"/> - </group> - <group> - <clip-path - android:pathData="M256,256m-200,0a200,200 0,1 1,400 0a200,200 0,1 1,-400 0"/> - <path - android:pathData="M188.8,298.95h4.04v4.04h-4.04z" - android:fillColor="#fff"/> - </group> - <group> - <clip-path - android:pathData="M256,256m-200,0a200,200 0,1 1,400 0a200,200 0,1 1,-400 0"/> - <path - android:pathData="M220.14,238.94h8.08v8.08h-8.08z" - android:fillColor="#fff"/> - </group> - <group> - <clip-path - android:pathData="M256,256m-200,0a200,200 0,1 1,400 0a200,200 0,1 1,-400 0"/> - <path - android:pathData="M272.1,318.9h8.08v8.08h-8.08z" - android:fillColor="#fff"/> - </group> - <group> - <clip-path - android:pathData="M256,256m-200,0a200,200 0,1 1,400 0a200,200 0,1 1,-400 0"/> - <path - android:pathData="M293.34,349.25h8.08v8.08h-8.08z" - android:fillColor="#fff"/> - </group> - <group> - <clip-path - android:pathData="M256,256m-200,0a200,200 0,1 1,400 0a200,200 0,1 1,-400 0"/> - <path - android:pathData="M161.05,254.24h8.08v8.08h-8.08z" - android:fillColor="#fff"/> - </group> - <group> - <clip-path - android:pathData="M256,256m-200,0a200,200 0,1 1,400 0a200,200 0,1 1,-400 0"/> - <path - android:pathData="M378.92,192h8.08v8.08h-8.08z" - android:fillColor="#fff"/> - </group> - <group> - <clip-path - android:pathData="M256,256m-200,0a200,200 0,1 1,400 0a200,200 0,1 1,-400 0"/> - <path - android:pathData="M137.87,323.7h8.08v8.08h-8.08z" - android:fillColor="#fff"/> - </group> - <path - android:pathData="M256,256m-200,0a200,200 0,1 1,400 0a200,200 0,1 1,-400 0" - android:strokeWidth="56.561" - android:fillColor="#00000000" - android:strokeColor="#f86734"/> <path - android:pathData="m256.22,126.57c-6.69,0 -12.12,5.27 -12.12,11.77v14.52c0,1.25 1.02,2.27 2.27,2.27h0c1.25,0 2.27,-1.02 2.27,-2.27v-3.91c0,-2.51 2.04,-4.55 4.55,-4.55h6.06c2.51,0 4.55,2.04 4.55,4.55v3.91c0,1.25 1.02,2.27 2.27,2.27s2.27,-1.02 2.27,-2.27v-14.52c0,-6.5 -5.43,-11.77 -12.12,-11.77Z" + android:pathData="M198.85,168.57l2.03,-6.03c12.55,70.48 45.87,256.56 45.87,45.41 0,-0.88 0.65,-1.63 1.46,-1.63h11.45c0.81,0 1.46,0.75 1.46,1.63 -0.18,369.8 -62.28,-39.37 -62.28,-39.37Z" + android:strokeWidth="0" + android:fillColor="#3ddc84"/> + <path + android:pathData="M186.69,167.97l-23.69,41.03c-1.36,2.36 -0.55,5.37 1.8,6.73 2.36,1.36 5.37,0.55 6.73,-1.8l23.99,-41.55c38.76,17.38 83.1,17.38 121.86,0l23.99,41.55c1.41,2.33 4.44,3.07 6.77,1.66 2.26,-1.37 3.04,-4.28 1.76,-6.59l-23.69,-41.03c40.68,-22.12 68.5,-63.31 72.57,-111.97H114.11c4.07,48.65 31.89,89.85 72.57,111.97Z" + android:strokeWidth="0" + android:fillColor="#fff"/> + <path + android:pathData="M248.46,168.59L259.2,168.59A1.92,1.92 0,0 1,261.12 170.5L261.12,195.83A1.92,1.92 0,0 1,259.2 197.75L248.46,197.75A1.92,1.92 0,0 1,246.54 195.83L246.54,170.5A1.92,1.92 0,0 1,248.46 168.59z" + android:strokeWidth="0" + android:fillColor="#3ddc84"/> + <path + android:pathData="M248.32,152.92L259.34,152.92A1.78,1.78 0,0 1,261.12 154.7L261.12,158.43A1.78,1.78 0,0 1,259.34 160.21L248.32,160.21A1.78,1.78 0,0 1,246.54 158.43L246.54,154.7A1.78,1.78 0,0 1,248.32 152.92z" + android:strokeWidth="0" android:fillColor="#3ddc84"/> <path - android:pathData="m93.34,116.36l3.85,-4.36 29.64,9.76 -4.44,5.03 -6.23,-2.1 -7.86,8.91 2.86,5.92 -4.43,5.03 -13.39,-28.18ZM110.43,122.76l-8.86,-3.02 4.11,8.41 4.76,-5.39Z" + android:pathData="M159.03,176.91h4.04v4.04h-4.04z" + android:strokeWidth="0" + android:fillColor="#fff"/> + <path + android:pathData="M188.8,275.73h4.04v4.04h-4.04z" + android:strokeWidth="0" + android:fillColor="#fff"/> + <path + android:pathData="M373.41,158.93h4.04v4.04h-4.04z" + android:strokeWidth="0" + android:fillColor="#fff"/> + <path + android:pathData="M112.1,129.34h4.04v4.04h-4.04z" + android:strokeWidth="0" + android:fillColor="#fff"/> + <path + android:pathData="M285.93,252.22h4.04v4.04h-4.04z" + android:strokeWidth="0" + android:fillColor="#fff"/> + <path + android:pathData="M318.96,218.84h4.04v4.04h-4.04z" + android:strokeWidth="0" + android:fillColor="#fff"/> + <path + android:pathData="M294.05,288.55h4.04v4.04h-4.04z" + android:strokeWidth="0" android:fillColor="#fff"/> <path - android:pathData="m153.62,100.85l-21.71,-6.2 10.38,14.38 -5.21,3.76 -16.78,-23.26 4.49,-3.24 21.65,6.19 -10.35,-14.35 5.24,-3.78 16.78,23.26 -4.49,3.24Z" + android:pathData="M330.82,263.31h8.08v8.08h-8.08z" + android:strokeWidth="0" android:fillColor="#fff"/> <path - android:pathData="m161.46,63.15l8.99,-3.84c7.43,-3.18 15.96,0.12 19.09,7.44 3.13,7.32 -0.38,15.76 -7.81,18.94l-8.99,3.84 -11.28,-26.38ZM179.41,80.26c4.46,-1.91 5.96,-6.72 4.15,-10.96 -1.81,-4.24 -6.33,-6.48 -10.79,-4.57l-3.08,1.32 6.64,15.53 3.08,-1.32Z" + android:pathData="M188.8,298.95h4.04v4.04h-4.04z" + android:strokeWidth="0" android:fillColor="#fff"/> <path - android:pathData="m204.23,47.57l11.1,-2.2c5.31,-1.05 9.47,2.08 10.4,6.76 0.72,3.65 -0.76,6.37 -4.07,8.34l12.4,10.44 -7.57,1.5 -11.65,-9.76 -1.03,0.2 2.3,11.61 -6.3,1.25 -5.57,-28.14ZM216.78,56.7c1.86,-0.37 3,-1.71 2.68,-3.33 -0.34,-1.7 -1.88,-2.43 -3.74,-2.06l-4.04,0.8 1.07,5.39 4.04,-0.8Z" + android:pathData="M224.18,216.82h8.08v8.08h-8.08z" + android:strokeWidth="0" android:fillColor="#fff"/> <path - android:pathData="m244.29,55.6c0.13,-8.16 6.86,-14.72 15.06,-14.58 8.16,0.13 14.72,6.9 14.58,15.06s-6.91,14.72 -15.06,14.58c-8.2,-0.13 -14.71,-6.9 -14.58,-15.06ZM267.44,55.98c0.08,-4.64 -3.54,-8.66 -8.18,-8.74 -4.68,-0.08 -8.42,3.82 -8.5,8.47 -0.08,4.65 3.54,8.66 8.22,8.74 4.64,0.08 8.39,-3.82 8.46,-8.47Z" + android:pathData="M272.1,318.9h8.08v8.08h-8.08z" + android:strokeWidth="0" android:fillColor="#fff"/> <path - android:pathData="m294.39,44.84l6.31,1.23 -5.49,28.16 -6.31,-1.23 5.49,-28.16Z" + android:pathData="M293.34,339.25h8.08v8.08h-8.08z" + android:strokeWidth="0" android:fillColor="#fff"/> <path - android:pathData="m321.94,51.41l9.14,3.48c7.55,2.88 11.39,11.17 8.56,18.61 -2.83,7.44 -11.22,11.07 -18.77,8.19l-9.14,-3.48 10.22,-26.8ZM322.96,76.19c4.53,1.73 8.95,-0.69 10.6,-5 1.64,-4.3 -0.05,-9.06 -4.58,-10.78l-3.13,-1.19 -6.01,15.78 3.13,1.19Z" + android:pathData="M165.09,236.28h8.08v8.08h-8.08z" + android:strokeWidth="0" android:fillColor="#fff"/> <path - android:pathData="m381.41,89.24l-4.21,-3.21 3.65,-4.78 9.06,6.91 -17.4,22.81 -4.85,-3.7 13.75,-18.02Z" + android:pathData="M378.92,192h8.08v8.08h-8.08z" + android:strokeWidth="0" android:fillColor="#fff"/> <path - android:pathData="m397.96,126.37l-9.56,-10.26 3.61,-3.36 22.8,-1.25 3.74,4.02 -12.35,11.51 2.51,2.69 -4.08,3.8 -2.51,-2.69 -4.55,4.24 -4.16,-4.46 4.55,-4.24ZM407.83,117.17l-10.28,0.58 4.49,4.82 5.79,-5.4Z" + android:pathData="M204.28,314.86h8.08v8.08h-8.08z" + android:strokeWidth="0" android:fillColor="#fff"/> + <path + android:pathData="M253.83,118.47c-6.04,0 -10.93,4.76 -10.93,10.62v13.1c0,1.13 0.92,2.05 2.05,2.05h0c1.13,0 2.05,-0.92 2.05,-2.05v-3.53c0,-2.27 1.84,-4.1 4.1,-4.1h5.47c2.27,0 4.1,1.84 4.1,4.1v3.53c0,1.13 0.92,2.05 2.05,2.05s2.05,-0.92 2.05,-2.05v-13.1c0,-5.86 -4.9,-10.62 -10.93,-10.62Z" + android:strokeWidth="0" + android:fillColor="#3ddc84"/> + <path + android:pathData="M256,437.7c-26.38,0 -43.81,-18.3 -61.2,-48.42 -17.39,-30.12 -103.26,-178.86 -120.65,-208.98 -17.39,-30.12 -24.52,-54.37 -11.33,-77.21 13.19,-22.84 37.75,-28.79 72.53,-28.79 34.78,0 206.53,0 241.31,0 34.78,0 59.35,5.95 72.53,28.79 13.19,22.84 6.06,47.09 -11.33,77.21 -17.39,30.12 -103.26,178.86 -120.65,208.98 -17.39,30.12 -34.83,48.42 -61.2,48.42Z" + android:strokeWidth="55" + android:fillColor="#00000000" + android:strokeColor="#f86733"/> </vector> - diff --git a/core/res/res/values/attrs_manifest.xml b/core/res/res/values/attrs_manifest.xml index 601952437650..8fae6db4114a 100644 --- a/core/res/res/values/attrs_manifest.xml +++ b/core/res/res/values/attrs_manifest.xml @@ -506,12 +506,6 @@ 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,7 +2859,6 @@ 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 @@ -3024,7 +3017,6 @@ 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 @@ -3152,7 +3144,7 @@ <attr name="uiOptions" /> <attr name="parentActivityName" /> <attr name="singleUser" /> - <!-- This broadcast receiver or activity will only receive broadcasts for the + <!-- @hide 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_telephony.xml b/core/res/res/values/config_telephony.xml index 53464547c272..d0216b308a4c 100644 --- a/core/res/res/values/config_telephony.xml +++ b/core/res/res/values/config_telephony.xml @@ -192,8 +192,13 @@ <!-- The identifier of the satellite's SIM profile. The identifier is composed of MCC and MNC of the satellite PLMN with the format "mccmnc". --> - <string name="config_satellite_sim_identifier" translatable="false"></string> - <java-symbol type="string" name="config_satellite_sim_identifier" /> + <string name="config_satellite_sim_plmn_identifier" translatable="false"></string> + <java-symbol type="string" name="config_satellite_sim_plmn_identifier" /> + + <!-- The identifier for the satellite's SIM profile. The identifier is the service provider name + (spn) from the profile metadata. --> + <string name="config_satellite_sim_spn_identifier" translatable="false"></string> + <java-symbol type="string" name="config_satellite_sim_spn_identifier" /> <!-- The app to which the emergency call will be handed over for OEM-enabled satellite messaging. The format of the config string is "package_name;class_name". --> diff --git a/core/res/res/values/public-staging.xml b/core/res/res/values/public-staging.xml index 7b5c49c8d9aa..53b473e0ac1f 100644 --- a/core/res/res/values/public-staging.xml +++ b/core/res/res/values/public-staging.xml @@ -119,8 +119,6 @@ <public name="optional"/> <!-- @FlaggedApi("android.media.tv.flags.enable_ad_service_fw") --> <public name="adServiceTypes" /> - <!-- @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/tests/BroadcastRadioTests/TEST_MAPPING b/core/tests/BroadcastRadioTests/TEST_MAPPING new file mode 100644 index 000000000000..b085a27b25b8 --- /dev/null +++ b/core/tests/BroadcastRadioTests/TEST_MAPPING @@ -0,0 +1,7 @@ +{ + "postsubmit": [ + { + "name": "BroadcastRadioTests" + } + ] +} diff --git a/core/tests/coretests/Android.bp b/core/tests/coretests/Android.bp index c0581746e6f6..e18de2e66399 100644 --- a/core/tests/coretests/Android.bp +++ b/core/tests/coretests/Android.bp @@ -30,6 +30,8 @@ filegroup { android_test { name: "FrameworksCoreTests", + // FrameworksCoreTestsRavenwood references the .aapt.srcjar + use_resource_processor: false, srcs: [ "src/**/*.java", @@ -83,6 +85,10 @@ android_test { "com.android.text.flags-aconfig-java", "flag-junit", "ravenwood-junit", + "perfetto_trace_java_protos", + "flickerlib-parsers", + "flickerlib-trace_processor_shell", + "mockito-target-extended-minus-junit4", ], libs: [ diff --git a/core/tests/coretests/src/android/app/AutomaticZenRuleTest.java b/core/tests/coretests/src/android/app/AutomaticZenRuleTest.java index 9d85b65d4dac..1925588e8904 100644 --- a/core/tests/coretests/src/android/app/AutomaticZenRuleTest.java +++ b/core/tests/coretests/src/android/app/AutomaticZenRuleTest.java @@ -16,8 +16,6 @@ package android.app; -import static com.google.common.truth.Truth.assertThat; - import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.fail; @@ -28,8 +26,6 @@ import android.net.Uri; import android.os.Parcel; import android.platform.test.annotations.EnableFlags; import android.platform.test.flag.junit.SetFlagsRule; -import android.service.notification.ZenDeviceEffects; -import android.service.notification.ZenPolicy; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; @@ -230,66 +226,4 @@ public class AutomaticZenRuleTest { assertThrows(IllegalArgumentException.class, () -> builder.setType(100)); } - - @Test - @EnableFlags(Flags.FLAG_MODES_API) - public void testCanUpdate_nullPolicyAndDeviceEffects() { - AutomaticZenRule.Builder builder = new AutomaticZenRule.Builder("name", - Uri.parse("uri://short")); - - AutomaticZenRule rule = builder.setUserModifiedFields(0) - .setZenPolicy(null) - .setDeviceEffects(null) - .build(); - - assertThat(rule.canUpdate()).isTrue(); - - rule = builder.setUserModifiedFields(1).build(); - assertThat(rule.canUpdate()).isFalse(); - } - - @Test - @EnableFlags(Flags.FLAG_MODES_API) - public void testCanUpdate_policyModified() { - ZenPolicy.Builder policyBuilder = new ZenPolicy.Builder().setUserModifiedFields(0); - ZenPolicy policy = policyBuilder.build(); - - AutomaticZenRule.Builder builder = new AutomaticZenRule.Builder("name", - Uri.parse("uri://short")); - AutomaticZenRule rule = builder.setUserModifiedFields(0) - .setZenPolicy(policy) - .setDeviceEffects(null).build(); - - // Newly created ZenPolicy is not user modified. - assertThat(policy.getUserModifiedFields()).isEqualTo(0); - assertThat(rule.canUpdate()).isTrue(); - - policy = policyBuilder.setUserModifiedFields(1).build(); - assertThat(policy.getUserModifiedFields()).isEqualTo(1); - rule = builder.setZenPolicy(policy).build(); - assertThat(rule.canUpdate()).isFalse(); - } - - @Test - @EnableFlags(Flags.FLAG_MODES_API) - public void testCanUpdate_deviceEffectsModified() { - ZenDeviceEffects.Builder deviceEffectsBuilder = - new ZenDeviceEffects.Builder().setUserModifiedFields(0); - ZenDeviceEffects deviceEffects = deviceEffectsBuilder.build(); - - AutomaticZenRule.Builder builder = new AutomaticZenRule.Builder("name", - Uri.parse("uri://short")); - AutomaticZenRule rule = builder.setUserModifiedFields(0) - .setZenPolicy(null) - .setDeviceEffects(deviceEffects).build(); - - // Newly created ZenDeviceEffects is not user modified. - assertThat(deviceEffects.getUserModifiedFields()).isEqualTo(0); - assertThat(rule.canUpdate()).isTrue(); - - deviceEffects = deviceEffectsBuilder.setUserModifiedFields(1).build(); - assertThat(deviceEffects.getUserModifiedFields()).isEqualTo(1); - rule = builder.setDeviceEffects(deviceEffects).build(); - assertThat(rule.canUpdate()).isFalse(); - } } diff --git a/core/tests/coretests/src/android/content/res/FontScaleConverterFactoryTest.kt b/core/tests/coretests/src/android/content/res/FontScaleConverterFactoryTest.kt index 1617eda6a77c..e32a57b1aefe 100644 --- a/core/tests/coretests/src/android/content/res/FontScaleConverterFactoryTest.kt +++ b/core/tests/coretests/src/android/content/res/FontScaleConverterFactoryTest.kt @@ -48,7 +48,7 @@ class FontScaleConverterFactoryTest { @get:Rule val checkFlagsRule: CheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule() - private lateinit var defaultLookupTables: SparseArray<FontScaleConverter> + private var defaultLookupTables: SparseArray<FontScaleConverter>? = null @Before fun setup() { @@ -58,7 +58,9 @@ class FontScaleConverterFactoryTest { @After fun teardown() { // Restore the default tables (since some tests will have added extras to the cache) - FontScaleConverterFactory.sLookupTables = defaultLookupTables + if (defaultLookupTables != null) { + FontScaleConverterFactory.sLookupTables = defaultLookupTables!! + } } @Test diff --git a/core/tests/coretests/src/android/text/TextLineLetterSpacingTest.kt b/core/tests/coretests/src/android/text/TextLineLetterSpacingTest.kt new file mode 100644 index 000000000000..27869bbf5392 --- /dev/null +++ b/core/tests/coretests/src/android/text/TextLineLetterSpacingTest.kt @@ -0,0 +1,224 @@ +/* + * 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.text + +import android.graphics.Paint +import android.platform.test.annotations.RequiresFlagsEnabled +import android.platform.test.flag.junit.DeviceFlagsValueProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.text.flags.Flags.FLAG_INTER_CHARACTER_JUSTIFICATION +import com.google.common.truth.Truth.assertThat +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +const val LEFT_EDGE = Paint.TEXT_RUN_FLAG_LEFT_EDGE +const val RIGHT_EDGE = Paint.TEXT_RUN_FLAG_RIGHT_EDGE +const val MIDDLE_OF_LINE = 0 +const val WHOLE_LINE = LEFT_EDGE or RIGHT_EDGE + +@SmallTest +@RunWith(AndroidJUnit4::class) +class TextLineLetterSpacingTest { + + @Rule + @JvmField + val mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule() + + @RequiresFlagsEnabled(FLAG_INTER_CHARACTER_JUSTIFICATION) + @Test + fun calculateRunFlagTest() { + // Only one Bidi run + assertThat(TextLine.calculateRunFlag(0, 1, Layout.DIR_LEFT_TO_RIGHT)) + .isEqualTo(WHOLE_LINE) + assertThat(TextLine.calculateRunFlag(0, 1, Layout.DIR_RIGHT_TO_LEFT)) + .isEqualTo(WHOLE_LINE) + + // Two BiDi Runs. + // If the layout is LTR, the first run is the left most run. + assertThat(TextLine.calculateRunFlag(0, 2, Layout.DIR_LEFT_TO_RIGHT)) + .isEqualTo(LEFT_EDGE) + // If the layout is LTR, the last run is the right most run. + assertThat(TextLine.calculateRunFlag(1, 2, Layout.DIR_LEFT_TO_RIGHT)) + .isEqualTo(RIGHT_EDGE) + // If the layout is RTL, the first run is the right most run. + assertThat(TextLine.calculateRunFlag(0, 2, Layout.DIR_RIGHT_TO_LEFT)) + .isEqualTo(RIGHT_EDGE) + // If the layout is RTL, the last run is the left most run. + assertThat(TextLine.calculateRunFlag(1, 2, Layout.DIR_RIGHT_TO_LEFT)) + .isEqualTo(LEFT_EDGE) + + // Three BiDi Runs. + // If the layout is LTR, the first run is the left most run. + assertThat(TextLine.calculateRunFlag(0, 3, Layout.DIR_LEFT_TO_RIGHT)) + .isEqualTo(LEFT_EDGE) + // Regardless of the context direction, the middle run must not have any flags. + assertThat(TextLine.calculateRunFlag(1, 3, Layout.DIR_LEFT_TO_RIGHT)) + .isEqualTo(MIDDLE_OF_LINE) + // If the layout is LTR, the last run is the right most run. + assertThat(TextLine.calculateRunFlag(2, 3, Layout.DIR_LEFT_TO_RIGHT)) + .isEqualTo(RIGHT_EDGE) + // If the layout is RTL, the first run is the right most run. + assertThat(TextLine.calculateRunFlag(0, 3, Layout.DIR_RIGHT_TO_LEFT)) + .isEqualTo(RIGHT_EDGE) + // Regardless of the context direction, the middle run must not have any flags. + assertThat(TextLine.calculateRunFlag(1, 3, Layout.DIR_RIGHT_TO_LEFT)) + .isEqualTo(MIDDLE_OF_LINE) + // If the layout is RTL, the last run is the left most run. + assertThat(TextLine.calculateRunFlag(2, 3, Layout.DIR_RIGHT_TO_LEFT)) + .isEqualTo(LEFT_EDGE) + } + + @RequiresFlagsEnabled(FLAG_INTER_CHARACTER_JUSTIFICATION) + @Test + fun resolveRunFlagForSubSequenceTest() { + val runStart = 5 + val runEnd = 15 + // Regardless of the run directions, if the span covers entire Bidi run, the same runFlag + // should be returned. + assertThat(TextLine.resolveRunFlagForSubSequence( + LEFT_EDGE, false, runStart, runEnd, runStart, runEnd)) + .isEqualTo(LEFT_EDGE) + assertThat(TextLine.resolveRunFlagForSubSequence( + LEFT_EDGE, true, runStart, runEnd, runStart, runEnd)) + .isEqualTo(LEFT_EDGE) + assertThat(TextLine.resolveRunFlagForSubSequence( + RIGHT_EDGE, false, runStart, runEnd, runStart, runEnd)) + .isEqualTo(RIGHT_EDGE) + assertThat(TextLine.resolveRunFlagForSubSequence( + RIGHT_EDGE, true, runStart, runEnd, runStart, runEnd)) + .isEqualTo(RIGHT_EDGE) + assertThat(TextLine.resolveRunFlagForSubSequence( + WHOLE_LINE, false, runStart, runEnd, runStart, runEnd)) + .isEqualTo(WHOLE_LINE) + assertThat(TextLine.resolveRunFlagForSubSequence( + WHOLE_LINE, true, runStart, runEnd, runStart, runEnd)) + .isEqualTo(WHOLE_LINE) + assertThat(TextLine.resolveRunFlagForSubSequence( + MIDDLE_OF_LINE, false, runStart, runEnd, runStart, runEnd)) + .isEqualTo(MIDDLE_OF_LINE) + assertThat(TextLine.resolveRunFlagForSubSequence( + MIDDLE_OF_LINE, true, runStart, runEnd, runStart, runEnd)) + .isEqualTo(MIDDLE_OF_LINE) + + + + // Left edge of LTR text, span start from run start offset but not cover the run. + assertThat(TextLine.resolveRunFlagForSubSequence( + LEFT_EDGE, false, runStart, runEnd, runStart, runEnd - 1)) + .isEqualTo(LEFT_EDGE) + // Left edge of RTL text, span start from run start offset but not cover the run. + assertThat(TextLine.resolveRunFlagForSubSequence( + LEFT_EDGE, true, runStart, runEnd, runStart, runEnd - 1)) + .isEqualTo(MIDDLE_OF_LINE) + // Right edge of LTR text, span start from run start offset but not cover the run. + assertThat(TextLine.resolveRunFlagForSubSequence( + RIGHT_EDGE, false, runStart, runEnd, runStart, runEnd - 1)) + .isEqualTo(MIDDLE_OF_LINE) + // Right edge of RTL text, span start from run start offset but not cover the run. + assertThat(TextLine.resolveRunFlagForSubSequence( + RIGHT_EDGE, true, runStart, runEnd, runStart, runEnd - 1)) + .isEqualTo(RIGHT_EDGE) + // Whole line of LTR text, span start from run start offset but not cover the run. + assertThat(TextLine.resolveRunFlagForSubSequence( + WHOLE_LINE, false, runStart, runEnd, runStart, runEnd - 1)) + .isEqualTo(LEFT_EDGE) + // Whole line of RTL text, span start from run start offset but not cover the run. + assertThat(TextLine.resolveRunFlagForSubSequence( + WHOLE_LINE, true, runStart, runEnd, runStart, runEnd - 1)) + .isEqualTo(RIGHT_EDGE) + // Middle of LTR text, span start from run start offset but not cover the run. + assertThat(TextLine.resolveRunFlagForSubSequence( + MIDDLE_OF_LINE, false, runStart, runEnd, runStart, runEnd - 1)) + .isEqualTo(MIDDLE_OF_LINE) + // Middle of RTL text, span start from run start offset but not cover the run. + assertThat(TextLine.resolveRunFlagForSubSequence( + MIDDLE_OF_LINE, true, runStart, runEnd, runStart, runEnd - 1)) + .isEqualTo(MIDDLE_OF_LINE) + + + + // Left edge of LTR text, span start from middle of run start offset until end of run. + assertThat(TextLine.resolveRunFlagForSubSequence( + LEFT_EDGE, false, runStart, runEnd, runStart + 1, runEnd)) + .isEqualTo(MIDDLE_OF_LINE) + // Left edge of RTL text, span start from middle of run start offset until end of run. + assertThat(TextLine.resolveRunFlagForSubSequence( + LEFT_EDGE, true, runStart, runEnd, runStart + 1, runEnd)) + .isEqualTo(LEFT_EDGE) + // Right edge of LTR text, span start from middle of run start offset until end of run. + assertThat(TextLine.resolveRunFlagForSubSequence( + RIGHT_EDGE, false, runStart, runEnd, runStart + 1, runEnd)) + .isEqualTo(RIGHT_EDGE) + // Right edge of RTL text, span start from middle of run start offset until end of run. + assertThat(TextLine.resolveRunFlagForSubSequence( + RIGHT_EDGE, true, runStart, runEnd, runStart + 1, runEnd)) + .isEqualTo(MIDDLE_OF_LINE) + // Whole line of LTR text, span start from middle of run start offset until end of run. + assertThat(TextLine.resolveRunFlagForSubSequence( + WHOLE_LINE, false, runStart, runEnd, runStart + 1, runEnd)) + .isEqualTo(RIGHT_EDGE) + // Whole line of RTL text, span start from middle of run start offset until end of run. + assertThat(TextLine.resolveRunFlagForSubSequence( + WHOLE_LINE, true, runStart, runEnd, runStart + 1, runEnd)) + .isEqualTo(LEFT_EDGE) + // Middle of LTR text, span start from middle of run start offset until end of run. + assertThat(TextLine.resolveRunFlagForSubSequence( + MIDDLE_OF_LINE, false, runStart, runEnd, runStart + 1, runEnd)) + .isEqualTo(MIDDLE_OF_LINE) + // Middle of RTL text, span start from middle of run start offset until end of run. + assertThat(TextLine.resolveRunFlagForSubSequence( + MIDDLE_OF_LINE, true, runStart, runEnd, runStart + 1, runEnd)) + .isEqualTo(MIDDLE_OF_LINE) + + + + // Left edge of LTR text, span start from middle of run. + assertThat(TextLine.resolveRunFlagForSubSequence( + LEFT_EDGE, false, runStart, runEnd, runStart + 1, runEnd - 1)) + .isEqualTo(MIDDLE_OF_LINE) + // Left edge of RTL text, span start from middle of run. + assertThat(TextLine.resolveRunFlagForSubSequence( + LEFT_EDGE, true, runStart, runEnd, runStart + 1, runEnd - 1)) + .isEqualTo(MIDDLE_OF_LINE) + // Right edge of LTR text, span start from middle of run. + assertThat(TextLine.resolveRunFlagForSubSequence( + RIGHT_EDGE, false, runStart, runEnd, runStart + 1, runEnd - 1)) + .isEqualTo(MIDDLE_OF_LINE) + // Right edge of RTL text, span start from middle of run. + assertThat(TextLine.resolveRunFlagForSubSequence( + RIGHT_EDGE, true, runStart, runEnd, runStart + 1, runEnd - 1)) + .isEqualTo(MIDDLE_OF_LINE) + // Whole line of LTR text, span start from middle of run. + assertThat(TextLine.resolveRunFlagForSubSequence( + WHOLE_LINE, false, runStart, runEnd, runStart + 1, runEnd - 1)) + .isEqualTo(MIDDLE_OF_LINE) + // Whole line of RTL text, span start from middle of run. + assertThat(TextLine.resolveRunFlagForSubSequence( + WHOLE_LINE, true, runStart, runEnd, runStart + 1, runEnd - 1)) + .isEqualTo(MIDDLE_OF_LINE) + // Middle of LTR text, span start from middle of run. + assertThat(TextLine.resolveRunFlagForSubSequence( + MIDDLE_OF_LINE, false, runStart, runEnd, runStart + 1, runEnd - 1)) + .isEqualTo(MIDDLE_OF_LINE) + // Middle of RTL text, span start from middle of run. + assertThat(TextLine.resolveRunFlagForSubSequence( + MIDDLE_OF_LINE, true, runStart, runEnd, runStart + 1, runEnd - 1)) + .isEqualTo(MIDDLE_OF_LINE) + } +}
\ No newline at end of file diff --git a/core/tests/coretests/src/android/tracing/perfetto/DataSourceTest.java b/core/tests/coretests/src/android/tracing/perfetto/DataSourceTest.java new file mode 100644 index 000000000000..bd2f36fb5198 --- /dev/null +++ b/core/tests/coretests/src/android/tracing/perfetto/DataSourceTest.java @@ -0,0 +1,664 @@ +/* + * 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.tracing.perfetto; + +import static android.internal.perfetto.protos.PerfettoTrace.TestEvent.PAYLOAD; +import static android.internal.perfetto.protos.PerfettoTrace.TestEvent.TestPayload.SINGLE_INT; +import static android.internal.perfetto.protos.PerfettoTrace.TracePacket.FOR_TESTING; + +import static java.io.File.createTempFile; +import static java.nio.file.Files.createTempDirectory; + +import android.internal.perfetto.protos.PerfettoTrace; +import android.tools.common.ScenarioBuilder; +import android.tools.common.Tag; +import android.tools.common.io.TraceType; +import android.tools.device.traces.TraceConfig; +import android.tools.device.traces.TraceConfigs; +import android.tools.device.traces.io.ResultReader; +import android.tools.device.traces.io.ResultWriter; +import android.tools.device.traces.monitors.PerfettoTraceMonitor; +import android.tools.device.traces.monitors.TraceMonitor; +import android.util.proto.ProtoInputStream; +import android.util.proto.ProtoOutputStream; + +import androidx.test.runner.AndroidJUnit4; + +import com.google.common.truth.Truth; +import com.google.protobuf.InvalidProtocolBufferException; + +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; + +import perfetto.protos.PerfettoConfig; +import perfetto.protos.TracePacketOuterClass; + +@RunWith(AndroidJUnit4.class) +public class DataSourceTest { + private final File mTracingDirectory = createTempDirectory("temp").toFile(); + + private final ResultWriter mWriter = new ResultWriter() + .forScenario(new ScenarioBuilder() + .forClass(createTempFile("temp", "").getName()).build()) + .withOutputDir(mTracingDirectory) + .setRunComplete(); + + private final TraceConfigs mTraceConfig = new TraceConfigs( + new TraceConfig(false, true, false), + new TraceConfig(false, true, false), + new TraceConfig(false, true, false), + new TraceConfig(false, true, false) + ); + + private static TestDataSource sTestDataSource; + + private static TestDataSource.DataSourceInstanceProvider sInstanceProvider; + private static TestDataSource.TlsStateProvider sTlsStateProvider; + private static TestDataSource.IncrementalStateProvider sIncrementalStateProvider; + + public DataSourceTest() throws IOException {} + + @BeforeClass + public static void beforeAll() { + Producer.init(InitArguments.DEFAULTS); + setupProviders(); + sTestDataSource = new TestDataSource( + (ds, idx, configStream) -> sInstanceProvider.provide(ds, idx, configStream), + args -> sTlsStateProvider.provide(args), + args -> sIncrementalStateProvider.provide(args)); + sTestDataSource.register(DataSourceParams.DEFAULTS); + } + + private static void setupProviders() { + sInstanceProvider = (ds, idx, configStream) -> + new TestDataSource.TestDataSourceInstance(ds, idx); + sTlsStateProvider = args -> new TestDataSource.TestTlsState(); + sIncrementalStateProvider = args -> new TestDataSource.TestIncrementalState(); + } + + @Before + public void setup() { + setupProviders(); + } + + @Test + public void canTraceData() throws InvalidProtocolBufferException { + final TraceMonitor traceMonitor = PerfettoTraceMonitor.newBuilder() + .enableCustomTrace(PerfettoConfig.DataSourceConfig.newBuilder() + .setName(sTestDataSource.name).build()).build(); + + try { + traceMonitor.start(); + + sTestDataSource.trace((ctx) -> { + final ProtoOutputStream protoOutputStream = ctx.newTracePacket(); + long forTestingToken = protoOutputStream.start(FOR_TESTING); + long payloadToken = protoOutputStream.start(PAYLOAD); + protoOutputStream.write(SINGLE_INT, 10); + protoOutputStream.end(payloadToken); + protoOutputStream.end(forTestingToken); + }); + } finally { + traceMonitor.stop(mWriter); + } + + final ResultReader reader = new ResultReader(mWriter.write(), mTraceConfig); + final byte[] rawProtoFromFile = reader.readBytes(TraceType.PERFETTO, Tag.ALL); + assert rawProtoFromFile != null; + final perfetto.protos.TraceOuterClass.Trace trace = perfetto.protos.TraceOuterClass.Trace + .parseFrom(rawProtoFromFile); + + Truth.assertThat(trace.getPacketCount()).isGreaterThan(0); + final List<TracePacketOuterClass.TracePacket> tracePackets = trace.getPacketList() + .stream().filter(TracePacketOuterClass.TracePacket::hasForTesting).toList(); + final List<TracePacketOuterClass.TracePacket> matchingPackets = tracePackets.stream() + .filter(it -> it.getForTesting().getPayload().getSingleInt() == 10).toList(); + Truth.assertThat(matchingPackets).hasSize(1); + } + + @Test + public void canUseTlsStateForCustomState() { + final int expectedStateTestValue = 10; + final AtomicInteger actualStateTestValue = new AtomicInteger(); + + final TraceMonitor traceMonitor = PerfettoTraceMonitor.newBuilder() + .enableCustomTrace(PerfettoConfig.DataSourceConfig.newBuilder() + .setName(sTestDataSource.name).build()).build(); + + try { + traceMonitor.start(); + + sTestDataSource.trace((ctx) -> { + TestDataSource.TestTlsState state = ctx.getCustomTlsState(); + state.testStateValue = expectedStateTestValue; + }); + + sTestDataSource.trace((ctx) -> { + TestDataSource.TestTlsState state = ctx.getCustomTlsState(); + actualStateTestValue.set(state.testStateValue); + }); + } finally { + traceMonitor.stop(mWriter); + } + + Truth.assertThat(actualStateTestValue.get()).isEqualTo(expectedStateTestValue); + } + + @Test + public void eachInstanceHasOwnTlsState() { + final int[] expectedStateTestValues = new int[] { 1, 2 }; + final int[] actualStateTestValues = new int[] { 0, 0 }; + + final TraceMonitor traceMonitor1 = PerfettoTraceMonitor.newBuilder() + .enableCustomTrace(PerfettoConfig.DataSourceConfig.newBuilder() + .setName(sTestDataSource.name).build()).build(); + final TraceMonitor traceMonitor2 = PerfettoTraceMonitor.newBuilder() + .enableCustomTrace(PerfettoConfig.DataSourceConfig.newBuilder() + .setName(sTestDataSource.name).build()).build(); + + try { + traceMonitor1.start(); + try { + traceMonitor2.start(); + + AtomicInteger index = new AtomicInteger(0); + sTestDataSource.trace((ctx) -> { + TestDataSource.TestTlsState state = ctx.getCustomTlsState(); + state.testStateValue = expectedStateTestValues[index.getAndIncrement()]; + }); + + index.set(0); + sTestDataSource.trace((ctx) -> { + TestDataSource.TestTlsState state = ctx.getCustomTlsState(); + actualStateTestValues[index.getAndIncrement()] = state.testStateValue; + }); + } finally { + traceMonitor1.stop(mWriter); + } + } finally { + traceMonitor2.stop(mWriter); + } + + Truth.assertThat(actualStateTestValues[0]).isEqualTo(expectedStateTestValues[0]); + Truth.assertThat(actualStateTestValues[1]).isEqualTo(expectedStateTestValues[1]); + } + + @Test + public void eachThreadHasOwnTlsState() throws InterruptedException { + final int thread1ExpectedStateValue = 1; + final int thread2ExpectedStateValue = 2; + + final AtomicInteger thread1ActualStateValue = new AtomicInteger(); + final AtomicInteger thread2ActualStateValue = new AtomicInteger(); + + final CountDownLatch setUpLatch = new CountDownLatch(2); + final CountDownLatch setStateLatch = new CountDownLatch(2); + final CountDownLatch setOutStateLatch = new CountDownLatch(2); + + final RunnableCreator createTask = (stateValue, stateOut) -> () -> { + Producer.init(InitArguments.DEFAULTS); + + setUpLatch.countDown(); + + try { + setUpLatch.await(3, TimeUnit.SECONDS); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + + sTestDataSource.trace((ctx) -> { + TestDataSource.TestTlsState state = ctx.getCustomTlsState(); + state.testStateValue = stateValue; + setStateLatch.countDown(); + }); + + try { + setStateLatch.await(3, TimeUnit.SECONDS); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + + sTestDataSource.trace((ctx) -> { + stateOut.set(ctx.getCustomTlsState().testStateValue); + setOutStateLatch.countDown(); + }); + }; + + final TraceMonitor traceMonitor = PerfettoTraceMonitor.newBuilder() + .enableCustomTrace(PerfettoConfig.DataSourceConfig.newBuilder() + .setName(sTestDataSource.name).build()).build(); + + try { + traceMonitor.start(); + + new Thread( + createTask.create(thread1ExpectedStateValue, thread1ActualStateValue)).start(); + new Thread( + createTask.create(thread2ExpectedStateValue, thread2ActualStateValue)).start(); + + setOutStateLatch.await(3, TimeUnit.SECONDS); + + } finally { + traceMonitor.stop(mWriter); + } + + Truth.assertThat(thread1ActualStateValue.get()).isEqualTo(thread1ExpectedStateValue); + Truth.assertThat(thread2ActualStateValue.get()).isEqualTo(thread2ExpectedStateValue); + } + + @Test + public void incrementalStateIsReset() throws InterruptedException { + + final TraceMonitor traceMonitor = PerfettoTraceMonitor.newBuilder() + .enableCustomTrace(PerfettoConfig.DataSourceConfig.newBuilder() + .setName(sTestDataSource.name).build()) + .setIncrementalTimeout(10) + .build(); + + final AtomicInteger testStateValue = new AtomicInteger(); + try { + traceMonitor.start(); + + sTestDataSource.trace(ctx -> ctx.getIncrementalState().testStateValue = 1); + + // Timeout to make sure the incremental state is cleared. + Thread.sleep(1000); + + sTestDataSource.trace(ctx -> + testStateValue.set(ctx.getIncrementalState().testStateValue)); + } finally { + traceMonitor.stop(mWriter); + } + + Truth.assertThat(testStateValue.get()).isNotEqualTo(1); + } + + @Test + public void getInstanceConfigOnCreateInstance() throws IOException { + final int expectedDummyIntValue = 10; + AtomicReference<ProtoInputStream> configStream = new AtomicReference<>(); + sInstanceProvider = (ds, idx, config) -> { + configStream.set(config); + return new TestDataSource.TestDataSourceInstance(ds, idx); + }; + + final TraceMonitor monitor = PerfettoTraceMonitor.newBuilder() + .enableCustomTrace(PerfettoConfig.DataSourceConfig.newBuilder() + .setName(sTestDataSource.name) + .setForTesting(PerfettoConfig.TestConfig.newBuilder().setDummyFields( + PerfettoConfig.TestConfig.DummyFields.newBuilder() + .setFieldInt32(expectedDummyIntValue) + .build()) + .build()) + .build()) + .build(); + + try { + monitor.start(); + } finally { + monitor.stop(mWriter); + } + + int configDummyIntValue = 0; + while (configStream.get().nextField() != ProtoInputStream.NO_MORE_FIELDS) { + if (configStream.get().getFieldNumber() + == (int) PerfettoTrace.DataSourceConfig.FOR_TESTING) { + final long forTestingToken = configStream.get() + .start(PerfettoTrace.DataSourceConfig.FOR_TESTING); + while (configStream.get().nextField() != ProtoInputStream.NO_MORE_FIELDS) { + if (configStream.get().getFieldNumber() + == (int) PerfettoTrace.TestConfig.DUMMY_FIELDS) { + final long dummyFieldsToken = configStream.get() + .start(PerfettoTrace.TestConfig.DUMMY_FIELDS); + while (configStream.get().nextField() != ProtoInputStream.NO_MORE_FIELDS) { + if (configStream.get().getFieldNumber() + == (int) PerfettoTrace.TestConfig.DummyFields.FIELD_INT32) { + int val = configStream.get().readInt( + PerfettoTrace.TestConfig.DummyFields.FIELD_INT32); + if (val != 0) { + configDummyIntValue = val; + break; + } + } + } + configStream.get().end(dummyFieldsToken); + break; + } + } + configStream.get().end(forTestingToken); + break; + } + } + + Truth.assertThat(configDummyIntValue).isEqualTo(expectedDummyIntValue); + } + + @Test + public void multipleTraceInstances() throws IOException, InterruptedException { + final int instanceCount = 3; + + final List<TraceMonitor> monitors = new ArrayList<>(); + final List<ResultWriter> writers = new ArrayList<>(); + + for (int i = 0; i < instanceCount; i++) { + final ResultWriter writer = new ResultWriter() + .forScenario(new ScenarioBuilder() + .forClass(createTempFile("temp", "").getName()).build()) + .withOutputDir(mTracingDirectory) + .setRunComplete(); + writers.add(writer); + } + + // Start at 1 because 0 is considered null value so payload will be ignored in that case + TestDataSource.TestTlsState.lastIndex = 1; + + final AtomicInteger traceCallCount = new AtomicInteger(); + final CountDownLatch latch = new CountDownLatch(instanceCount); + + try { + // Start instances + for (int i = 0; i < instanceCount; i++) { + final TraceMonitor traceMonitor = PerfettoTraceMonitor.newBuilder() + .enableCustomTrace(PerfettoConfig.DataSourceConfig.newBuilder() + .setName(sTestDataSource.name).build()).build(); + monitors.add(traceMonitor); + traceMonitor.start(); + } + + // Trace the stateIndex of the tracing instance. + sTestDataSource.trace(ctx -> { + final int testIntValue = ctx.getCustomTlsState().stateIndex; + traceCallCount.incrementAndGet(); + + final ProtoOutputStream os = ctx.newTracePacket(); + long forTestingToken = os.start(FOR_TESTING); + long payloadToken = os.start(PAYLOAD); + os.write(SINGLE_INT, testIntValue); + os.end(payloadToken); + os.end(forTestingToken); + + latch.countDown(); + }); + } finally { + // Stop instances + for (int i = 0; i < instanceCount; i++) { + final TraceMonitor monitor = monitors.get(i); + final ResultWriter writer = writers.get(i); + monitor.stop(writer); + } + } + + latch.await(3, TimeUnit.SECONDS); + Truth.assertThat(traceCallCount.get()).isEqualTo(instanceCount); + + for (int i = 0; i < instanceCount; i++) { + final int expectedTracedValue = i + 1; + final ResultWriter writer = writers.get(i); + final ResultReader reader = new ResultReader(writer.write(), mTraceConfig); + final byte[] rawProtoFromFile = reader.readBytes(TraceType.PERFETTO, Tag.ALL); + assert rawProtoFromFile != null; + final perfetto.protos.TraceOuterClass.Trace trace = + perfetto.protos.TraceOuterClass.Trace.parseFrom(rawProtoFromFile); + + Truth.assertThat(trace.getPacketCount()).isGreaterThan(0); + final List<TracePacketOuterClass.TracePacket> tracePackets = trace.getPacketList() + .stream().filter(TracePacketOuterClass.TracePacket::hasForTesting).toList(); + Truth.assertWithMessage("One packet has for testing data") + .that(tracePackets).hasSize(1); + + final List<TracePacketOuterClass.TracePacket> matchingPackets = + tracePackets.stream() + .filter(it -> it.getForTesting().getPayload() + .getSingleInt() == expectedTracedValue).toList(); + Truth.assertWithMessage( + "One packet has testing data with a payload with the expected value") + .that(matchingPackets).hasSize(1); + } + } + + @Test + public void onStartCallbackTriggered() throws InterruptedException { + final CountDownLatch latch = new CountDownLatch(1); + + final AtomicBoolean callbackCalled = new AtomicBoolean(false); + sInstanceProvider = (ds, idx, config) -> new TestDataSource.TestDataSourceInstance( + ds, + idx, + (args) -> { + callbackCalled.set(true); + latch.countDown(); + }, + (args) -> {}, + (args) -> {} + ); + + final TraceMonitor traceMonitor = PerfettoTraceMonitor.newBuilder() + .enableCustomTrace(PerfettoConfig.DataSourceConfig.newBuilder() + .setName(sTestDataSource.name).build()).build(); + + Truth.assertThat(callbackCalled.get()).isFalse(); + try { + traceMonitor.start(); + latch.await(3, TimeUnit.SECONDS); + Truth.assertThat(callbackCalled.get()).isTrue(); + } finally { + traceMonitor.stop(mWriter); + } + } + + @Test + public void onFlushCallbackTriggered() throws InterruptedException { + final CountDownLatch latch = new CountDownLatch(1); + final AtomicBoolean callbackCalled = new AtomicBoolean(false); + sInstanceProvider = (ds, idx, config) -> + new TestDataSource.TestDataSourceInstance( + ds, + idx, + (args) -> {}, + (args) -> { + callbackCalled.set(true); + latch.countDown(); + }, + (args) -> {} + ); + + final TraceMonitor traceMonitor = PerfettoTraceMonitor.newBuilder() + .enableCustomTrace(PerfettoConfig.DataSourceConfig.newBuilder() + .setName(sTestDataSource.name).build()).build(); + + try { + traceMonitor.start(); + Truth.assertThat(callbackCalled.get()).isFalse(); + sTestDataSource.trace((ctx) -> { + final ProtoOutputStream protoOutputStream = ctx.newTracePacket(); + long forTestingToken = protoOutputStream.start(FOR_TESTING); + long payloadToken = protoOutputStream.start(PAYLOAD); + protoOutputStream.write(SINGLE_INT, 10); + protoOutputStream.end(payloadToken); + protoOutputStream.end(forTestingToken); + }); + } finally { + traceMonitor.stop(mWriter); + } + + latch.await(3, TimeUnit.SECONDS); + Truth.assertThat(callbackCalled.get()).isTrue(); + } + + @Test + public void onStopCallbackTriggered() throws InterruptedException { + final CountDownLatch latch = new CountDownLatch(1); + final AtomicBoolean callbackCalled = new AtomicBoolean(false); + sInstanceProvider = (ds, idx, config) -> + new TestDataSource.TestDataSourceInstance( + ds, + idx, + (args) -> {}, + (args) -> {}, + (args) -> { + callbackCalled.set(true); + latch.countDown(); + } + ); + + final TraceMonitor traceMonitor = PerfettoTraceMonitor.newBuilder() + .enableCustomTrace(PerfettoConfig.DataSourceConfig.newBuilder() + .setName(sTestDataSource.name).build()).build(); + + try { + traceMonitor.start(); + Truth.assertThat(callbackCalled.get()).isFalse(); + } finally { + traceMonitor.stop(mWriter); + } + + latch.await(3, TimeUnit.SECONDS); + Truth.assertThat(callbackCalled.get()).isTrue(); + } + + @Test + public void canUseDataSourceInstanceToCreateTlsState() throws InvalidProtocolBufferException { + final Object testObject = new Object(); + + sInstanceProvider = (ds, idx, configStream) -> { + final TestDataSource.TestDataSourceInstance dsInstance = + new TestDataSource.TestDataSourceInstance(ds, idx); + dsInstance.testObject = testObject; + return dsInstance; + }; + + sTlsStateProvider = args -> { + final TestDataSource.TestTlsState tlsState = new TestDataSource.TestTlsState(); + + try (TestDataSource.TestDataSourceInstance dataSourceInstance = + args.getDataSourceInstanceLocked()) { + if (dataSourceInstance != null) { + tlsState.testStateValue = dataSourceInstance.testObject.hashCode(); + } + } + + return tlsState; + }; + + final TraceMonitor traceMonitor = PerfettoTraceMonitor.newBuilder() + .enableCustomTrace(PerfettoConfig.DataSourceConfig.newBuilder() + .setName(sTestDataSource.name).build()).build(); + + try { + traceMonitor.start(); + sTestDataSource.trace((ctx) -> { + final ProtoOutputStream protoOutputStream = ctx.newTracePacket(); + long forTestingToken = protoOutputStream.start(FOR_TESTING); + long payloadToken = protoOutputStream.start(PAYLOAD); + protoOutputStream.write(SINGLE_INT, ctx.getCustomTlsState().testStateValue); + protoOutputStream.end(payloadToken); + protoOutputStream.end(forTestingToken); + }); + } finally { + traceMonitor.stop(mWriter); + } + + final ResultReader reader = new ResultReader(mWriter.write(), mTraceConfig); + final byte[] rawProtoFromFile = reader.readBytes(TraceType.PERFETTO, Tag.ALL); + assert rawProtoFromFile != null; + final perfetto.protos.TraceOuterClass.Trace trace = perfetto.protos.TraceOuterClass.Trace + .parseFrom(rawProtoFromFile); + + Truth.assertThat(trace.getPacketCount()).isGreaterThan(0); + final List<TracePacketOuterClass.TracePacket> tracePackets = trace.getPacketList() + .stream().filter(TracePacketOuterClass.TracePacket::hasForTesting).toList(); + final List<TracePacketOuterClass.TracePacket> matchingPackets = tracePackets.stream() + .filter(it -> it.getForTesting().getPayload().getSingleInt() + == testObject.hashCode()).toList(); + Truth.assertThat(matchingPackets).hasSize(1); + } + + @Test + public void canUseDataSourceInstanceToCreateIncrementalState() + throws InvalidProtocolBufferException { + final Object testObject = new Object(); + + sInstanceProvider = (ds, idx, configStream) -> { + final TestDataSource.TestDataSourceInstance dsInstance = + new TestDataSource.TestDataSourceInstance(ds, idx); + dsInstance.testObject = testObject; + return dsInstance; + }; + + sIncrementalStateProvider = args -> { + final TestDataSource.TestIncrementalState incrementalState = + new TestDataSource.TestIncrementalState(); + + try (TestDataSource.TestDataSourceInstance dataSourceInstance = + args.getDataSourceInstanceLocked()) { + if (dataSourceInstance != null) { + incrementalState.testStateValue = dataSourceInstance.testObject.hashCode(); + } + } + + return incrementalState; + }; + + final TraceMonitor traceMonitor = PerfettoTraceMonitor.newBuilder() + .enableCustomTrace(PerfettoConfig.DataSourceConfig.newBuilder() + .setName(sTestDataSource.name).build()).build(); + + try { + traceMonitor.start(); + sTestDataSource.trace((ctx) -> { + final ProtoOutputStream protoOutputStream = ctx.newTracePacket(); + long forTestingToken = protoOutputStream.start(FOR_TESTING); + long payloadToken = protoOutputStream.start(PAYLOAD); + protoOutputStream.write(SINGLE_INT, ctx.getIncrementalState().testStateValue); + protoOutputStream.end(payloadToken); + protoOutputStream.end(forTestingToken); + }); + } finally { + traceMonitor.stop(mWriter); + } + + final ResultReader reader = new ResultReader(mWriter.write(), mTraceConfig); + final byte[] rawProtoFromFile = reader.readBytes(TraceType.PERFETTO, Tag.ALL); + assert rawProtoFromFile != null; + final perfetto.protos.TraceOuterClass.Trace trace = perfetto.protos.TraceOuterClass.Trace + .parseFrom(rawProtoFromFile); + + Truth.assertThat(trace.getPacketCount()).isGreaterThan(0); + final List<TracePacketOuterClass.TracePacket> tracePackets = trace.getPacketList() + .stream().filter(TracePacketOuterClass.TracePacket::hasForTesting).toList(); + final List<TracePacketOuterClass.TracePacket> matchingPackets = tracePackets.stream() + .filter(it -> it.getForTesting().getPayload().getSingleInt() + == testObject.hashCode()).toList(); + Truth.assertThat(matchingPackets).hasSize(1); + } + + interface RunnableCreator { + Runnable create(int state, AtomicInteger stateOut); + } +} diff --git a/core/tests/coretests/src/android/tracing/perfetto/TestDataSource.java b/core/tests/coretests/src/android/tracing/perfetto/TestDataSource.java new file mode 100644 index 000000000000..d78f78b1cb0e --- /dev/null +++ b/core/tests/coretests/src/android/tracing/perfetto/TestDataSource.java @@ -0,0 +1,122 @@ +/* + * 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.tracing.perfetto; + +import android.util.proto.ProtoInputStream; + +import java.util.UUID; +import java.util.function.Consumer; + +public class TestDataSource extends DataSource<TestDataSource.TestDataSourceInstance, + TestDataSource.TestTlsState, TestDataSource.TestIncrementalState> { + private final DataSourceInstanceProvider mDataSourceInstanceProvider; + private final TlsStateProvider mTlsStateProvider; + private final IncrementalStateProvider mIncrementalStateProvider; + + interface DataSourceInstanceProvider { + TestDataSourceInstance provide( + TestDataSource dataSource, int instanceIndex, ProtoInputStream configStream); + } + + interface TlsStateProvider { + TestTlsState provide(CreateTlsStateArgs<TestDataSourceInstance> args); + } + + interface IncrementalStateProvider { + TestIncrementalState provide(CreateIncrementalStateArgs<TestDataSourceInstance> args); + } + + public TestDataSource() { + this((ds, idx, config) -> new TestDataSourceInstance(ds, idx), + args -> new TestTlsState(), args -> new TestIncrementalState()); + } + + public TestDataSource( + DataSourceInstanceProvider dataSourceInstanceProvider, + TlsStateProvider tlsStateProvider, + IncrementalStateProvider incrementalStateProvider + ) { + super("android.tracing.perfetto.TestDataSource#" + UUID.randomUUID().toString()); + this.mDataSourceInstanceProvider = dataSourceInstanceProvider; + this.mTlsStateProvider = tlsStateProvider; + this.mIncrementalStateProvider = incrementalStateProvider; + } + + @Override + public TestDataSourceInstance createInstance(ProtoInputStream configStream, int instanceIndex) { + return mDataSourceInstanceProvider.provide(this, instanceIndex, configStream); + } + + @Override + public TestTlsState createTlsState(CreateTlsStateArgs args) { + return mTlsStateProvider.provide(args); + } + + @Override + public TestIncrementalState createIncrementalState(CreateIncrementalStateArgs args) { + return mIncrementalStateProvider.provide(args); + } + + public static class TestTlsState { + public int testStateValue; + public int stateIndex = lastIndex++; + + public static int lastIndex = 0; + } + + public static class TestIncrementalState { + public int testStateValue; + } + + public static class TestDataSourceInstance extends DataSourceInstance { + public Object testObject; + Consumer<StartCallbackArguments> mStartCallback; + Consumer<FlushCallbackArguments> mFlushCallback; + Consumer<StopCallbackArguments> mStopCallback; + + public TestDataSourceInstance(DataSource dataSource, int instanceIndex) { + this(dataSource, instanceIndex, args -> {}, args -> {}, args -> {}); + } + + public TestDataSourceInstance( + DataSource dataSource, + int instanceIndex, + Consumer<StartCallbackArguments> startCallback, + Consumer<FlushCallbackArguments> flushCallback, + Consumer<StopCallbackArguments> stopCallback) { + super(dataSource, instanceIndex); + this.mStartCallback = startCallback; + this.mFlushCallback = flushCallback; + this.mStopCallback = stopCallback; + } + + @Override + public void onStart(StartCallbackArguments args) { + this.mStartCallback.accept(args); + } + + @Override + public void onFlush(FlushCallbackArguments args) { + this.mFlushCallback.accept(args); + } + + @Override + public void onStop(StopCallbackArguments args) { + this.mStopCallback.accept(args); + } + } +} diff --git a/core/tests/coretests/src/android/view/ViewRootImplTest.java b/core/tests/coretests/src/android/view/ViewRootImplTest.java index cf3eb12498ca..cfbda84f24e1 100644 --- a/core/tests/coretests/src/android/view/ViewRootImplTest.java +++ b/core/tests/coretests/src/android/view/ViewRootImplTest.java @@ -19,6 +19,7 @@ package android.view; 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; @@ -575,8 +576,13 @@ public class ViewRootImplTest { 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/com/android/internal/os/MonotonicClockTest.java b/core/tests/coretests/src/com/android/internal/os/MonotonicClockTest.java index 0742052cce53..ec4c563e469e 100644 --- a/core/tests/coretests/src/com/android/internal/os/MonotonicClockTest.java +++ b/core/tests/coretests/src/com/android/internal/os/MonotonicClockTest.java @@ -18,39 +18,67 @@ package com.android.internal.os; import static com.google.common.truth.Truth.assertThat; -import android.util.Xml; - import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; +import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileWriter; import java.io.IOException; +import java.nio.file.Files; @RunWith(AndroidJUnit4.class) @SmallTest public class MonotonicClockTest { private final MockClock mClock = new MockClock(); + private File mFile; + + @Before + public void setup() throws IOException { + File systemDir = Files.createTempDirectory("MonotonicClockTest").toFile(); + mFile = new File(systemDir, "test_monotonic_clock.xml"); + if (mFile.exists()) { + assertThat(mFile.delete()).isTrue(); + } + } @Test public void persistence() throws IOException { - MonotonicClock monotonicClock = new MonotonicClock(1000, mClock); + MonotonicClock monotonicClock = new MonotonicClock(mFile, 1000, mClock); mClock.realtime = 234; assertThat(monotonicClock.monotonicTime()).isEqualTo(1234); - ByteArrayOutputStream out = new ByteArrayOutputStream(); - monotonicClock.writeXml(out, Xml.newBinarySerializer()); + monotonicClock.write(); mClock.realtime = 42; - MonotonicClock newMonotonicClock = new MonotonicClock(0, mClock); - ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray()); - newMonotonicClock.readXml(in, Xml.newBinaryPullParser()); + MonotonicClock newMonotonicClock = new MonotonicClock(mFile, 0, mClock); mClock.realtime = 2000; assertThat(newMonotonicClock.monotonicTime()).isEqualTo(1234 - 42 + 2000); } + + @Test + public void constructor() { + MonotonicClock monotonicClock = new MonotonicClock(null, 1000, mClock); + mClock.realtime = 234; + + assertThat(monotonicClock.monotonicTime()).isEqualTo(1234); + } + + @Test + public void corruptedFile() throws IOException { + // Create an invalid binary XML file to cause IOException: "Unexpected magic number" + try (FileWriter w = new FileWriter(mFile)) { + w.write("garbage"); + } + + MonotonicClock monotonicClock = new MonotonicClock(mFile, 1000, mClock); + mClock.realtime = 234; + + assertThat(monotonicClock.monotonicTime()).isEqualTo(1234); + } } diff --git a/core/tests/coretests/src/com/android/internal/os/StoragedUidIoStatsReaderTest.java b/core/tests/coretests/src/com/android/internal/os/StoragedUidIoStatsReaderTest.java index 84c93c232d9f..80061a57b3d2 100644 --- a/core/tests/coretests/src/com/android/internal/os/StoragedUidIoStatsReaderTest.java +++ b/core/tests/coretests/src/com/android/internal/os/StoragedUidIoStatsReaderTest.java @@ -18,7 +18,6 @@ package com.android.internal.os; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; -import static org.mockito.Mockito.verifyZeroInteractions; import android.os.FileUtils; @@ -73,8 +72,7 @@ public class StoragedUidIoStatsReaderTest { @Test public void testReadNonexistentFile() throws Exception { mStoragedUidIoStatsReader.readAbsolute(mCallback); - verifyZeroInteractions(mCallback); - + verifyNoMoreInteractions(mCallback); } /** diff --git a/data/etc/com.android.documentsui.xml b/data/etc/com.android.documentsui.xml index d32cbecb16ec..2e521e30c673 100644 --- a/data/etc/com.android.documentsui.xml +++ b/data/etc/com.android.documentsui.xml @@ -23,5 +23,7 @@ <permission name="android.permission.MODIFY_QUIET_MODE"/> <permission name="android.permission.READ_COMPAT_CHANGE_CONFIG"/> <permission name="android.permission.START_FOREGROUND_SERVICES_FROM_BACKGROUND"/> + <!-- Permissions required for reading device configs --> + <permission name="android.permission.READ_DEVICE_CONFIG" /> </privapp-permissions> </permissions> diff --git a/data/etc/privapp-permissions-platform.xml b/data/etc/privapp-permissions-platform.xml index a1ea2b8ce032..4be75f83422e 100644 --- a/data/etc/privapp-permissions-platform.xml +++ b/data/etc/privapp-permissions-platform.xml @@ -426,6 +426,7 @@ applications that come with the platform <!-- Permissions required for CTS test - android.server.biometrics --> <permission name="android.permission.USE_BIOMETRIC" /> <permission name="android.permission.TEST_BIOMETRIC" /> + <permission name="android.permission.MANAGE_BIOMETRIC_DIALOG" /> <!-- Permissions required for CTS test - CtsContactsProviderTestCases --> <permission name="android.contacts.permission.MANAGE_SIM_ACCOUNTS" /> <!-- Permissions required for CTS test - CtsHdmiCecHostTestCases --> @@ -541,6 +542,8 @@ applications that come with the platform <permission name="android.permission.GET_BINDING_UID_IMPORTANCE"/> <!-- Permission required for CTS test NotificationManagerZenTest --> <permission name="android.permission.CONTROL_DISPLAY_COLOR_TRANSFORMS" /> + <!-- Permission required for BinaryTransparencyService shell API and host test --> + <permission name="android.permission.GET_BACKGROUND_INSTALLED_PACKAGES" /> </privapp-permissions> <privapp-permissions package="com.android.statementservice"> diff --git a/data/etc/services.core.protolog.json b/data/etc/services.core.protolog.json index 917a30061aca..3a778c314606 100644 --- a/data/etc/services.core.protolog.json +++ b/data/etc/services.core.protolog.json @@ -535,6 +535,12 @@ "group": "WM_DEBUG_TASKS", "at": "com\/android\/server\/wm\/ActivityStarter.java" }, + "-1583619037": { + "message": "Failed to register MediaProjectionWatcherCallback", + "level": "ERROR", + "group": "WM_ERROR", + "at": "com\/android\/server\/wm\/ScreenRecordingCallbackController.java" + }, "-1582845629": { "message": "Starting animation on %s", "level": "VERBOSE", @@ -2983,12 +2989,6 @@ "group": "WM_DEBUG_SCREEN_ON", "at": "com\/android\/server\/wm\/WindowManagerService.java" }, - "466506262": { - "message": "Clear freezing of %s: visible=%b freezing=%b", - "level": "VERBOSE", - "group": "WM_DEBUG_ORIENTATION", - "at": "com\/android\/server\/wm\/ActivityRecord.java" - }, "485170982": { "message": "Not finishing noHistory %s on stop because we're just sleeping", "level": "DEBUG", diff --git a/data/fonts/Android.bp b/data/fonts/Android.bp index 3dd9ba9db1d9..471acaa0b1b6 100644 --- a/data/fonts/Android.bp +++ b/data/fonts/Android.bp @@ -48,12 +48,35 @@ prebuilt_font { // Copies the font configuration file into system/etc for the product as fonts.xml. // Additional fonts should be installed to /product/fonts/ alongside a corresponding // fonts_customiztion.xml in /product/etc/ -prebuilt_etc { + +soong_config_bool_variable { + name: "use_var_font", +} + +soong_config_module_type { + name: "prebuilt_fonts_xml", + module_type: "prebuilt_etc", + config_namespace: "noto_sans_cjk_config", + bool_variables: ["use_var_font"], + properties: ["src"], +} + +prebuilt_fonts_xml { name: "fonts.xml", src: "fonts.xml", + soong_config_variables: { + use_var_font: { + src: "fonts_cjkvf.xml", + }, + }, } -prebuilt_etc { +prebuilt_fonts_xml { name: "font_fallback.xml", src: "font_fallback.xml", + soong_config_variables: { + use_var_font: { + src: "font_fallback_cjkvf.xml", + }, + }, } diff --git a/data/fonts/font_fallback_cjkvf.xml b/data/fonts/font_fallback_cjkvf.xml new file mode 100644 index 000000000000..70474bae11e9 --- /dev/null +++ b/data/fonts/font_fallback_cjkvf.xml @@ -0,0 +1,1015 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + In this file, all fonts without names are added to the default list. + Fonts are chosen based on a match: full BCP-47 language tag including + script, then just language, and finally order (the first font containing + the glyph). + + Order of appearance is also the tiebreaker for weight matching. This is + the reason why the 900 weights of Roboto precede the 700 weights - we + prefer the former when an 800 weight is requested. Since bold spans + effectively add 300 to the weight, this ensures that 900 is the bold + paired with the 500 weight, ensuring adequate contrast. + + TODO(rsheeter) update comment; ordering to match 800 to 900 is no longer required +--> +<familyset version="23"> + <!-- first font is default --> + <family name="sans-serif" varFamilyType="2"> + <font>Roboto-Regular.ttf + <axis tag="wdth" stylevalue="100" /> + </font> + </family> + + + <!-- Note that aliases must come after the fonts they reference. --> + <alias name="sans-serif-thin" to="sans-serif" weight="100" /> + <alias name="sans-serif-light" to="sans-serif" weight="300" /> + <alias name="sans-serif-medium" to="sans-serif" weight="500" /> + <alias name="sans-serif-black" to="sans-serif" weight="900" /> + <alias name="arial" to="sans-serif" /> + <alias name="helvetica" to="sans-serif" /> + <alias name="tahoma" to="sans-serif" /> + <alias name="verdana" to="sans-serif" /> + + <family name="sans-serif-condensed" varFamilyType="2"> + <font>Roboto-Regular.ttf + <axis tag="wdth" stylevalue="75" /> + </font> + </family> + <alias name="sans-serif-condensed-light" to="sans-serif-condensed" weight="300" /> + <alias name="sans-serif-condensed-medium" to="sans-serif-condensed" weight="500" /> + + <family name="serif"> + <font weight="400" style="normal" postScriptName="NotoSerif">NotoSerif-Regular.ttf</font> + <font weight="700" style="normal">NotoSerif-Bold.ttf</font> + <font weight="400" style="italic">NotoSerif-Italic.ttf</font> + <font weight="700" style="italic">NotoSerif-BoldItalic.ttf</font> + </family> + <alias name="serif-bold" to="serif" weight="700" /> + <alias name="times" to="serif" /> + <alias name="times new roman" to="serif" /> + <alias name="palatino" to="serif" /> + <alias name="georgia" to="serif" /> + <alias name="baskerville" to="serif" /> + <alias name="goudy" to="serif" /> + <alias name="fantasy" to="serif" /> + <alias name="ITC Stone Serif" to="serif" /> + + <family name="monospace"> + <font weight="400" style="normal">DroidSansMono.ttf</font> + </family> + <alias name="sans-serif-monospace" to="monospace" /> + <alias name="monaco" to="monospace" /> + + <family name="serif-monospace"> + <font weight="400" style="normal" postScriptName="CutiveMono-Regular">CutiveMono.ttf</font> + </family> + <alias name="courier" to="serif-monospace" /> + <alias name="courier new" to="serif-monospace" /> + + <family name="casual"> + <font weight="400" style="normal" postScriptName="ComingSoon-Regular">ComingSoon.ttf</font> + </family> + + <family name="cursive" varFamilyType="1"> + <font>DancingScript-Regular.ttf</font> + </family> + + <family name="sans-serif-smallcaps"> + <font weight="400" style="normal">CarroisGothicSC-Regular.ttf</font> + </family> + + <family name="source-sans-pro"> + <font weight="400" style="normal">SourceSansPro-Regular.ttf</font> + <font weight="400" style="italic">SourceSansPro-Italic.ttf</font> + <font weight="600" style="normal">SourceSansPro-SemiBold.ttf</font> + <font weight="600" style="italic">SourceSansPro-SemiBoldItalic.ttf</font> + <font weight="700" style="normal">SourceSansPro-Bold.ttf</font> + <font weight="700" style="italic">SourceSansPro-BoldItalic.ttf</font> + </family> + <alias name="source-sans-pro-semi-bold" to="source-sans-pro" weight="600"/> + + <family name="roboto-flex" varFamilyType="2"> + <font>RobotoFlex-Regular.ttf + <axis tag="wdth" stylevalue="100" /> + </font> + </family> + + <!-- fallback fonts --> + <family lang="und-Arab" variant="elegant"> + <font weight="400" style="normal" postScriptName="NotoNaskhArabic"> + NotoNaskhArabic-Regular.ttf + </font> + <font weight="700" style="normal">NotoNaskhArabic-Bold.ttf</font> + </family> + <family lang="und-Arab" variant="compact"> + <font weight="400" style="normal" postScriptName="NotoNaskhArabicUI"> + NotoNaskhArabicUI-Regular.ttf + </font> + <font weight="700" style="normal">NotoNaskhArabicUI-Bold.ttf</font> + </family> + <family lang="und-Ethi" varFamilyType="1"> + <font postScriptName="NotoSansEthiopic-Regular"> + NotoSansEthiopic-VF.ttf + </font> + <font fallbackFor="serif" postScriptName="NotoSerifEthiopic-Regular"> + NotoSerifEthiopic-VF.ttf + </font> + </family> + <family lang="und-Hebr"> + <font weight="400" style="normal" postScriptName="NotoSansHebrew"> + NotoSansHebrew-Regular.ttf + </font> + <font weight="700" style="normal">NotoSansHebrew-Bold.ttf</font> + <font weight="400" style="normal" fallbackFor="serif">NotoSerifHebrew-Regular.ttf</font> + <font weight="700" style="normal" fallbackFor="serif">NotoSerifHebrew-Bold.ttf</font> + </family> + <family lang="und-Thai" variant="elegant"> + <font weight="400" style="normal" postScriptName="NotoSansThai">NotoSansThai-Regular.ttf + </font> + <font weight="700" style="normal">NotoSansThai-Bold.ttf</font> + <font weight="400" style="normal" fallbackFor="serif"> + NotoSerifThai-Regular.ttf + </font> + <font weight="700" style="normal" fallbackFor="serif">NotoSerifThai-Bold.ttf</font> + </family> + <family lang="und-Thai" variant="compact"> + <font weight="400" style="normal" postScriptName="NotoSansThaiUI"> + NotoSansThaiUI-Regular.ttf + </font> + <font weight="700" style="normal">NotoSansThaiUI-Bold.ttf</font> + </family> + <family lang="und-Armn" varFamilyType="1"> + <font postScriptName="NotoSansArmenian-Regular"> + NotoSansArmenian-VF.ttf + </font> + <font fallbackFor="serif" postScriptName="NotoSerifArmenian-Regular"> + NotoSerifArmenian-VF.ttf + </font> + </family> + <family lang="und-Geor,und-Geok" varFamilyType="1"> + <font postScriptName="NotoSansGeorgian-Regular"> + NotoSansGeorgian-VF.ttf + </font> + <font fallbackFor="serif" postScriptName="NotoSerifGeorgian-Regular"> + NotoSerifGeorgian-VF.ttf + </font> + </family> + <family lang="und-Deva" variant="elegant" varFamilyType="1"> + <font postScriptName="NotoSansDevanagari-Regular"> + NotoSansDevanagari-VF.ttf + </font> + <font fallbackFor="serif" postScriptName="NotoSerifDevanagari-Regular"> + NotoSerifDevanagari-VF.ttf + </font> + </family> + <family lang="und-Deva" variant="compact" varFamilyType="1"> + <font postScriptName="NotoSansDevanagariUI-Regular"> + NotoSansDevanagariUI-VF.ttf + </font> + </family> + + <!-- All scripts of India should come after Devanagari, due to shared + danda characters. + --> + <family lang="und-Gujr" variant="elegant"> + <font weight="400" style="normal" postScriptName="NotoSansGujarati"> + NotoSansGujarati-Regular.ttf + </font> + <font weight="700" style="normal">NotoSansGujarati-Bold.ttf</font> + <font weight="400" style="normal" fallbackFor="serif" + postScriptName="NotoSerifGujarati-Regular">NotoSerifGujarati-VF.ttf + <axis tag="wght" stylevalue="400"/> + </font> + <font weight="500" style="normal" fallbackFor="serif" + postScriptName="NotoSerifGujarati-Regular">NotoSerifGujarati-VF.ttf + <axis tag="wght" stylevalue="500"/> + </font> + <font weight="600" style="normal" fallbackFor="serif" + postScriptName="NotoSerifGujarati-Regular">NotoSerifGujarati-VF.ttf + <axis tag="wght" stylevalue="600"/> + </font> + <font weight="700" style="normal" fallbackFor="serif" + postScriptName="NotoSerifGujarati-Regular">NotoSerifGujarati-VF.ttf + <axis tag="wght" stylevalue="700"/> + </font> + </family> + <family lang="und-Gujr" variant="compact"> + <font weight="400" style="normal" postScriptName="NotoSansGujaratiUI"> + NotoSansGujaratiUI-Regular.ttf + </font> + <font weight="700" style="normal">NotoSansGujaratiUI-Bold.ttf</font> + </family> + <family lang="und-Guru" variant="elegant" varFamilyType="1"> + <font postScriptName="NotoSansGurmukhi-Regular"> + NotoSansGurmukhi-VF.ttf + </font> + <font fallbackFor="serif" postScriptName="NotoSerifGurmukhi-Regular"> + NotoSerifGurmukhi-VF.ttf + </font> + </family> + <family lang="und-Guru" variant="compact" varFamilyType="1"> + <font postScriptName="NotoSansGurmukhiUI-Regular"> + NotoSansGurmukhiUI-VF.ttf + </font> + </family> + <family lang="und-Taml" variant="elegant" varFamilyType="1"> + <font postScriptName="NotoSansTamil-Regular"> + NotoSansTamil-VF.ttf + </font> + <font fallbackFor="serif" postScriptName="NotoSerifTamil-Regular"> + NotoSerifTamil-VF.ttf + </font> + </family> + <family lang="und-Taml" variant="compact" varFamilyType="1"> + <font postScriptName="NotoSansTamilUI-Regular"> + NotoSansTamilUI-VF.ttf + </font> + </family> + <family lang="und-Mlym" variant="elegant" varFamilyType="1"> + <font postScriptName="NotoSansMalayalam-Regular"> + NotoSansMalayalam-VF.ttf + </font> + <font fallbackFor="serif" postScriptName="NotoSerifMalayalam-Regular"> + NotoSerifMalayalam-VF.ttf + </font> + </family> + <family lang="und-Mlym" variant="compact" varFamilyType="1"> + <font postScriptName="NotoSansMalayalamUI-Regular"> + NotoSansMalayalamUI-VF.ttf + </font> + </family> + <family lang="und-Beng" variant="elegant" varFamilyType="1"> + <font postScriptName="NotoSansBengali-Regular"> + NotoSansBengali-VF.ttf + </font> + <font fallbackFor="serif" postScriptName="NotoSerifBengali-Regular"> + NotoSerifBengali-VF.ttf + </font> + </family> + <family lang="und-Beng" variant="compact" varFamilyType="1"> + <font postScriptName="NotoSansBengaliUI-Regular"> + NotoSansBengaliUI-VF.ttf + </font> + </family> + <family lang="und-Telu" variant="elegant" varFamilyType="1"> + <font postScriptName="NotoSansTelugu-Regular"> + NotoSansTelugu-VF.ttf + </font> + <font fallbackFor="serif" postScriptName="NotoSerifTelugu-Regular"> + NotoSerifTelugu-VF.ttf + </font> + </family> + <family lang="und-Telu" variant="compact" varFamilyType="1"> + <font postScriptName="NotoSansTeluguUI-Regular"> + NotoSansTeluguUI-VF.ttf + </font> + </family> + <family lang="und-Knda" variant="elegant" varFamilyType="1"> + <font postScriptName="NotoSansKannada-Regular"> + NotoSansKannada-VF.ttf + </font> + <font fallbackFor="serif" postScriptName="NotoSerifKannada-Regular"> + NotoSerifKannada-VF.ttf + </font> + </family> + <family lang="und-Knda" variant="compact" varFamilyType="1"> + <font postScriptName="NotoSansKannadaUI-Regular"> + NotoSansKannadaUI-VF.ttf + </font> + </family> + <family lang="und-Orya" variant="elegant"> + <font weight="400" style="normal" postScriptName="NotoSansOriya">NotoSansOriya-Regular.ttf + </font> + <font weight="700" style="normal">NotoSansOriya-Bold.ttf</font> + </family> + <family lang="und-Orya" variant="compact"> + <font weight="400" style="normal" postScriptName="NotoSansOriyaUI"> + NotoSansOriyaUI-Regular.ttf + </font> + <font weight="700" style="normal">NotoSansOriyaUI-Bold.ttf</font> + </family> + <family lang="und-Sinh" variant="elegant" varFamilyType="1"> + <font postScriptName="NotoSansSinhala-Regular"> + NotoSansSinhala-VF.ttf + </font> + <font fallbackFor="serif" postScriptName="NotoSerifSinhala-Regular"> + NotoSerifSinhala-VF.ttf + </font> + </family> + <family lang="und-Sinh" variant="compact" varFamilyType="1"> + <font postScriptName="NotoSansSinhalaUI-Regular"> + NotoSansSinhalaUI-VF.ttf + </font> + </family> + <family lang="und-Khmr" variant="elegant"> + <font weight="100" style="normal" postScriptName="NotoSansKhmer-Regular"> + NotoSansKhmer-VF.ttf + <axis tag="wdth" stylevalue="100.0"/> + <axis tag="wght" stylevalue="26.0"/> + </font> + <font weight="200" style="normal" postScriptName="NotoSansKhmer-Regular"> + NotoSansKhmer-VF.ttf + <axis tag="wdth" stylevalue="100.0"/> + <axis tag="wght" stylevalue="39.0"/> + </font> + <font weight="300" style="normal" postScriptName="NotoSansKhmer-Regular"> + NotoSansKhmer-VF.ttf + <axis tag="wdth" stylevalue="100.0"/> + <axis tag="wght" stylevalue="58.0"/> + </font> + <font weight="400" style="normal" postScriptName="NotoSansKhmer-Regular"> + NotoSansKhmer-VF.ttf + <axis tag="wdth" stylevalue="100.0"/> + <axis tag="wght" stylevalue="90.0"/> + </font> + <font weight="500" style="normal" postScriptName="NotoSansKhmer-Regular"> + NotoSansKhmer-VF.ttf + <axis tag="wdth" stylevalue="100.0"/> + <axis tag="wght" stylevalue="108.0"/> + </font> + <font weight="600" style="normal" postScriptName="NotoSansKhmer-Regular"> + NotoSansKhmer-VF.ttf + <axis tag="wdth" stylevalue="100.0"/> + <axis tag="wght" stylevalue="128.0"/> + </font> + <font weight="700" style="normal" postScriptName="NotoSansKhmer-Regular"> + NotoSansKhmer-VF.ttf + <axis tag="wdth" stylevalue="100.0"/> + <axis tag="wght" stylevalue="151.0"/> + </font> + <font weight="800" style="normal" postScriptName="NotoSansKhmer-Regular"> + NotoSansKhmer-VF.ttf + <axis tag="wdth" stylevalue="100.0"/> + <axis tag="wght" stylevalue="169.0"/> + </font> + <font weight="900" style="normal" postScriptName="NotoSansKhmer-Regular"> + NotoSansKhmer-VF.ttf + <axis tag="wdth" stylevalue="100.0"/> + <axis tag="wght" stylevalue="190.0"/> + </font> + <font weight="400" style="normal" fallbackFor="serif">NotoSerifKhmer-Regular.otf</font> + <font weight="700" style="normal" fallbackFor="serif">NotoSerifKhmer-Bold.otf</font> + </family> + <family lang="und-Khmr" variant="compact"> + <font weight="400" style="normal" postScriptName="NotoSansKhmerUI"> + NotoSansKhmerUI-Regular.ttf + </font> + <font weight="700" style="normal">NotoSansKhmerUI-Bold.ttf</font> + </family> + <family lang="und-Laoo" variant="elegant"> + <font weight="400" style="normal">NotoSansLao-Regular.ttf + </font> + <font weight="700" style="normal">NotoSansLao-Bold.ttf</font> + <font weight="400" style="normal" fallbackFor="serif"> + NotoSerifLao-Regular.ttf + </font> + <font weight="700" style="normal" fallbackFor="serif">NotoSerifLao-Bold.ttf</font> + </family> + <family lang="und-Laoo" variant="compact"> + <font weight="400" style="normal" postScriptName="NotoSansLaoUI">NotoSansLaoUI-Regular.ttf + </font> + <font weight="700" style="normal">NotoSansLaoUI-Bold.ttf</font> + </family> + <family lang="und-Mymr" variant="elegant"> + <font weight="400" style="normal">NotoSansMyanmar-Regular.otf</font> + <font weight="500" style="normal">NotoSansMyanmar-Medium.otf</font> + <font weight="700" style="normal">NotoSansMyanmar-Bold.otf</font> + <font weight="400" style="normal" fallbackFor="serif">NotoSerifMyanmar-Regular.otf</font> + <font weight="700" style="normal" fallbackFor="serif">NotoSerifMyanmar-Bold.otf</font> + </family> + <family lang="und-Mymr" variant="compact"> + <font weight="400" style="normal">NotoSansMyanmarUI-Regular.otf</font> + <font weight="500" style="normal">NotoSansMyanmarUI-Medium.otf</font> + <font weight="700" style="normal">NotoSansMyanmarUI-Bold.otf</font> + </family> + <family lang="und-Thaa"> + <font weight="400" style="normal" postScriptName="NotoSansThaana"> + NotoSansThaana-Regular.ttf + </font> + <font weight="700" style="normal">NotoSansThaana-Bold.ttf</font> + </family> + <family lang="und-Cham"> + <font weight="400" style="normal" postScriptName="NotoSansCham">NotoSansCham-Regular.ttf + </font> + <font weight="700" style="normal">NotoSansCham-Bold.ttf</font> + </family> + <family lang="und-Ahom"> + <font weight="400" style="normal">NotoSansAhom-Regular.otf</font> + </family> + <family lang="und-Adlm" varFamilyType="1"> + <font postScriptName="NotoSansAdlam-Regular"> + NotoSansAdlam-VF.ttf + </font> + </family> + <family lang="und-Avst"> + <font weight="400" style="normal" postScriptName="NotoSansAvestan"> + NotoSansAvestan-Regular.ttf + </font> + </family> + <family lang="und-Bali"> + <font weight="400" style="normal" postScriptName="NotoSansBalinese"> + NotoSansBalinese-Regular.ttf + </font> + </family> + <family lang="und-Bamu"> + <font weight="400" style="normal" postScriptName="NotoSansBamum">NotoSansBamum-Regular.ttf + </font> + </family> + <family lang="und-Batk"> + <font weight="400" style="normal" postScriptName="NotoSansBatak">NotoSansBatak-Regular.ttf + </font> + </family> + <family lang="und-Brah"> + <font weight="400" style="normal" postScriptName="NotoSansBrahmi"> + NotoSansBrahmi-Regular.ttf + </font> + </family> + <family lang="und-Bugi"> + <font weight="400" style="normal" postScriptName="NotoSansBuginese"> + NotoSansBuginese-Regular.ttf + </font> + </family> + <family lang="und-Buhd"> + <font weight="400" style="normal" postScriptName="NotoSansBuhid">NotoSansBuhid-Regular.ttf + </font> + </family> + <family lang="und-Cans"> + <font weight="400" style="normal"> + NotoSansCanadianAboriginal-Regular.ttf + </font> + </family> + <family lang="und-Cari"> + <font weight="400" style="normal" postScriptName="NotoSansCarian"> + NotoSansCarian-Regular.ttf + </font> + </family> + <family lang="und-Cakm"> + <font weight="400" style="normal">NotoSansChakma-Regular.otf</font> + </family> + <family lang="und-Cher"> + <font weight="400" style="normal">NotoSansCherokee-Regular.ttf</font> + </family> + <family lang="und-Copt"> + <font weight="400" style="normal" postScriptName="NotoSansCoptic"> + NotoSansCoptic-Regular.ttf + </font> + </family> + <family lang="und-Xsux"> + <font weight="400" style="normal" postScriptName="NotoSansCuneiform"> + NotoSansCuneiform-Regular.ttf + </font> + </family> + <family lang="und-Cprt"> + <font weight="400" style="normal" postScriptName="NotoSansCypriot"> + NotoSansCypriot-Regular.ttf + </font> + </family> + <family lang="und-Dsrt"> + <font weight="400" style="normal" postScriptName="NotoSansDeseret"> + NotoSansDeseret-Regular.ttf + </font> + </family> + <family lang="und-Egyp"> + <font weight="400" style="normal" postScriptName="NotoSansEgyptianHieroglyphs"> + NotoSansEgyptianHieroglyphs-Regular.ttf + </font> + </family> + <family lang="und-Elba"> + <font weight="400" style="normal">NotoSansElbasan-Regular.otf</font> + </family> + <family lang="und-Glag"> + <font weight="400" style="normal" postScriptName="NotoSansGlagolitic"> + NotoSansGlagolitic-Regular.ttf + </font> + </family> + <family lang="und-Goth"> + <font weight="400" style="normal" postScriptName="NotoSansGothic"> + NotoSansGothic-Regular.ttf + </font> + </family> + <family lang="und-Hano"> + <font weight="400" style="normal" postScriptName="NotoSansHanunoo"> + NotoSansHanunoo-Regular.ttf + </font> + </family> + <family lang="und-Armi"> + <font weight="400" style="normal" postScriptName="NotoSansImperialAramaic"> + NotoSansImperialAramaic-Regular.ttf + </font> + </family> + <family lang="und-Phli"> + <font weight="400" style="normal" postScriptName="NotoSansInscriptionalPahlavi"> + NotoSansInscriptionalPahlavi-Regular.ttf + </font> + </family> + <family lang="und-Prti"> + <font weight="400" style="normal" postScriptName="NotoSansInscriptionalParthian"> + NotoSansInscriptionalParthian-Regular.ttf + </font> + </family> + <family lang="und-Java"> + <font weight="400" style="normal">NotoSansJavanese-Regular.otf</font> + </family> + <family lang="und-Kthi"> + <font weight="400" style="normal" postScriptName="NotoSansKaithi"> + NotoSansKaithi-Regular.ttf + </font> + </family> + <family lang="und-Kali"> + <font weight="400" style="normal" postScriptName="NotoSansKayahLi"> + NotoSansKayahLi-Regular.ttf + </font> + </family> + <family lang="und-Khar"> + <font weight="400" style="normal" postScriptName="NotoSansKharoshthi"> + NotoSansKharoshthi-Regular.ttf + </font> + </family> + <family lang="und-Lepc"> + <font weight="400" style="normal" postScriptName="NotoSansLepcha"> + NotoSansLepcha-Regular.ttf + </font> + </family> + <family lang="und-Limb"> + <font weight="400" style="normal" postScriptName="NotoSansLimbu">NotoSansLimbu-Regular.ttf + </font> + </family> + <family lang="und-Linb"> + <font weight="400" style="normal" postScriptName="NotoSansLinearB"> + NotoSansLinearB-Regular.ttf + </font> + </family> + <family lang="und-Lisu"> + <font weight="400" style="normal" postScriptName="NotoSansLisu">NotoSansLisu-Regular.ttf + </font> + </family> + <family lang="und-Lyci"> + <font weight="400" style="normal" postScriptName="NotoSansLycian"> + NotoSansLycian-Regular.ttf + </font> + </family> + <family lang="und-Lydi"> + <font weight="400" style="normal" postScriptName="NotoSansLydian"> + NotoSansLydian-Regular.ttf + </font> + </family> + <family lang="und-Mand"> + <font weight="400" style="normal" postScriptName="NotoSansMandaic"> + NotoSansMandaic-Regular.ttf + </font> + </family> + <family lang="und-Mtei"> + <font weight="400" style="normal" postScriptName="NotoSansMeeteiMayek"> + NotoSansMeeteiMayek-Regular.ttf + </font> + </family> + <family lang="und-Talu"> + <font weight="400" style="normal" postScriptName="NotoSansNewTaiLue"> + NotoSansNewTaiLue-Regular.ttf + </font> + </family> + <family lang="und-Nkoo"> + <font weight="400" style="normal" postScriptName="NotoSansNKo">NotoSansNKo-Regular.ttf + </font> + </family> + <family lang="und-Ogam"> + <font weight="400" style="normal" postScriptName="NotoSansOgham">NotoSansOgham-Regular.ttf + </font> + </family> + <family lang="und-Olck"> + <font weight="400" style="normal" postScriptName="NotoSansOlChiki"> + NotoSansOlChiki-Regular.ttf + </font> + </family> + <family lang="und-Ital"> + <font weight="400" style="normal" postScriptName="NotoSansOldItalic"> + NotoSansOldItalic-Regular.ttf + </font> + </family> + <family lang="und-Xpeo"> + <font weight="400" style="normal" postScriptName="NotoSansOldPersian"> + NotoSansOldPersian-Regular.ttf + </font> + </family> + <family lang="und-Sarb"> + <font weight="400" style="normal" postScriptName="NotoSansOldSouthArabian"> + NotoSansOldSouthArabian-Regular.ttf + </font> + </family> + <family lang="und-Orkh"> + <font weight="400" style="normal" postScriptName="NotoSansOldTurkic"> + NotoSansOldTurkic-Regular.ttf + </font> + </family> + <family lang="und-Osge"> + <font weight="400" style="normal">NotoSansOsage-Regular.ttf</font> + </family> + <family lang="und-Osma"> + <font weight="400" style="normal" postScriptName="NotoSansOsmanya"> + NotoSansOsmanya-Regular.ttf + </font> + </family> + <family lang="und-Phnx"> + <font weight="400" style="normal" postScriptName="NotoSansPhoenician"> + NotoSansPhoenician-Regular.ttf + </font> + </family> + <family lang="und-Rjng"> + <font weight="400" style="normal" postScriptName="NotoSansRejang"> + NotoSansRejang-Regular.ttf + </font> + </family> + <family lang="und-Runr"> + <font weight="400" style="normal" postScriptName="NotoSansRunic">NotoSansRunic-Regular.ttf + </font> + </family> + <family lang="und-Samr"> + <font weight="400" style="normal" postScriptName="NotoSansSamaritan"> + NotoSansSamaritan-Regular.ttf + </font> + </family> + <family lang="und-Saur"> + <font weight="400" style="normal" postScriptName="NotoSansSaurashtra"> + NotoSansSaurashtra-Regular.ttf + </font> + </family> + <family lang="und-Shaw"> + <font weight="400" style="normal" postScriptName="NotoSansShavian"> + NotoSansShavian-Regular.ttf + </font> + </family> + <family lang="und-Sund"> + <font weight="400" style="normal" postScriptName="NotoSansSundanese"> + NotoSansSundanese-Regular.ttf + </font> + </family> + <family lang="und-Sylo"> + <font weight="400" style="normal" postScriptName="NotoSansSylotiNagri"> + NotoSansSylotiNagri-Regular.ttf + </font> + </family> + <!-- Esrangela should precede Eastern and Western Syriac, since it's our default form. --> + <family lang="und-Syre"> + <font weight="400" style="normal" postScriptName="NotoSansSyriacEstrangela"> + NotoSansSyriacEstrangela-Regular.ttf + </font> + </family> + <family lang="und-Syrn"> + <font weight="400" style="normal" postScriptName="NotoSansSyriacEastern"> + NotoSansSyriacEastern-Regular.ttf + </font> + </family> + <family lang="und-Syrj"> + <font weight="400" style="normal" postScriptName="NotoSansSyriacWestern"> + NotoSansSyriacWestern-Regular.ttf + </font> + </family> + <family lang="und-Tglg"> + <font weight="400" style="normal" postScriptName="NotoSansTagalog"> + NotoSansTagalog-Regular.ttf + </font> + </family> + <family lang="und-Tagb"> + <font weight="400" style="normal" postScriptName="NotoSansTagbanwa"> + NotoSansTagbanwa-Regular.ttf + </font> + </family> + <family lang="und-Lana"> + <font weight="400" style="normal" postScriptName="NotoSansTaiTham"> + NotoSansTaiTham-Regular.ttf + </font> + </family> + <family lang="und-Tavt"> + <font weight="400" style="normal" postScriptName="NotoSansTaiViet"> + NotoSansTaiViet-Regular.ttf + </font> + </family> + <family lang="und-Tibt" varFamilyType="1"> + <font postScriptName="NotoSerifTibetan-Regular"> + NotoSerifTibetan-VF.ttf + </font> + </family> + <family lang="und-Tfng"> + <font weight="400" style="normal">NotoSansTifinagh-Regular.otf</font> + </family> + <family lang="und-Ugar"> + <font weight="400" style="normal" postScriptName="NotoSansUgaritic"> + NotoSansUgaritic-Regular.ttf + </font> + </family> + <family lang="und-Vaii"> + <font weight="400" style="normal" postScriptName="NotoSansVai">NotoSansVai-Regular.ttf + </font> + </family> + <family> + <font weight="400" style="normal">NotoSansSymbols-Regular-Subsetted.ttf</font> + </family> + <family lang="zh-Hans"> + <font weight="100" style="normal" index="2" postScriptName="NotoSansCJKjp-Thin"> + NotoSansCJK-Regular.ttc + <axis tag="wght" stylevalue="100"/> + </font> + <font weight="200" style="normal" index="2" postScriptName="NotoSansCJKjp-Thin"> + NotoSansCJK-Regular.ttc + <axis tag="wght" stylevalue="200"/> + </font> + <font weight="300" style="normal" index="2" postScriptName="NotoSansCJKjp-Thin"> + NotoSansCJK-Regular.ttc + <axis tag="wght" stylevalue="300"/> + </font> + <font weight="400" style="normal" index="2" postScriptName="NotoSansCJKjp-Thin"> + NotoSansCJK-Regular.ttc + <axis tag="wght" stylevalue="400"/> + </font> + <font weight="500" style="normal" index="2" postScriptName="NotoSansCJKjp-Thin"> + NotoSansCJK-Regular.ttc + <axis tag="wght" stylevalue="500"/> + </font> + <font weight="600" style="normal" index="2" postScriptName="NotoSansCJKjp-Thin"> + NotoSansCJK-Regular.ttc + <axis tag="wght" stylevalue="600"/> + </font> + <font weight="700" style="normal" index="2" postScriptName="NotoSansCJKjp-Thin"> + NotoSansCJK-Regular.ttc + <axis tag="wght" stylevalue="700"/> + </font> + <font weight="800" style="normal" index="2" postScriptName="NotoSansCJKjp-Thin"> + NotoSansCJK-Regular.ttc + <axis tag="wght" stylevalue="800"/> + </font> + <font weight="900" style="normal" index="2" postScriptName="NotoSansCJKjp-Thin"> + NotoSansCJK-Regular.ttc + <axis tag="wght" stylevalue="900"/> + </font> + <font weight="400" style="normal" index="2" fallbackFor="serif" + postScriptName="NotoSerifCJKjp-Regular">NotoSerifCJK-Regular.ttc + </font> + </family> + <family lang="zh-Hant,zh-Bopo"> + <font weight="100" style="normal" index="3" postScriptName="NotoSansCJKjp-Thin"> + NotoSansCJK-Regular.ttc + <axis tag="wght" stylevalue="100"/> + </font> + <font weight="200" style="normal" index="3" postScriptName="NotoSansCJKjp-Thin"> + NotoSansCJK-Regular.ttc + <axis tag="wght" stylevalue="200"/> + </font> + <font weight="300" style="normal" index="3" postScriptName="NotoSansCJKjp-Thin"> + NotoSansCJK-Regular.ttc + <axis tag="wght" stylevalue="300"/> + </font> + <font weight="400" style="normal" index="3" postScriptName="NotoSansCJKjp-Thin"> + NotoSansCJK-Regular.ttc + <axis tag="wght" stylevalue="400"/> + </font> + <font weight="500" style="normal" index="3" postScriptName="NotoSansCJKjp-Thin"> + NotoSansCJK-Regular.ttc + <axis tag="wght" stylevalue="500"/> + </font> + <font weight="600" style="normal" index="3" postScriptName="NotoSansCJKjp-Thin"> + NotoSansCJK-Regular.ttc + <axis tag="wght" stylevalue="600"/> + </font> + <font weight="700" style="normal" index="3" postScriptName="NotoSansCJKjp-Thin"> + NotoSansCJK-Regular.ttc + <axis tag="wght" stylevalue="700"/> + </font> + <font weight="800" style="normal" index="3" postScriptName="NotoSansCJKjp-Thin"> + NotoSansCJK-Regular.ttc + <axis tag="wght" stylevalue="800"/> + </font> + <font weight="900" style="normal" index="3" postScriptName="NotoSansCJKjp-Thin"> + NotoSansCJK-Regular.ttc + <axis tag="wght" stylevalue="900"/> + </font> + <font weight="400" style="normal" index="3" fallbackFor="serif" + postScriptName="NotoSerifCJKjp-Regular">NotoSerifCJK-Regular.ttc + </font> + </family> + <family lang="ja"> + <font weight="100" style="normal" index="0" postScriptName="NotoSansCJKjp-Thin"> + NotoSansCJK-Regular.ttc + <axis tag="wght" stylevalue="100"/> + </font> + <font weight="200" style="normal" index="0" postScriptName="NotoSansCJKjp-Thin"> + NotoSansCJK-Regular.ttc + <axis tag="wght" stylevalue="200"/> + </font> + <font weight="300" style="normal" index="0" postScriptName="NotoSansCJKjp-Thin"> + NotoSansCJK-Regular.ttc + <axis tag="wght" stylevalue="300"/> + </font> + <font weight="400" style="normal" index="0" postScriptName="NotoSansCJKjp-Thin"> + NotoSansCJK-Regular.ttc + <axis tag="wght" stylevalue="400"/> + </font> + <font weight="500" style="normal" index="0" postScriptName="NotoSansCJKjp-Thin"> + NotoSansCJK-Regular.ttc + <axis tag="wght" stylevalue="500"/> + </font> + <font weight="600" style="normal" index="0" postScriptName="NotoSansCJKjp-Thin"> + NotoSansCJK-Regular.ttc + <axis tag="wght" stylevalue="600"/> + </font> + <font weight="700" style="normal" index="0" postScriptName="NotoSansCJKjp-Thin"> + NotoSansCJK-Regular.ttc + <axis tag="wght" stylevalue="700"/> + </font> + <font weight="800" style="normal" index="0" postScriptName="NotoSansCJKjp-Thin"> + NotoSansCJK-Regular.ttc + <axis tag="wght" stylevalue="800"/> + </font> + <font weight="900" style="normal" index="0" postScriptName="NotoSansCJKjp-Thin"> + NotoSansCJK-Regular.ttc + <axis tag="wght" stylevalue="900"/> + </font> + <font weight="400" style="normal" index="0" fallbackFor="serif" + postScriptName="NotoSerifCJKjp-Regular">NotoSerifCJK-Regular.ttc + </font> + </family> + <family lang="ko"> + <font weight="100" style="normal" index="1" postScriptName="NotoSansCJKjp-Thin"> + NotoSansCJK-Regular.ttc + <axis tag="wght" stylevalue="100"/> + </font> + <font weight="200" style="normal" index="1" postScriptName="NotoSansCJKjp-Thin"> + NotoSansCJK-Regular.ttc + <axis tag="wght" stylevalue="200"/> + </font> + <font weight="300" style="normal" index="1" postScriptName="NotoSansCJKjp-Thin"> + NotoSansCJK-Regular.ttc + <axis tag="wght" stylevalue="300"/> + </font> + <font weight="400" style="normal" index="1" postScriptName="NotoSansCJKjp-Thin"> + NotoSansCJK-Regular.ttc + <axis tag="wght" stylevalue="400"/> + </font> + <font weight="500" style="normal" index="1" postScriptName="NotoSansCJKjp-Thin"> + NotoSansCJK-Regular.ttc + <axis tag="wght" stylevalue="500"/> + </font> + <font weight="600" style="normal" index="1" postScriptName="NotoSansCJKjp-Thin"> + NotoSansCJK-Regular.ttc + <axis tag="wght" stylevalue="600"/> + </font> + <font weight="700" style="normal" index="1" postScriptName="NotoSansCJKjp-Thin"> + NotoSansCJK-Regular.ttc + <axis tag="wght" stylevalue="700"/> + </font> + <font weight="800" style="normal" index="1" postScriptName="NotoSansCJKjp-Thin"> + NotoSansCJK-Regular.ttc + <axis tag="wght" stylevalue="800"/> + </font> + <font weight="900" style="normal" index="1" postScriptName="NotoSansCJKjp-Thin"> + NotoSansCJK-Regular.ttc + <axis tag="wght" stylevalue="900"/> + </font> + <font weight="400" style="normal" index="1" fallbackFor="serif" + postScriptName="NotoSerifCJKjp-Regular">NotoSerifCJK-Regular.ttc + </font> + </family> + <family lang="und-Zsye"> + <font weight="400" style="normal">NotoColorEmoji.ttf</font> + </family> + <family lang="und-Zsye"> + <font weight="400" style="normal">NotoColorEmojiFlags.ttf</font> + </family> + <family lang="und-Zsym"> + <font weight="400" style="normal">NotoSansSymbols-Regular-Subsetted2.ttf</font> + </family> + <!-- + Tai Le, Yi, Mongolian, and Phags-pa are intentionally kept last, to make sure they don't + override the East Asian punctuation for Chinese. + --> + <family lang="und-Tale"> + <font weight="400" style="normal" postScriptName="NotoSansTaiLe">NotoSansTaiLe-Regular.ttf + </font> + </family> + <family lang="und-Yiii"> + <font weight="400" style="normal" postScriptName="NotoSansYi">NotoSansYi-Regular.ttf</font> + </family> + <family lang="und-Mong"> + <font weight="400" style="normal" postScriptName="NotoSansMongolian"> + NotoSansMongolian-Regular.ttf + </font> + </family> + <family lang="und-Phag"> + <font weight="400" style="normal" postScriptName="NotoSansPhagsPa"> + NotoSansPhagsPa-Regular.ttf + </font> + </family> + <family lang="und-Hluw"> + <font weight="400" style="normal">NotoSansAnatolianHieroglyphs-Regular.otf</font> + </family> + <family lang="und-Bass"> + <font weight="400" style="normal">NotoSansBassaVah-Regular.otf</font> + </family> + <family lang="und-Bhks"> + <font weight="400" style="normal">NotoSansBhaiksuki-Regular.otf</font> + </family> + <family lang="und-Hatr"> + <font weight="400" style="normal">NotoSansHatran-Regular.otf</font> + </family> + <family lang="und-Lina"> + <font weight="400" style="normal">NotoSansLinearA-Regular.otf</font> + </family> + <family lang="und-Mani"> + <font weight="400" style="normal">NotoSansManichaean-Regular.otf</font> + </family> + <family lang="und-Marc"> + <font weight="400" style="normal">NotoSansMarchen-Regular.otf</font> + </family> + <family lang="und-Merc"> + <font weight="400" style="normal">NotoSansMeroitic-Regular.otf</font> + </family> + <family lang="und-Plrd"> + <font weight="400" style="normal">NotoSansMiao-Regular.otf</font> + </family> + <family lang="und-Mroo"> + <font weight="400" style="normal">NotoSansMro-Regular.otf</font> + </family> + <family lang="und-Mult"> + <font weight="400" style="normal">NotoSansMultani-Regular.otf</font> + </family> + <family lang="und-Nbat"> + <font weight="400" style="normal">NotoSansNabataean-Regular.otf</font> + </family> + <family lang="und-Newa"> + <font weight="400" style="normal">NotoSansNewa-Regular.otf</font> + </family> + <family lang="und-Narb"> + <font weight="400" style="normal">NotoSansOldNorthArabian-Regular.otf</font> + </family> + <family lang="und-Perm"> + <font weight="400" style="normal">NotoSansOldPermic-Regular.otf</font> + </family> + <family lang="und-Hmng"> + <font weight="400" style="normal">NotoSansPahawhHmong-Regular.otf</font> + </family> + <family lang="und-Palm"> + <font weight="400" style="normal">NotoSansPalmyrene-Regular.otf</font> + </family> + <family lang="und-Pauc"> + <font weight="400" style="normal">NotoSansPauCinHau-Regular.otf</font> + </family> + <family lang="und-Shrd"> + <font weight="400" style="normal">NotoSansSharada-Regular.otf</font> + </family> + <family lang="und-Sora"> + <font weight="400" style="normal">NotoSansSoraSompeng-Regular.otf</font> + </family> + <family lang="und-Gong"> + <font weight="400" style="normal">NotoSansGunjalaGondi-Regular.otf</font> + </family> + <family lang="und-Rohg"> + <font weight="400" style="normal">NotoSansHanifiRohingya-Regular.otf</font> + </family> + <family lang="und-Khoj"> + <font weight="400" style="normal">NotoSansKhojki-Regular.otf</font> + </family> + <family lang="und-Gonm"> + <font weight="400" style="normal">NotoSansMasaramGondi-Regular.otf</font> + </family> + <family lang="und-Wcho"> + <font weight="400" style="normal">NotoSansWancho-Regular.otf</font> + </family> + <family lang="und-Wara"> + <font weight="400" style="normal">NotoSansWarangCiti-Regular.otf</font> + </family> + <family lang="und-Gran"> + <font weight="400" style="normal">NotoSansGrantha-Regular.ttf</font> + </family> + <family lang="und-Modi"> + <font weight="400" style="normal">NotoSansModi-Regular.ttf</font> + </family> + <family lang="und-Dogr"> + <font weight="400" style="normal">NotoSerifDogra-Regular.ttf</font> + </family> + <family lang="und-Medf" varFamilyType="1"> + <font postScriptName="NotoSansMedefaidrin-Regular"> + NotoSansMedefaidrin-VF.ttf + </font> + </family> + <family lang="und-Soyo" varFamilyType="1"> + <font postScriptName="NotoSansSoyombo-Regular"> + NotoSansSoyombo-VF.ttf + </font> + </family> + <family lang="und-Takr" varFamilyType="1"> + <font postScriptName="NotoSansTakri-Regular"> + NotoSansTakri-VF.ttf + </font> + </family> + <family lang="und-Hmnp" varFamilyType="1"> + <font postScriptName="NotoSerifHmongNyiakeng-Regular"> + NotoSerifNyiakengPuachueHmong-VF.ttf + </font> + </family> + <family lang="und-Yezi" varFamilyType="1"> + <font postScriptName="NotoSerifYezidi-Regular"> + NotoSerifYezidi-VF.ttf + </font> + </family> +</familyset> diff --git a/data/fonts/fonts_cjkvf.xml b/data/fonts/fonts_cjkvf.xml new file mode 100644 index 000000000000..8d91eddd807d --- /dev/null +++ b/data/fonts/fonts_cjkvf.xml @@ -0,0 +1,1785 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + DEPRECATED: This XML file is no longer a source of the font files installed + in the system. + + For the device vendors: please add your font configurations to the + platform/frameworks/base/data/font_fallback.xml and also add it to this XML + file as much as possible for apps that reads this XML file. + + For the application developers: please stop reading this XML file and use + android.graphics.fonts.SystemFonts#getAvailableFonts Java API or + ASystemFontIterator_open NDK API for getting list of system installed + font files. + + WARNING: Parsing of this file by third-party apps is not supported. The + file, and the font files it refers to, will be renamed and/or moved out + from their respective location in the next Android release, and/or the + format or syntax of the file may change significantly. If you parse this + file for information about system fonts, do it at your own risk. Your + application will almost certainly break with the next major Android + release. + + In this file, all fonts without names are added to the default list. + Fonts are chosen based on a match: full BCP-47 language tag including + script, then just language, and finally order (the first font containing + the glyph). + + Order of appearance is also the tiebreaker for weight matching. This is + the reason why the 900 weights of Roboto precede the 700 weights - we + prefer the former when an 800 weight is requested. Since bold spans + effectively add 300 to the weight, this ensures that 900 is the bold + paired with the 500 weight, ensuring adequate contrast. + + TODO(rsheeter) update comment; ordering to match 800 to 900 is no longer required +--> +<familyset version="23"> + <!-- first font is default --> + <family name="sans-serif"> + <font weight="100" style="normal">Roboto-Regular.ttf + <axis tag="ital" stylevalue="0" /> + <axis tag="wdth" stylevalue="100" /> + <axis tag="wght" stylevalue="100" /> + </font> + <font weight="200" style="normal">Roboto-Regular.ttf + <axis tag="ital" stylevalue="0" /> + <axis tag="wdth" stylevalue="100" /> + <axis tag="wght" stylevalue="200" /> + </font> + <font weight="300" style="normal">Roboto-Regular.ttf + <axis tag="ital" stylevalue="0" /> + <axis tag="wdth" stylevalue="100" /> + <axis tag="wght" stylevalue="300" /> + </font> + <font weight="400" style="normal">Roboto-Regular.ttf + <axis tag="ital" stylevalue="0" /> + <axis tag="wdth" stylevalue="100" /> + <axis tag="wght" stylevalue="400" /> + </font> + <font weight="500" style="normal">Roboto-Regular.ttf + <axis tag="ital" stylevalue="0" /> + <axis tag="wdth" stylevalue="100" /> + <axis tag="wght" stylevalue="500" /> + </font> + <font weight="600" style="normal">Roboto-Regular.ttf + <axis tag="ital" stylevalue="0" /> + <axis tag="wdth" stylevalue="100" /> + <axis tag="wght" stylevalue="600" /> + </font> + <font weight="700" style="normal">Roboto-Regular.ttf + <axis tag="ital" stylevalue="0" /> + <axis tag="wdth" stylevalue="100" /> + <axis tag="wght" stylevalue="700" /> + </font> + <font weight="800" style="normal">Roboto-Regular.ttf + <axis tag="ital" stylevalue="0" /> + <axis tag="wdth" stylevalue="100" /> + <axis tag="wght" stylevalue="800" /> + </font> + <font weight="900" style="normal">Roboto-Regular.ttf + <axis tag="ital" stylevalue="0" /> + <axis tag="wdth" stylevalue="100" /> + <axis tag="wght" stylevalue="900" /> + </font> + <font weight="100" style="italic">Roboto-Regular.ttf + <axis tag="ital" stylevalue="1" /> + <axis tag="wdth" stylevalue="100" /> + <axis tag="wght" stylevalue="100" /> + </font> + <font weight="200" style="italic">Roboto-Regular.ttf + <axis tag="ital" stylevalue="1" /> + <axis tag="wdth" stylevalue="100" /> + <axis tag="wght" stylevalue="200" /> + </font> + <font weight="300" style="italic">Roboto-Regular.ttf + <axis tag="ital" stylevalue="1" /> + <axis tag="wdth" stylevalue="100" /> + <axis tag="wght" stylevalue="300" /> + </font> + <font weight="400" style="italic">Roboto-Regular.ttf + <axis tag="ital" stylevalue="1" /> + <axis tag="wdth" stylevalue="100" /> + <axis tag="wght" stylevalue="400" /> + </font> + <font weight="500" style="italic">Roboto-Regular.ttf + <axis tag="ital" stylevalue="1" /> + <axis tag="wdth" stylevalue="100" /> + <axis tag="wght" stylevalue="500" /> + </font> + <font weight="600" style="italic">Roboto-Regular.ttf + <axis tag="ital" stylevalue="1" /> + <axis tag="wdth" stylevalue="100" /> + <axis tag="wght" stylevalue="600" /> + </font> + <font weight="700" style="italic">Roboto-Regular.ttf + <axis tag="ital" stylevalue="1" /> + <axis tag="wdth" stylevalue="100" /> + <axis tag="wght" stylevalue="700" /> + </font> + <font weight="800" style="italic">Roboto-Regular.ttf + <axis tag="ital" stylevalue="1" /> + <axis tag="wdth" stylevalue="100" /> + <axis tag="wght" stylevalue="800" /> + </font> + <font weight="900" style="italic">Roboto-Regular.ttf + <axis tag="ital" stylevalue="1" /> + <axis tag="wdth" stylevalue="100" /> + <axis tag="wght" stylevalue="900" /> + </font> + </family> + + + <!-- Note that aliases must come after the fonts they reference. --> + <alias name="sans-serif-thin" to="sans-serif" weight="100" /> + <alias name="sans-serif-light" to="sans-serif" weight="300" /> + <alias name="sans-serif-medium" to="sans-serif" weight="500" /> + <alias name="sans-serif-black" to="sans-serif" weight="900" /> + <alias name="arial" to="sans-serif" /> + <alias name="helvetica" to="sans-serif" /> + <alias name="tahoma" to="sans-serif" /> + <alias name="verdana" to="sans-serif" /> + + <family name="sans-serif-condensed"> + <font weight="100" style="normal">Roboto-Regular.ttf + <axis tag="ital" stylevalue="0" /> + <axis tag="wdth" stylevalue="75" /> + <axis tag="wght" stylevalue="100" /> + </font> + <font weight="200" style="normal">Roboto-Regular.ttf + <axis tag="ital" stylevalue="0" /> + <axis tag="wdth" stylevalue="75" /> + <axis tag="wght" stylevalue="200" /> + </font> + <font weight="300" style="normal">Roboto-Regular.ttf + <axis tag="ital" stylevalue="0" /> + <axis tag="wdth" stylevalue="75" /> + <axis tag="wght" stylevalue="300" /> + </font> + <font weight="400" style="normal">Roboto-Regular.ttf + <axis tag="ital" stylevalue="0" /> + <axis tag="wdth" stylevalue="75" /> + <axis tag="wght" stylevalue="400" /> + </font> + <font weight="500" style="normal">Roboto-Regular.ttf + <axis tag="ital" stylevalue="0" /> + <axis tag="wdth" stylevalue="75" /> + <axis tag="wght" stylevalue="500" /> + </font> + <font weight="600" style="normal">Roboto-Regular.ttf + <axis tag="ital" stylevalue="0" /> + <axis tag="wdth" stylevalue="75" /> + <axis tag="wght" stylevalue="600" /> + </font> + <font weight="700" style="normal">Roboto-Regular.ttf + <axis tag="ital" stylevalue="0" /> + <axis tag="wdth" stylevalue="75" /> + <axis tag="wght" stylevalue="700" /> + </font> + <font weight="800" style="normal">Roboto-Regular.ttf + <axis tag="ital" stylevalue="0" /> + <axis tag="wdth" stylevalue="75" /> + <axis tag="wght" stylevalue="800" /> + </font> + <font weight="900" style="normal">Roboto-Regular.ttf + <axis tag="ital" stylevalue="0" /> + <axis tag="wdth" stylevalue="75" /> + <axis tag="wght" stylevalue="900" /> + </font> + <font weight="100" style="italic">Roboto-Regular.ttf + <axis tag="ital" stylevalue="1" /> + <axis tag="wdth" stylevalue="75" /> + <axis tag="wght" stylevalue="100" /> + </font> + <font weight="200" style="italic">Roboto-Regular.ttf + <axis tag="ital" stylevalue="1" /> + <axis tag="wdth" stylevalue="75" /> + <axis tag="wght" stylevalue="200" /> + </font> + <font weight="300" style="italic">Roboto-Regular.ttf + <axis tag="ital" stylevalue="1" /> + <axis tag="wdth" stylevalue="75" /> + <axis tag="wght" stylevalue="300" /> + </font> + <font weight="400" style="italic">Roboto-Regular.ttf + <axis tag="ital" stylevalue="1" /> + <axis tag="wdth" stylevalue="75" /> + <axis tag="wght" stylevalue="400" /> + </font> + <font weight="500" style="italic">Roboto-Regular.ttf + <axis tag="ital" stylevalue="1" /> + <axis tag="wdth" stylevalue="75" /> + <axis tag="wght" stylevalue="500" /> + </font> + <font weight="600" style="italic">Roboto-Regular.ttf + <axis tag="ital" stylevalue="1" /> + <axis tag="wdth" stylevalue="75" /> + <axis tag="wght" stylevalue="600" /> + </font> + <font weight="700" style="italic">Roboto-Regular.ttf + <axis tag="ital" stylevalue="1" /> + <axis tag="wdth" stylevalue="75" /> + <axis tag="wght" stylevalue="700" /> + </font> + <font weight="800" style="italic">Roboto-Regular.ttf + <axis tag="ital" stylevalue="1" /> + <axis tag="wdth" stylevalue="75" /> + <axis tag="wght" stylevalue="800" /> + </font> + <font weight="900" style="italic">Roboto-Regular.ttf + <axis tag="ital" stylevalue="1" /> + <axis tag="wdth" stylevalue="75" /> + <axis tag="wght" stylevalue="900" /> + </font> + </family> + <alias name="sans-serif-condensed-light" to="sans-serif-condensed" weight="300" /> + <alias name="sans-serif-condensed-medium" to="sans-serif-condensed" weight="500" /> + + <family name="serif"> + <font weight="400" style="normal" postScriptName="NotoSerif">NotoSerif-Regular.ttf</font> + <font weight="700" style="normal">NotoSerif-Bold.ttf</font> + <font weight="400" style="italic">NotoSerif-Italic.ttf</font> + <font weight="700" style="italic">NotoSerif-BoldItalic.ttf</font> + </family> + <alias name="serif-bold" to="serif" weight="700" /> + <alias name="times" to="serif" /> + <alias name="times new roman" to="serif" /> + <alias name="palatino" to="serif" /> + <alias name="georgia" to="serif" /> + <alias name="baskerville" to="serif" /> + <alias name="goudy" to="serif" /> + <alias name="fantasy" to="serif" /> + <alias name="ITC Stone Serif" to="serif" /> + + <family name="monospace"> + <font weight="400" style="normal">DroidSansMono.ttf</font> + </family> + <alias name="sans-serif-monospace" to="monospace" /> + <alias name="monaco" to="monospace" /> + + <family name="serif-monospace"> + <font weight="400" style="normal" postScriptName="CutiveMono-Regular">CutiveMono.ttf</font> + </family> + <alias name="courier" to="serif-monospace" /> + <alias name="courier new" to="serif-monospace" /> + + <family name="casual"> + <font weight="400" style="normal" postScriptName="ComingSoon-Regular">ComingSoon.ttf</font> + </family> + + <family name="cursive"> + <font weight="400" style="normal">DancingScript-Regular.ttf + <axis tag="wght" stylevalue="400" /> + </font> + <font weight="700" style="normal">DancingScript-Regular.ttf + <axis tag="wght" stylevalue="700" /> + </font> + </family> + + <family name="sans-serif-smallcaps"> + <font weight="400" style="normal">CarroisGothicSC-Regular.ttf</font> + </family> + + <family name="source-sans-pro"> + <font weight="400" style="normal">SourceSansPro-Regular.ttf</font> + <font weight="400" style="italic">SourceSansPro-Italic.ttf</font> + <font weight="600" style="normal">SourceSansPro-SemiBold.ttf</font> + <font weight="600" style="italic">SourceSansPro-SemiBoldItalic.ttf</font> + <font weight="700" style="normal">SourceSansPro-Bold.ttf</font> + <font weight="700" style="italic">SourceSansPro-BoldItalic.ttf</font> + </family> + <alias name="source-sans-pro-semi-bold" to="source-sans-pro" weight="600"/> + + <family name="roboto-flex"> + <font weight="100" style="normal">RobotoFlex-Regular.ttf + <axis tag="slnt" stylevalue="0" /> + <axis tag="wdth" stylevalue="100" /> + <axis tag="wght" stylevalue="100" /> + </font> + <font weight="200" style="normal">RobotoFlex-Regular.ttf + <axis tag="slnt" stylevalue="0" /> + <axis tag="wdth" stylevalue="100" /> + <axis tag="wght" stylevalue="200" /> + </font> + <font weight="300" style="normal">RobotoFlex-Regular.ttf + <axis tag="slnt" stylevalue="0" /> + <axis tag="wdth" stylevalue="100" /> + <axis tag="wght" stylevalue="300" /> + </font> + <font weight="400" style="normal">RobotoFlex-Regular.ttf + <axis tag="slnt" stylevalue="0" /> + <axis tag="wdth" stylevalue="100" /> + <axis tag="wght" stylevalue="400" /> + </font> + <font weight="500" style="normal">RobotoFlex-Regular.ttf + <axis tag="slnt" stylevalue="0" /> + <axis tag="wdth" stylevalue="100" /> + <axis tag="wght" stylevalue="500" /> + </font> + <font weight="600" style="normal">RobotoFlex-Regular.ttf + <axis tag="slnt" stylevalue="0" /> + <axis tag="wdth" stylevalue="100" /> + <axis tag="wght" stylevalue="600" /> + </font> + <font weight="700" style="normal">RobotoFlex-Regular.ttf + <axis tag="slnt" stylevalue="0" /> + <axis tag="wdth" stylevalue="100" /> + <axis tag="wght" stylevalue="700" /> + </font> + <font weight="800" style="normal">RobotoFlex-Regular.ttf + <axis tag="slnt" stylevalue="0" /> + <axis tag="wdth" stylevalue="100" /> + <axis tag="wght" stylevalue="800" /> + </font> + <font weight="900" style="normal">RobotoFlex-Regular.ttf + <axis tag="slnt" stylevalue="0" /> + <axis tag="wdth" stylevalue="100" /> + <axis tag="wght" stylevalue="900" /> + </font> + <font weight="100" style="italic">RobotoFlex-Regular.ttf + <axis tag="slnt" stylevalue="-10" /> + <axis tag="wdth" stylevalue="100" /> + <axis tag="wght" stylevalue="100" /> + </font> + <font weight="200" style="italic">RobotoFlex-Regular.ttf + <axis tag="slnt" stylevalue="-10" /> + <axis tag="wdth" stylevalue="100" /> + <axis tag="wght" stylevalue="200" /> + </font> + <font weight="300" style="italic">RobotoFlex-Regular.ttf + <axis tag="slnt" stylevalue="-10" /> + <axis tag="wdth" stylevalue="100" /> + <axis tag="wght" stylevalue="300" /> + </font> + <font weight="400" style="italic">RobotoFlex-Regular.ttf + <axis tag="slnt" stylevalue="-10" /> + <axis tag="wdth" stylevalue="100" /> + <axis tag="wght" stylevalue="400" /> + </font> + <font weight="500" style="italic">RobotoFlex-Regular.ttf + <axis tag="slnt" stylevalue="-10" /> + <axis tag="wdth" stylevalue="100" /> + <axis tag="wght" stylevalue="500" /> + </font> + <font weight="600" style="italic">RobotoFlex-Regular.ttf + <axis tag="slnt" stylevalue="-10" /> + <axis tag="wdth" stylevalue="100" /> + <axis tag="wght" stylevalue="600" /> + </font> + <font weight="700" style="italic">RobotoFlex-Regular.ttf + <axis tag="slnt" stylevalue="-10" /> + <axis tag="wdth" stylevalue="100" /> + <axis tag="wght" stylevalue="700" /> + </font> + <font weight="800" style="italic">RobotoFlex-Regular.ttf + <axis tag="slnt" stylevalue="-10" /> + <axis tag="wdth" stylevalue="100" /> + <axis tag="wght" stylevalue="800" /> + </font> + <font weight="900" style="italic">RobotoFlex-Regular.ttf + <axis tag="slnt" stylevalue="-10" /> + <axis tag="wdth" stylevalue="100" /> + <axis tag="wght" stylevalue="900" /> + </font> + </family> + + <!-- fallback fonts --> + <family lang="und-Arab" variant="elegant"> + <font weight="400" style="normal" postScriptName="NotoNaskhArabic"> + NotoNaskhArabic-Regular.ttf + </font> + <font weight="700" style="normal">NotoNaskhArabic-Bold.ttf</font> + </family> + <family lang="und-Arab" variant="compact"> + <font weight="400" style="normal" postScriptName="NotoNaskhArabicUI"> + NotoNaskhArabicUI-Regular.ttf + </font> + <font weight="700" style="normal">NotoNaskhArabicUI-Bold.ttf</font> + </family> + <family lang="und-Ethi"> + <font weight="400" style="normal" postScriptName="NotoSansEthiopic-Regular"> + NotoSansEthiopic-VF.ttf + <axis tag="wght" stylevalue="400"/> + </font> + <font weight="500" style="normal" postScriptName="NotoSansEthiopic-Regular"> + NotoSansEthiopic-VF.ttf + <axis tag="wght" stylevalue="500"/> + </font> + <font weight="600" style="normal" postScriptName="NotoSansEthiopic-Regular"> + NotoSansEthiopic-VF.ttf + <axis tag="wght" stylevalue="600"/> + </font> + <font weight="700" style="normal" postScriptName="NotoSansEthiopic-Regular"> + NotoSansEthiopic-VF.ttf + <axis tag="wght" stylevalue="700"/> + </font> + <font weight="400" style="normal" fallbackFor="serif" + postScriptName="NotoSerifEthiopic-Regular">NotoSerifEthiopic-VF.ttf + <axis tag="wght" stylevalue="400"/> + </font> + <font weight="500" style="normal" fallbackFor="serif" + postScriptName="NotoSerifEthiopic-Regular">NotoSerifEthiopic-VF.ttf + <axis tag="wght" stylevalue="500"/> + </font> + <font weight="600" style="normal" fallbackFor="serif" + postScriptName="NotoSerifEthiopic-Regular">NotoSerifEthiopic-VF.ttf + <axis tag="wght" stylevalue="600"/> + </font> + <font weight="700" style="normal" fallbackFor="serif" + postScriptName="NotoSerifEthiopic-Regular">NotoSerifEthiopic-VF.ttf + <axis tag="wght" stylevalue="700"/> + </font> + </family> + <family lang="und-Hebr"> + <font weight="400" style="normal" postScriptName="NotoSansHebrew"> + NotoSansHebrew-Regular.ttf + </font> + <font weight="700" style="normal">NotoSansHebrew-Bold.ttf</font> + <font weight="400" style="normal" fallbackFor="serif">NotoSerifHebrew-Regular.ttf</font> + <font weight="700" style="normal" fallbackFor="serif">NotoSerifHebrew-Bold.ttf</font> + </family> + <family lang="und-Thai" variant="elegant"> + <font weight="400" style="normal" postScriptName="NotoSansThai">NotoSansThai-Regular.ttf + </font> + <font weight="700" style="normal">NotoSansThai-Bold.ttf</font> + <font weight="400" style="normal" fallbackFor="serif"> + NotoSerifThai-Regular.ttf + </font> + <font weight="700" style="normal" fallbackFor="serif">NotoSerifThai-Bold.ttf</font> + </family> + <family lang="und-Thai" variant="compact"> + <font weight="400" style="normal" postScriptName="NotoSansThaiUI"> + NotoSansThaiUI-Regular.ttf + </font> + <font weight="700" style="normal">NotoSansThaiUI-Bold.ttf</font> + </family> + <family lang="und-Armn"> + <font weight="400" style="normal" postScriptName="NotoSansArmenian-Regular"> + NotoSansArmenian-VF.ttf + <axis tag="wght" stylevalue="400"/> + </font> + <font weight="500" style="normal" postScriptName="NotoSansArmenian-Regular"> + NotoSansArmenian-VF.ttf + <axis tag="wght" stylevalue="500"/> + </font> + <font weight="600" style="normal" postScriptName="NotoSansArmenian-Regular"> + NotoSansArmenian-VF.ttf + <axis tag="wght" stylevalue="600"/> + </font> + <font weight="700" style="normal" postScriptName="NotoSansArmenian-Regular"> + NotoSansArmenian-VF.ttf + <axis tag="wght" stylevalue="700"/> + </font> + <font weight="400" style="normal" fallbackFor="serif" + postScriptName="NotoSerifArmenian-Regular">NotoSerifArmenian-VF.ttf + <axis tag="wght" stylevalue="400"/> + </font> + <font weight="500" style="normal" fallbackFor="serif" + postScriptName="NotoSerifArmenian-Regular">NotoSerifArmenian-VF.ttf + <axis tag="wght" stylevalue="500"/> + </font> + <font weight="600" style="normal" fallbackFor="serif" + postScriptName="NotoSerifArmenian-Regular">NotoSerifArmenian-VF.ttf + <axis tag="wght" stylevalue="600"/> + </font> + <font weight="700" style="normal" fallbackFor="serif" + postScriptName="NotoSerifArmenian-Regular">NotoSerifArmenian-VF.ttf + <axis tag="wght" stylevalue="700"/> + </font> + </family> + <family lang="und-Geor,und-Geok"> + <font weight="400" style="normal" postScriptName="NotoSansGeorgian-Regular"> + NotoSansGeorgian-VF.ttf + <axis tag="wght" stylevalue="400"/> + </font> + <font weight="500" style="normal" postScriptName="NotoSansGeorgian-Regular"> + NotoSansGeorgian-VF.ttf + <axis tag="wght" stylevalue="500"/> + </font> + <font weight="600" style="normal" postScriptName="NotoSansGeorgian-Regular"> + NotoSansGeorgian-VF.ttf + <axis tag="wght" stylevalue="600"/> + </font> + <font weight="700" style="normal" postScriptName="NotoSansGeorgian-Regular"> + NotoSansGeorgian-VF.ttf + <axis tag="wght" stylevalue="700"/> + </font> + <font weight="400" style="normal" fallbackFor="serif" + postScriptName="NotoSerifGeorgian-Regular">NotoSerifGeorgian-VF.ttf + <axis tag="wght" stylevalue="400"/> + </font> + <font weight="500" style="normal" fallbackFor="serif" + postScriptName="NotoSerifGeorgian-Regular">NotoSerifGeorgian-VF.ttf + <axis tag="wght" stylevalue="500"/> + </font> + <font weight="600" style="normal" fallbackFor="serif" + postScriptName="NotoSerifGeorgian-Regular">NotoSerifGeorgian-VF.ttf + <axis tag="wght" stylevalue="600"/> + </font> + <font weight="700" style="normal" fallbackFor="serif" + postScriptName="NotoSerifGeorgian-Regular">NotoSerifGeorgian-VF.ttf + <axis tag="wght" stylevalue="700"/> + </font> + </family> + <family lang="und-Deva" variant="elegant"> + <font weight="400" style="normal" postScriptName="NotoSansDevanagari-Regular"> + NotoSansDevanagari-VF.ttf + <axis tag="wght" stylevalue="400"/> + </font> + <font weight="500" style="normal" postScriptName="NotoSansDevanagari-Regular"> + NotoSansDevanagari-VF.ttf + <axis tag="wght" stylevalue="500"/> + </font> + <font weight="600" style="normal" postScriptName="NotoSansDevanagari-Regular"> + NotoSansDevanagari-VF.ttf + <axis tag="wght" stylevalue="600"/> + </font> + <font weight="700" style="normal" postScriptName="NotoSansDevanagari-Regular"> + NotoSansDevanagari-VF.ttf + <axis tag="wght" stylevalue="700"/> + </font> + <font weight="400" style="normal" fallbackFor="serif" + postScriptName="NotoSerifDevanagari-Regular">NotoSerifDevanagari-VF.ttf + <axis tag="wght" stylevalue="400"/> + </font> + <font weight="500" style="normal" fallbackFor="serif" + postScriptName="NotoSerifDevanagari-Regular">NotoSerifDevanagari-VF.ttf + <axis tag="wght" stylevalue="500"/> + </font> + <font weight="600" style="normal" fallbackFor="serif" + postScriptName="NotoSerifDevanagari-Regular">NotoSerifDevanagari-VF.ttf + <axis tag="wght" stylevalue="600"/> + </font> + <font weight="700" style="normal" fallbackFor="serif" + postScriptName="NotoSerifDevanagari-Regular">NotoSerifDevanagari-VF.ttf + <axis tag="wght" stylevalue="700"/> + </font> + </family> + <family lang="und-Deva" variant="compact"> + <font weight="400" style="normal" postScriptName="NotoSansDevanagariUI-Regular"> + NotoSansDevanagariUI-VF.ttf + <axis tag="wght" stylevalue="400"/> + </font> + <font weight="500" style="normal" postScriptName="NotoSansDevanagariUI-Regular"> + NotoSansDevanagariUI-VF.ttf + <axis tag="wght" stylevalue="500"/> + </font> + <font weight="600" style="normal" postScriptName="NotoSansDevanagariUI-Regular"> + NotoSansDevanagariUI-VF.ttf + <axis tag="wght" stylevalue="600"/> + </font> + <font weight="700" style="normal" postScriptName="NotoSansDevanagariUI-Regular"> + NotoSansDevanagariUI-VF.ttf + <axis tag="wght" stylevalue="700"/> + </font> + </family> + + <!-- All scripts of India should come after Devanagari, due to shared + danda characters. + --> + <family lang="und-Gujr" variant="elegant"> + <font weight="400" style="normal" postScriptName="NotoSansGujarati"> + NotoSansGujarati-Regular.ttf + </font> + <font weight="700" style="normal">NotoSansGujarati-Bold.ttf</font> + <font weight="400" style="normal" fallbackFor="serif" + postScriptName="NotoSerifGujarati-Regular">NotoSerifGujarati-VF.ttf + <axis tag="wght" stylevalue="400"/> + </font> + <font weight="500" style="normal" fallbackFor="serif" + postScriptName="NotoSerifGujarati-Regular">NotoSerifGujarati-VF.ttf + <axis tag="wght" stylevalue="500"/> + </font> + <font weight="600" style="normal" fallbackFor="serif" + postScriptName="NotoSerifGujarati-Regular">NotoSerifGujarati-VF.ttf + <axis tag="wght" stylevalue="600"/> + </font> + <font weight="700" style="normal" fallbackFor="serif" + postScriptName="NotoSerifGujarati-Regular">NotoSerifGujarati-VF.ttf + <axis tag="wght" stylevalue="700"/> + </font> + </family> + <family lang="und-Gujr" variant="compact"> + <font weight="400" style="normal" postScriptName="NotoSansGujaratiUI"> + NotoSansGujaratiUI-Regular.ttf + </font> + <font weight="700" style="normal">NotoSansGujaratiUI-Bold.ttf</font> + </family> + <family lang="und-Guru" variant="elegant"> + <font weight="400" style="normal" postScriptName="NotoSansGurmukhi-Regular"> + NotoSansGurmukhi-VF.ttf + <axis tag="wght" stylevalue="400"/> + </font> + <font weight="500" style="normal" postScriptName="NotoSansGurmukhi-Regular"> + NotoSansGurmukhi-VF.ttf + <axis tag="wght" stylevalue="500"/> + </font> + <font weight="600" style="normal" postScriptName="NotoSansGurmukhi-Regular"> + NotoSansGurmukhi-VF.ttf + <axis tag="wght" stylevalue="600"/> + </font> + <font weight="700" style="normal" postScriptName="NotoSansGurmukhi-Regular"> + NotoSansGurmukhi-VF.ttf + <axis tag="wght" stylevalue="700"/> + </font> + <font weight="400" style="normal" fallbackFor="serif" + postScriptName="NotoSerifGurmukhi-Regular">NotoSerifGurmukhi-VF.ttf + <axis tag="wght" stylevalue="400"/> + </font> + <font weight="500" style="normal" fallbackFor="serif" + postScriptName="NotoSerifGurmukhi-Regular">NotoSerifGurmukhi-VF.ttf + <axis tag="wght" stylevalue="500"/> + </font> + <font weight="600" style="normal" fallbackFor="serif" + postScriptName="NotoSerifGurmukhi-Regular">NotoSerifGurmukhi-VF.ttf + <axis tag="wght" stylevalue="600"/> + </font> + <font weight="700" style="normal" fallbackFor="serif" + postScriptName="NotoSerifGurmukhi-Regular">NotoSerifGurmukhi-VF.ttf + <axis tag="wght" stylevalue="700"/> + </font> + </family> + <family lang="und-Guru" variant="compact"> + <font weight="400" style="normal" postScriptName="NotoSansGurmukhiUI-Regular"> + NotoSansGurmukhiUI-VF.ttf + <axis tag="wght" stylevalue="400"/> + </font> + <font weight="500" style="normal" postScriptName="NotoSansGurmukhiUI-Regular"> + NotoSansGurmukhiUI-VF.ttf + <axis tag="wght" stylevalue="500"/> + </font> + <font weight="600" style="normal" postScriptName="NotoSansGurmukhiUI-Regular"> + NotoSansGurmukhiUI-VF.ttf + <axis tag="wght" stylevalue="600"/> + </font> + <font weight="700" style="normal" postScriptName="NotoSansGurmukhiUI-Regular"> + NotoSansGurmukhiUI-VF.ttf + <axis tag="wght" stylevalue="700"/> + </font> + </family> + <family lang="und-Taml" variant="elegant"> + <font weight="400" style="normal" postScriptName="NotoSansTamil-Regular"> + NotoSansTamil-VF.ttf + <axis tag="wght" stylevalue="400"/> + </font> + <font weight="500" style="normal" postScriptName="NotoSansTamil-Regular"> + NotoSansTamil-VF.ttf + <axis tag="wght" stylevalue="500"/> + </font> + <font weight="600" style="normal" postScriptName="NotoSansTamil-Regular"> + NotoSansTamil-VF.ttf + <axis tag="wght" stylevalue="600"/> + </font> + <font weight="700" style="normal" postScriptName="NotoSansTamil-Regular"> + NotoSansTamil-VF.ttf + <axis tag="wght" stylevalue="700"/> + </font> + <font weight="400" style="normal" fallbackFor="serif" + postScriptName="NotoSerifTamil-Regular">NotoSerifTamil-VF.ttf + <axis tag="wght" stylevalue="400"/> + </font> + <font weight="500" style="normal" fallbackFor="serif" + postScriptName="NotoSerifTamil-Regular">NotoSerifTamil-VF.ttf + <axis tag="wght" stylevalue="500"/> + </font> + <font weight="600" style="normal" fallbackFor="serif" + postScriptName="NotoSerifTamil-Regular">NotoSerifTamil-VF.ttf + <axis tag="wght" stylevalue="600"/> + </font> + <font weight="700" style="normal" fallbackFor="serif" + postScriptName="NotoSerifTamil-Regular">NotoSerifTamil-VF.ttf + <axis tag="wght" stylevalue="700"/> + </font> + </family> + <family lang="und-Taml" variant="compact"> + <font weight="400" style="normal" postScriptName="NotoSansTamilUI-Regular"> + NotoSansTamilUI-VF.ttf + <axis tag="wght" stylevalue="400"/> + </font> + <font weight="500" style="normal" postScriptName="NotoSansTamilUI-Regular"> + NotoSansTamilUI-VF.ttf + <axis tag="wght" stylevalue="500"/> + </font> + <font weight="600" style="normal" postScriptName="NotoSansTamilUI-Regular"> + NotoSansTamilUI-VF.ttf + <axis tag="wght" stylevalue="600"/> + </font> + <font weight="700" style="normal" postScriptName="NotoSansTamilUI-Regular"> + NotoSansTamilUI-VF.ttf + <axis tag="wght" stylevalue="700"/> + </font> + </family> + <family lang="und-Mlym" variant="elegant"> + <font weight="400" style="normal" postScriptName="NotoSansMalayalam-Regular"> + NotoSansMalayalam-VF.ttf + <axis tag="wght" stylevalue="400"/> + </font> + <font weight="500" style="normal" postScriptName="NotoSansMalayalam-Regular"> + NotoSansMalayalam-VF.ttf + <axis tag="wght" stylevalue="500"/> + </font> + <font weight="600" style="normal" postScriptName="NotoSansMalayalam-Regular"> + NotoSansMalayalam-VF.ttf + <axis tag="wght" stylevalue="600"/> + </font> + <font weight="700" style="normal" postScriptName="NotoSansMalayalam-Regular"> + NotoSansMalayalam-VF.ttf + <axis tag="wght" stylevalue="700"/> + </font> + <font weight="400" style="normal" fallbackFor="serif" + postScriptName="NotoSerifMalayalam-Regular">NotoSerifMalayalam-VF.ttf + <axis tag="wght" stylevalue="400"/> + </font> + <font weight="500" style="normal" fallbackFor="serif" + postScriptName="NotoSerifMalayalam-Regular">NotoSerifMalayalam-VF.ttf + <axis tag="wght" stylevalue="500"/> + </font> + <font weight="600" style="normal" fallbackFor="serif" + postScriptName="NotoSerifMalayalam-Regular">NotoSerifMalayalam-VF.ttf + <axis tag="wght" stylevalue="600"/> + </font> + <font weight="700" style="normal" fallbackFor="serif" + postScriptName="NotoSerifMalayalam-Regular">NotoSerifMalayalam-VF.ttf + <axis tag="wght" stylevalue="700"/> + </font> + </family> + <family lang="und-Mlym" variant="compact"> + <font weight="400" style="normal" postScriptName="NotoSansMalayalamUI-Regular"> + NotoSansMalayalamUI-VF.ttf + <axis tag="wght" stylevalue="400"/> + </font> + <font weight="500" style="normal" postScriptName="NotoSansMalayalamUI-Regular"> + NotoSansMalayalamUI-VF.ttf + <axis tag="wght" stylevalue="500"/> + </font> + <font weight="600" style="normal" postScriptName="NotoSansMalayalamUI-Regular"> + NotoSansMalayalamUI-VF.ttf + <axis tag="wght" stylevalue="600"/> + </font> + <font weight="700" style="normal" postScriptName="NotoSansMalayalamUI-Regular"> + NotoSansMalayalamUI-VF.ttf + <axis tag="wght" stylevalue="700"/> + </font> + </family> + <family lang="und-Beng" variant="elegant"> + <font weight="400" style="normal" postScriptName="NotoSansBengali-Regular"> + NotoSansBengali-VF.ttf + <axis tag="wght" stylevalue="400"/> + </font> + <font weight="500" style="normal" postScriptName="NotoSansBengali-Regular"> + NotoSansBengali-VF.ttf + <axis tag="wght" stylevalue="500"/> + </font> + <font weight="600" style="normal" postScriptName="NotoSansBengali-Regular"> + NotoSansBengali-VF.ttf + <axis tag="wght" stylevalue="600"/> + </font> + <font weight="700" style="normal" postScriptName="NotoSansBengali-Regular"> + NotoSansBengali-VF.ttf + <axis tag="wght" stylevalue="700"/> + </font> + <font weight="400" style="normal" fallbackFor="serif" + postScriptName="NotoSerifBengali-Regular">NotoSerifBengali-VF.ttf + <axis tag="wght" stylevalue="400"/> + </font> + <font weight="500" style="normal" fallbackFor="serif" + postScriptName="NotoSerifBengali-Regular">NotoSerifBengali-VF.ttf + <axis tag="wght" stylevalue="500"/> + </font> + <font weight="600" style="normal" fallbackFor="serif" + postScriptName="NotoSerifBengali-Regular">NotoSerifBengali-VF.ttf + <axis tag="wght" stylevalue="600"/> + </font> + <font weight="700" style="normal" fallbackFor="serif" + postScriptName="NotoSerifBengali-Regular">NotoSerifBengali-VF.ttf + <axis tag="wght" stylevalue="700"/> + </font> + </family> + <family lang="und-Beng" variant="compact"> + <font weight="400" style="normal" postScriptName="NotoSansBengaliUI-Regular"> + NotoSansBengaliUI-VF.ttf + <axis tag="wght" stylevalue="400"/> + </font> + <font weight="500" style="normal" postScriptName="NotoSansBengaliUI-Regular"> + NotoSansBengaliUI-VF.ttf + <axis tag="wght" stylevalue="500"/> + </font> + <font weight="600" style="normal" postScriptName="NotoSansBengaliUI-Regular"> + NotoSansBengaliUI-VF.ttf + <axis tag="wght" stylevalue="600"/> + </font> + <font weight="700" style="normal" postScriptName="NotoSansBengaliUI-Regular"> + NotoSansBengaliUI-VF.ttf + <axis tag="wght" stylevalue="700"/> + </font> + </family> + <family lang="und-Telu" variant="elegant"> + <font weight="400" style="normal" postScriptName="NotoSansTelugu-Regular"> + NotoSansTelugu-VF.ttf + <axis tag="wght" stylevalue="400"/> + </font> + <font weight="500" style="normal" postScriptName="NotoSansTelugu-Regular"> + NotoSansTelugu-VF.ttf + <axis tag="wght" stylevalue="500"/> + </font> + <font weight="600" style="normal" postScriptName="NotoSansTelugu-Regular"> + NotoSansTelugu-VF.ttf + <axis tag="wght" stylevalue="600"/> + </font> + <font weight="700" style="normal" postScriptName="NotoSansTelugu-Regular"> + NotoSansTelugu-VF.ttf + <axis tag="wght" stylevalue="700"/> + </font> + <font weight="400" style="normal" fallbackFor="serif" + postScriptName="NotoSerifTelugu-Regular">NotoSerifTelugu-VF.ttf + <axis tag="wght" stylevalue="400"/> + </font> + <font weight="500" style="normal" fallbackFor="serif" + postScriptName="NotoSerifTelugu-Regular">NotoSerifTelugu-VF.ttf + <axis tag="wght" stylevalue="500"/> + </font> + <font weight="600" style="normal" fallbackFor="serif" + postScriptName="NotoSerifTelugu-Regular">NotoSerifTelugu-VF.ttf + <axis tag="wght" stylevalue="600"/> + </font> + <font weight="700" style="normal" fallbackFor="serif" + postScriptName="NotoSerifTelugu-Regular">NotoSerifTelugu-VF.ttf + <axis tag="wght" stylevalue="700"/> + </font> + </family> + <family lang="und-Telu" variant="compact"> + <font weight="400" style="normal" postScriptName="NotoSansTeluguUI-Regular"> + NotoSansTeluguUI-VF.ttf + <axis tag="wght" stylevalue="400"/> + </font> + <font weight="500" style="normal" postScriptName="NotoSansTeluguUI-Regular"> + NotoSansTeluguUI-VF.ttf + <axis tag="wght" stylevalue="500"/> + </font> + <font weight="600" style="normal" postScriptName="NotoSansTeluguUI-Regular"> + NotoSansTeluguUI-VF.ttf + <axis tag="wght" stylevalue="600"/> + </font> + <font weight="700" style="normal" postScriptName="NotoSansTeluguUI-Regular"> + NotoSansTeluguUI-VF.ttf + <axis tag="wght" stylevalue="700"/> + </font> + </family> + <family lang="und-Knda" variant="elegant"> + <font weight="400" style="normal" postScriptName="NotoSansKannada-Regular"> + NotoSansKannada-VF.ttf + <axis tag="wght" stylevalue="400"/> + </font> + <font weight="500" style="normal" postScriptName="NotoSansKannada-Regular"> + NotoSansKannada-VF.ttf + <axis tag="wght" stylevalue="500"/> + </font> + <font weight="600" style="normal" postScriptName="NotoSansKannada-Regular"> + NotoSansKannada-VF.ttf + <axis tag="wght" stylevalue="600"/> + </font> + <font weight="700" style="normal" postScriptName="NotoSansKannada-Regular"> + NotoSansKannada-VF.ttf + <axis tag="wght" stylevalue="700"/> + </font> + <font weight="400" style="normal" fallbackFor="serif" + postScriptName="NotoSerifKannada-Regular">NotoSerifKannada-VF.ttf + <axis tag="wght" stylevalue="400"/> + </font> + <font weight="500" style="normal" fallbackFor="serif" + postScriptName="NotoSerifKannada-Regular">NotoSerifKannada-VF.ttf + <axis tag="wght" stylevalue="500"/> + </font> + <font weight="600" style="normal" fallbackFor="serif" + postScriptName="NotoSerifKannada-Regular">NotoSerifKannada-VF.ttf + <axis tag="wght" stylevalue="600"/> + </font> + <font weight="700" style="normal" fallbackFor="serif" + postScriptName="NotoSerifKannada-Regular">NotoSerifKannada-VF.ttf + <axis tag="wght" stylevalue="700"/> + </font> + </family> + <family lang="und-Knda" variant="compact"> + <font weight="400" style="normal" postScriptName="NotoSansKannadaUI-Regular"> + NotoSansKannadaUI-VF.ttf + <axis tag="wght" stylevalue="400"/> + </font> + <font weight="500" style="normal" postScriptName="NotoSansKannadaUI-Regular"> + NotoSansKannadaUI-VF.ttf + <axis tag="wght" stylevalue="500"/> + </font> + <font weight="600" style="normal" postScriptName="NotoSansKannadaUI-Regular"> + NotoSansKannadaUI-VF.ttf + <axis tag="wght" stylevalue="600"/> + </font> + <font weight="700" style="normal" postScriptName="NotoSansKannadaUI-Regular"> + NotoSansKannadaUI-VF.ttf + <axis tag="wght" stylevalue="700"/> + </font> + </family> + <family lang="und-Orya" variant="elegant"> + <font weight="400" style="normal" postScriptName="NotoSansOriya">NotoSansOriya-Regular.ttf + </font> + <font weight="700" style="normal">NotoSansOriya-Bold.ttf</font> + </family> + <family lang="und-Orya" variant="compact"> + <font weight="400" style="normal" postScriptName="NotoSansOriyaUI"> + NotoSansOriyaUI-Regular.ttf + </font> + <font weight="700" style="normal">NotoSansOriyaUI-Bold.ttf</font> + </family> + <family lang="und-Sinh" variant="elegant"> + <font weight="400" style="normal" postScriptName="NotoSansSinhala-Regular"> + NotoSansSinhala-VF.ttf + <axis tag="wght" stylevalue="400"/> + </font> + <font weight="500" style="normal" postScriptName="NotoSansSinhala-Regular"> + NotoSansSinhala-VF.ttf + <axis tag="wght" stylevalue="500"/> + </font> + <font weight="600" style="normal" postScriptName="NotoSansSinhala-Regular"> + NotoSansSinhala-VF.ttf + <axis tag="wght" stylevalue="600"/> + </font> + <font weight="700" style="normal" postScriptName="NotoSansSinhala-Regular"> + NotoSansSinhala-VF.ttf + <axis tag="wght" stylevalue="700"/> + </font> + <font weight="400" style="normal" fallbackFor="serif" + postScriptName="NotoSerifSinhala-Regular">NotoSerifSinhala-VF.ttf + <axis tag="wght" stylevalue="400"/> + </font> + <font weight="500" style="normal" fallbackFor="serif" + postScriptName="NotoSerifSinhala-Regular">NotoSerifSinhala-VF.ttf + <axis tag="wght" stylevalue="500"/> + </font> + <font weight="600" style="normal" fallbackFor="serif" + postScriptName="NotoSerifSinhala-Regular">NotoSerifSinhala-VF.ttf + <axis tag="wght" stylevalue="600"/> + </font> + <font weight="700" style="normal" fallbackFor="serif" + postScriptName="NotoSerifSinhala-Regular">NotoSerifSinhala-VF.ttf + <axis tag="wght" stylevalue="700"/> + </font> + </family> + <family lang="und-Sinh" variant="compact"> + <font weight="400" style="normal" postScriptName="NotoSansSinhalaUI-Regular"> + NotoSansSinhalaUI-VF.ttf + <axis tag="wght" stylevalue="400"/> + </font> + <font weight="500" style="normal" postScriptName="NotoSansSinhalaUI-Regular"> + NotoSansSinhalaUI-VF.ttf + <axis tag="wght" stylevalue="500"/> + </font> + <font weight="600" style="normal" postScriptName="NotoSansSinhalaUI-Regular"> + NotoSansSinhalaUI-VF.ttf + <axis tag="wght" stylevalue="600"/> + </font> + <font weight="700" style="normal" postScriptName="NotoSansSinhalaUI-Regular"> + NotoSansSinhalaUI-VF.ttf + <axis tag="wght" stylevalue="700"/> + </font> + </family> + <family lang="und-Khmr" variant="elegant"> + <font weight="100" style="normal" postScriptName="NotoSansKhmer-Regular"> + NotoSansKhmer-VF.ttf + <axis tag="wdth" stylevalue="100.0"/> + <axis tag="wght" stylevalue="26.0"/> + </font> + <font weight="200" style="normal" postScriptName="NotoSansKhmer-Regular"> + NotoSansKhmer-VF.ttf + <axis tag="wdth" stylevalue="100.0"/> + <axis tag="wght" stylevalue="39.0"/> + </font> + <font weight="300" style="normal" postScriptName="NotoSansKhmer-Regular"> + NotoSansKhmer-VF.ttf + <axis tag="wdth" stylevalue="100.0"/> + <axis tag="wght" stylevalue="58.0"/> + </font> + <font weight="400" style="normal" postScriptName="NotoSansKhmer-Regular"> + NotoSansKhmer-VF.ttf + <axis tag="wdth" stylevalue="100.0"/> + <axis tag="wght" stylevalue="90.0"/> + </font> + <font weight="500" style="normal" postScriptName="NotoSansKhmer-Regular"> + NotoSansKhmer-VF.ttf + <axis tag="wdth" stylevalue="100.0"/> + <axis tag="wght" stylevalue="108.0"/> + </font> + <font weight="600" style="normal" postScriptName="NotoSansKhmer-Regular"> + NotoSansKhmer-VF.ttf + <axis tag="wdth" stylevalue="100.0"/> + <axis tag="wght" stylevalue="128.0"/> + </font> + <font weight="700" style="normal" postScriptName="NotoSansKhmer-Regular"> + NotoSansKhmer-VF.ttf + <axis tag="wdth" stylevalue="100.0"/> + <axis tag="wght" stylevalue="151.0"/> + </font> + <font weight="800" style="normal" postScriptName="NotoSansKhmer-Regular"> + NotoSansKhmer-VF.ttf + <axis tag="wdth" stylevalue="100.0"/> + <axis tag="wght" stylevalue="169.0"/> + </font> + <font weight="900" style="normal" postScriptName="NotoSansKhmer-Regular"> + NotoSansKhmer-VF.ttf + <axis tag="wdth" stylevalue="100.0"/> + <axis tag="wght" stylevalue="190.0"/> + </font> + <font weight="400" style="normal" fallbackFor="serif">NotoSerifKhmer-Regular.otf</font> + <font weight="700" style="normal" fallbackFor="serif">NotoSerifKhmer-Bold.otf</font> + </family> + <family lang="und-Khmr" variant="compact"> + <font weight="400" style="normal" postScriptName="NotoSansKhmerUI"> + NotoSansKhmerUI-Regular.ttf + </font> + <font weight="700" style="normal">NotoSansKhmerUI-Bold.ttf</font> + </family> + <family lang="und-Laoo" variant="elegant"> + <font weight="400" style="normal">NotoSansLao-Regular.ttf + </font> + <font weight="700" style="normal">NotoSansLao-Bold.ttf</font> + <font weight="400" style="normal" fallbackFor="serif"> + NotoSerifLao-Regular.ttf + </font> + <font weight="700" style="normal" fallbackFor="serif">NotoSerifLao-Bold.ttf</font> + </family> + <family lang="und-Laoo" variant="compact"> + <font weight="400" style="normal" postScriptName="NotoSansLaoUI">NotoSansLaoUI-Regular.ttf + </font> + <font weight="700" style="normal">NotoSansLaoUI-Bold.ttf</font> + </family> + <family lang="und-Mymr" variant="elegant"> + <font weight="400" style="normal">NotoSansMyanmar-Regular.otf</font> + <font weight="500" style="normal">NotoSansMyanmar-Medium.otf</font> + <font weight="700" style="normal">NotoSansMyanmar-Bold.otf</font> + <font weight="400" style="normal" fallbackFor="serif">NotoSerifMyanmar-Regular.otf</font> + <font weight="700" style="normal" fallbackFor="serif">NotoSerifMyanmar-Bold.otf</font> + </family> + <family lang="und-Mymr" variant="compact"> + <font weight="400" style="normal">NotoSansMyanmarUI-Regular.otf</font> + <font weight="500" style="normal">NotoSansMyanmarUI-Medium.otf</font> + <font weight="700" style="normal">NotoSansMyanmarUI-Bold.otf</font> + </family> + <family lang="und-Thaa"> + <font weight="400" style="normal" postScriptName="NotoSansThaana"> + NotoSansThaana-Regular.ttf + </font> + <font weight="700" style="normal">NotoSansThaana-Bold.ttf</font> + </family> + <family lang="und-Cham"> + <font weight="400" style="normal" postScriptName="NotoSansCham">NotoSansCham-Regular.ttf + </font> + <font weight="700" style="normal">NotoSansCham-Bold.ttf</font> + </family> + <family lang="und-Ahom"> + <font weight="400" style="normal">NotoSansAhom-Regular.otf</font> + </family> + <family lang="und-Adlm"> + <font weight="400" style="normal" postScriptName="NotoSansAdlam-Regular"> + NotoSansAdlam-VF.ttf + <axis tag="wght" stylevalue="400"/> + </font> + <font weight="500" style="normal" postScriptName="NotoSansAdlam-Regular"> + NotoSansAdlam-VF.ttf + <axis tag="wght" stylevalue="500"/> + </font> + <font weight="600" style="normal" postScriptName="NotoSansAdlam-Regular"> + NotoSansAdlam-VF.ttf + <axis tag="wght" stylevalue="600"/> + </font> + <font weight="700" style="normal" postScriptName="NotoSansAdlam-Regular"> + NotoSansAdlam-VF.ttf + <axis tag="wght" stylevalue="700"/> + </font> + </family> + <family lang="und-Avst"> + <font weight="400" style="normal" postScriptName="NotoSansAvestan"> + NotoSansAvestan-Regular.ttf + </font> + </family> + <family lang="und-Bali"> + <font weight="400" style="normal" postScriptName="NotoSansBalinese"> + NotoSansBalinese-Regular.ttf + </font> + </family> + <family lang="und-Bamu"> + <font weight="400" style="normal" postScriptName="NotoSansBamum">NotoSansBamum-Regular.ttf + </font> + </family> + <family lang="und-Batk"> + <font weight="400" style="normal" postScriptName="NotoSansBatak">NotoSansBatak-Regular.ttf + </font> + </family> + <family lang="und-Brah"> + <font weight="400" style="normal" postScriptName="NotoSansBrahmi"> + NotoSansBrahmi-Regular.ttf + </font> + </family> + <family lang="und-Bugi"> + <font weight="400" style="normal" postScriptName="NotoSansBuginese"> + NotoSansBuginese-Regular.ttf + </font> + </family> + <family lang="und-Buhd"> + <font weight="400" style="normal" postScriptName="NotoSansBuhid">NotoSansBuhid-Regular.ttf + </font> + </family> + <family lang="und-Cans"> + <font weight="400" style="normal"> + NotoSansCanadianAboriginal-Regular.ttf + </font> + </family> + <family lang="und-Cari"> + <font weight="400" style="normal" postScriptName="NotoSansCarian"> + NotoSansCarian-Regular.ttf + </font> + </family> + <family lang="und-Cakm"> + <font weight="400" style="normal">NotoSansChakma-Regular.otf</font> + </family> + <family lang="und-Cher"> + <font weight="400" style="normal">NotoSansCherokee-Regular.ttf</font> + </family> + <family lang="und-Copt"> + <font weight="400" style="normal" postScriptName="NotoSansCoptic"> + NotoSansCoptic-Regular.ttf + </font> + </family> + <family lang="und-Xsux"> + <font weight="400" style="normal" postScriptName="NotoSansCuneiform"> + NotoSansCuneiform-Regular.ttf + </font> + </family> + <family lang="und-Cprt"> + <font weight="400" style="normal" postScriptName="NotoSansCypriot"> + NotoSansCypriot-Regular.ttf + </font> + </family> + <family lang="und-Dsrt"> + <font weight="400" style="normal" postScriptName="NotoSansDeseret"> + NotoSansDeseret-Regular.ttf + </font> + </family> + <family lang="und-Egyp"> + <font weight="400" style="normal" postScriptName="NotoSansEgyptianHieroglyphs"> + NotoSansEgyptianHieroglyphs-Regular.ttf + </font> + </family> + <family lang="und-Elba"> + <font weight="400" style="normal">NotoSansElbasan-Regular.otf</font> + </family> + <family lang="und-Glag"> + <font weight="400" style="normal" postScriptName="NotoSansGlagolitic"> + NotoSansGlagolitic-Regular.ttf + </font> + </family> + <family lang="und-Goth"> + <font weight="400" style="normal" postScriptName="NotoSansGothic"> + NotoSansGothic-Regular.ttf + </font> + </family> + <family lang="und-Hano"> + <font weight="400" style="normal" postScriptName="NotoSansHanunoo"> + NotoSansHanunoo-Regular.ttf + </font> + </family> + <family lang="und-Armi"> + <font weight="400" style="normal" postScriptName="NotoSansImperialAramaic"> + NotoSansImperialAramaic-Regular.ttf + </font> + </family> + <family lang="und-Phli"> + <font weight="400" style="normal" postScriptName="NotoSansInscriptionalPahlavi"> + NotoSansInscriptionalPahlavi-Regular.ttf + </font> + </family> + <family lang="und-Prti"> + <font weight="400" style="normal" postScriptName="NotoSansInscriptionalParthian"> + NotoSansInscriptionalParthian-Regular.ttf + </font> + </family> + <family lang="und-Java"> + <font weight="400" style="normal">NotoSansJavanese-Regular.otf</font> + </family> + <family lang="und-Kthi"> + <font weight="400" style="normal" postScriptName="NotoSansKaithi"> + NotoSansKaithi-Regular.ttf + </font> + </family> + <family lang="und-Kali"> + <font weight="400" style="normal" postScriptName="NotoSansKayahLi"> + NotoSansKayahLi-Regular.ttf + </font> + </family> + <family lang="und-Khar"> + <font weight="400" style="normal" postScriptName="NotoSansKharoshthi"> + NotoSansKharoshthi-Regular.ttf + </font> + </family> + <family lang="und-Lepc"> + <font weight="400" style="normal" postScriptName="NotoSansLepcha"> + NotoSansLepcha-Regular.ttf + </font> + </family> + <family lang="und-Limb"> + <font weight="400" style="normal" postScriptName="NotoSansLimbu">NotoSansLimbu-Regular.ttf + </font> + </family> + <family lang="und-Linb"> + <font weight="400" style="normal" postScriptName="NotoSansLinearB"> + NotoSansLinearB-Regular.ttf + </font> + </family> + <family lang="und-Lisu"> + <font weight="400" style="normal" postScriptName="NotoSansLisu">NotoSansLisu-Regular.ttf + </font> + </family> + <family lang="und-Lyci"> + <font weight="400" style="normal" postScriptName="NotoSansLycian"> + NotoSansLycian-Regular.ttf + </font> + </family> + <family lang="und-Lydi"> + <font weight="400" style="normal" postScriptName="NotoSansLydian"> + NotoSansLydian-Regular.ttf + </font> + </family> + <family lang="und-Mand"> + <font weight="400" style="normal" postScriptName="NotoSansMandaic"> + NotoSansMandaic-Regular.ttf + </font> + </family> + <family lang="und-Mtei"> + <font weight="400" style="normal" postScriptName="NotoSansMeeteiMayek"> + NotoSansMeeteiMayek-Regular.ttf + </font> + </family> + <family lang="und-Talu"> + <font weight="400" style="normal" postScriptName="NotoSansNewTaiLue"> + NotoSansNewTaiLue-Regular.ttf + </font> + </family> + <family lang="und-Nkoo"> + <font weight="400" style="normal" postScriptName="NotoSansNKo">NotoSansNKo-Regular.ttf + </font> + </family> + <family lang="und-Ogam"> + <font weight="400" style="normal" postScriptName="NotoSansOgham">NotoSansOgham-Regular.ttf + </font> + </family> + <family lang="und-Olck"> + <font weight="400" style="normal" postScriptName="NotoSansOlChiki"> + NotoSansOlChiki-Regular.ttf + </font> + </family> + <family lang="und-Ital"> + <font weight="400" style="normal" postScriptName="NotoSansOldItalic"> + NotoSansOldItalic-Regular.ttf + </font> + </family> + <family lang="und-Xpeo"> + <font weight="400" style="normal" postScriptName="NotoSansOldPersian"> + NotoSansOldPersian-Regular.ttf + </font> + </family> + <family lang="und-Sarb"> + <font weight="400" style="normal" postScriptName="NotoSansOldSouthArabian"> + NotoSansOldSouthArabian-Regular.ttf + </font> + </family> + <family lang="und-Orkh"> + <font weight="400" style="normal" postScriptName="NotoSansOldTurkic"> + NotoSansOldTurkic-Regular.ttf + </font> + </family> + <family lang="und-Osge"> + <font weight="400" style="normal">NotoSansOsage-Regular.ttf</font> + </family> + <family lang="und-Osma"> + <font weight="400" style="normal" postScriptName="NotoSansOsmanya"> + NotoSansOsmanya-Regular.ttf + </font> + </family> + <family lang="und-Phnx"> + <font weight="400" style="normal" postScriptName="NotoSansPhoenician"> + NotoSansPhoenician-Regular.ttf + </font> + </family> + <family lang="und-Rjng"> + <font weight="400" style="normal" postScriptName="NotoSansRejang"> + NotoSansRejang-Regular.ttf + </font> + </family> + <family lang="und-Runr"> + <font weight="400" style="normal" postScriptName="NotoSansRunic">NotoSansRunic-Regular.ttf + </font> + </family> + <family lang="und-Samr"> + <font weight="400" style="normal" postScriptName="NotoSansSamaritan"> + NotoSansSamaritan-Regular.ttf + </font> + </family> + <family lang="und-Saur"> + <font weight="400" style="normal" postScriptName="NotoSansSaurashtra"> + NotoSansSaurashtra-Regular.ttf + </font> + </family> + <family lang="und-Shaw"> + <font weight="400" style="normal" postScriptName="NotoSansShavian"> + NotoSansShavian-Regular.ttf + </font> + </family> + <family lang="und-Sund"> + <font weight="400" style="normal" postScriptName="NotoSansSundanese"> + NotoSansSundanese-Regular.ttf + </font> + </family> + <family lang="und-Sylo"> + <font weight="400" style="normal" postScriptName="NotoSansSylotiNagri"> + NotoSansSylotiNagri-Regular.ttf + </font> + </family> + <!-- Esrangela should precede Eastern and Western Syriac, since it's our default form. --> + <family lang="und-Syre"> + <font weight="400" style="normal" postScriptName="NotoSansSyriacEstrangela"> + NotoSansSyriacEstrangela-Regular.ttf + </font> + </family> + <family lang="und-Syrn"> + <font weight="400" style="normal" postScriptName="NotoSansSyriacEastern"> + NotoSansSyriacEastern-Regular.ttf + </font> + </family> + <family lang="und-Syrj"> + <font weight="400" style="normal" postScriptName="NotoSansSyriacWestern"> + NotoSansSyriacWestern-Regular.ttf + </font> + </family> + <family lang="und-Tglg"> + <font weight="400" style="normal" postScriptName="NotoSansTagalog"> + NotoSansTagalog-Regular.ttf + </font> + </family> + <family lang="und-Tagb"> + <font weight="400" style="normal" postScriptName="NotoSansTagbanwa"> + NotoSansTagbanwa-Regular.ttf + </font> + </family> + <family lang="und-Lana"> + <font weight="400" style="normal" postScriptName="NotoSansTaiTham"> + NotoSansTaiTham-Regular.ttf + </font> + </family> + <family lang="und-Tavt"> + <font weight="400" style="normal" postScriptName="NotoSansTaiViet"> + NotoSansTaiViet-Regular.ttf + </font> + </family> + <family lang="und-Tibt"> + <font weight="400" style="normal" postScriptName="NotoSerifTibetan-Regular"> + NotoSerifTibetan-VF.ttf + <axis tag="wght" stylevalue="400"/> + </font> + <font weight="500" style="normal" postScriptName="NotoSerifTibetan-Regular"> + NotoSerifTibetan-VF.ttf + <axis tag="wght" stylevalue="500"/> + </font> + <font weight="600" style="normal" postScriptName="NotoSerifTibetan-Regular"> + NotoSerifTibetan-VF.ttf + <axis tag="wght" stylevalue="600"/> + </font> + <font weight="700" style="normal" postScriptName="NotoSerifTibetan-Regular"> + NotoSerifTibetan-VF.ttf + <axis tag="wght" stylevalue="700"/> + </font> + </family> + <family lang="und-Tfng"> + <font weight="400" style="normal">NotoSansTifinagh-Regular.otf</font> + </family> + <family lang="und-Ugar"> + <font weight="400" style="normal" postScriptName="NotoSansUgaritic"> + NotoSansUgaritic-Regular.ttf + </font> + </family> + <family lang="und-Vaii"> + <font weight="400" style="normal" postScriptName="NotoSansVai">NotoSansVai-Regular.ttf + </font> + </family> + <family> + <font weight="400" style="normal">NotoSansSymbols-Regular-Subsetted.ttf</font> + </family> + <family lang="zh-Hans"> + <font weight="100" style="normal" index="2" postScriptName="NotoSansCJKjp-Thin"> + NotoSansCJK-Regular.ttc + <axis tag="wght" stylevalue="100"/> + </font> + <font weight="200" style="normal" index="2" postScriptName="NotoSansCJKjp-Thin"> + NotoSansCJK-Regular.ttc + <axis tag="wght" stylevalue="200"/> + </font> + <font weight="300" style="normal" index="2" postScriptName="NotoSansCJKjp-Thin"> + NotoSansCJK-Regular.ttc + <axis tag="wght" stylevalue="300"/> + </font> + <font weight="400" style="normal" index="2" postScriptName="NotoSansCJKjp-Thin"> + NotoSansCJK-Regular.ttc + <axis tag="wght" stylevalue="400"/> + </font> + <font weight="500" style="normal" index="2" postScriptName="NotoSansCJKjp-Thin"> + NotoSansCJK-Regular.ttc + <axis tag="wght" stylevalue="500"/> + </font> + <font weight="600" style="normal" index="2" postScriptName="NotoSansCJKjp-Thin"> + NotoSansCJK-Regular.ttc + <axis tag="wght" stylevalue="600"/> + </font> + <font weight="700" style="normal" index="2" postScriptName="NotoSansCJKjp-Thin"> + NotoSansCJK-Regular.ttc + <axis tag="wght" stylevalue="700"/> + </font> + <font weight="800" style="normal" index="2" postScriptName="NotoSansCJKjp-Thin"> + NotoSansCJK-Regular.ttc + <axis tag="wght" stylevalue="800"/> + </font> + <font weight="900" style="normal" index="2" postScriptName="NotoSansCJKjp-Thin"> + NotoSansCJK-Regular.ttc + <axis tag="wght" stylevalue="900"/> + </font> + <font weight="400" style="normal" index="2" fallbackFor="serif" + postScriptName="NotoSerifCJKjp-Regular">NotoSerifCJK-Regular.ttc + </font> + </family> + <family lang="zh-Hant,zh-Bopo"> + <font weight="100" style="normal" index="3" postScriptName="NotoSansCJKjp-Thin"> + NotoSansCJK-Regular.ttc + <axis tag="wght" stylevalue="100"/> + </font> + <font weight="200" style="normal" index="3" postScriptName="NotoSansCJKjp-Thin"> + NotoSansCJK-Regular.ttc + <axis tag="wght" stylevalue="200"/> + </font> + <font weight="300" style="normal" index="3" postScriptName="NotoSansCJKjp-Thin"> + NotoSansCJK-Regular.ttc + <axis tag="wght" stylevalue="300"/> + </font> + <font weight="400" style="normal" index="3" postScriptName="NotoSansCJKjp-Thin"> + NotoSansCJK-Regular.ttc + <axis tag="wght" stylevalue="400"/> + </font> + <font weight="500" style="normal" index="3" postScriptName="NotoSansCJKjp-Thin"> + NotoSansCJK-Regular.ttc + <axis tag="wght" stylevalue="500"/> + </font> + <font weight="600" style="normal" index="3" postScriptName="NotoSansCJKjp-Thin"> + NotoSansCJK-Regular.ttc + <axis tag="wght" stylevalue="600"/> + </font> + <font weight="700" style="normal" index="3" postScriptName="NotoSansCJKjp-Thin"> + NotoSansCJK-Regular.ttc + <axis tag="wght" stylevalue="700"/> + </font> + <font weight="800" style="normal" index="3" postScriptName="NotoSansCJKjp-Thin"> + NotoSansCJK-Regular.ttc + <axis tag="wght" stylevalue="800"/> + </font> + <font weight="900" style="normal" index="3" postScriptName="NotoSansCJKjp-Thin"> + NotoSansCJK-Regular.ttc + <axis tag="wght" stylevalue="900"/> + </font> + <font weight="400" style="normal" index="3" fallbackFor="serif" + postScriptName="NotoSerifCJKjp-Regular">NotoSerifCJK-Regular.ttc + </font> + </family> + <family lang="ja"> + <font weight="100" style="normal" index="0" postScriptName="NotoSansCJKjp-Thin"> + NotoSansCJK-Regular.ttc + <axis tag="wght" stylevalue="100"/> + </font> + <font weight="200" style="normal" index="0" postScriptName="NotoSansCJKjp-Thin"> + NotoSansCJK-Regular.ttc + <axis tag="wght" stylevalue="200"/> + </font> + <font weight="300" style="normal" index="0" postScriptName="NotoSansCJKjp-Thin"> + NotoSansCJK-Regular.ttc + <axis tag="wght" stylevalue="300"/> + </font> + <font weight="400" style="normal" index="0" postScriptName="NotoSansCJKjp-Thin"> + NotoSansCJK-Regular.ttc + <axis tag="wght" stylevalue="400"/> + </font> + <font weight="500" style="normal" index="0" postScriptName="NotoSansCJKjp-Thin"> + NotoSansCJK-Regular.ttc + <axis tag="wght" stylevalue="500"/> + </font> + <font weight="600" style="normal" index="0" postScriptName="NotoSansCJKjp-Thin"> + NotoSansCJK-Regular.ttc + <axis tag="wght" stylevalue="600"/> + </font> + <font weight="700" style="normal" index="0" postScriptName="NotoSansCJKjp-Thin"> + NotoSansCJK-Regular.ttc + <axis tag="wght" stylevalue="700"/> + </font> + <font weight="800" style="normal" index="0" postScriptName="NotoSansCJKjp-Thin"> + NotoSansCJK-Regular.ttc + <axis tag="wght" stylevalue="800"/> + </font> + <font weight="900" style="normal" index="0" postScriptName="NotoSansCJKjp-Thin"> + NotoSansCJK-Regular.ttc + <axis tag="wght" stylevalue="900"/> + </font> + <font weight="400" style="normal" index="0" fallbackFor="serif" + postScriptName="NotoSerifCJKjp-Regular">NotoSerifCJK-Regular.ttc + </font> + </family> + <family lang="ko"> + <font weight="100" style="normal" index="1" postScriptName="NotoSansCJKjp-Thin"> + NotoSansCJK-Regular.ttc + <axis tag="wght" stylevalue="100"/> + </font> + <font weight="200" style="normal" index="1" postScriptName="NotoSansCJKjp-Thin"> + NotoSansCJK-Regular.ttc + <axis tag="wght" stylevalue="200"/> + </font> + <font weight="300" style="normal" index="1" postScriptName="NotoSansCJKjp-Thin"> + NotoSansCJK-Regular.ttc + <axis tag="wght" stylevalue="300"/> + </font> + <font weight="400" style="normal" index="1" postScriptName="NotoSansCJKjp-Thin"> + NotoSansCJK-Regular.ttc + <axis tag="wght" stylevalue="400"/> + </font> + <font weight="500" style="normal" index="1" postScriptName="NotoSansCJKjp-Thin"> + NotoSansCJK-Regular.ttc + <axis tag="wght" stylevalue="500"/> + </font> + <font weight="600" style="normal" index="1" postScriptName="NotoSansCJKjp-Thin"> + NotoSansCJK-Regular.ttc + <axis tag="wght" stylevalue="600"/> + </font> + <font weight="700" style="normal" index="1" postScriptName="NotoSansCJKjp-Thin"> + NotoSansCJK-Regular.ttc + <axis tag="wght" stylevalue="700"/> + </font> + <font weight="800" style="normal" index="1" postScriptName="NotoSansCJKjp-Thin"> + NotoSansCJK-Regular.ttc + <axis tag="wght" stylevalue="800"/> + </font> + <font weight="900" style="normal" index="1" postScriptName="NotoSansCJKjp-Thin"> + NotoSansCJK-Regular.ttc + <axis tag="wght" stylevalue="900"/> + </font> + <font weight="400" style="normal" index="1" fallbackFor="serif" + postScriptName="NotoSerifCJKjp-Regular">NotoSerifCJK-Regular.ttc + </font> + </family> + <family lang="und-Zsye" ignore="true"> + <font weight="400" style="normal">NotoColorEmojiLegacy.ttf</font> + </family> + <family lang="und-Zsye"> + <font weight="400" style="normal">NotoColorEmoji.ttf</font> + </family> + <family lang="und-Zsye"> + <font weight="400" style="normal">NotoColorEmojiFlags.ttf</font> + </family> + <family lang="und-Zsym"> + <font weight="400" style="normal">NotoSansSymbols-Regular-Subsetted2.ttf</font> + </family> + <!-- + Tai Le, Yi, Mongolian, and Phags-pa are intentionally kept last, to make sure they don't + override the East Asian punctuation for Chinese. + --> + <family lang="und-Tale"> + <font weight="400" style="normal" postScriptName="NotoSansTaiLe">NotoSansTaiLe-Regular.ttf + </font> + </family> + <family lang="und-Yiii"> + <font weight="400" style="normal" postScriptName="NotoSansYi">NotoSansYi-Regular.ttf</font> + </family> + <family lang="und-Mong"> + <font weight="400" style="normal" postScriptName="NotoSansMongolian"> + NotoSansMongolian-Regular.ttf + </font> + </family> + <family lang="und-Phag"> + <font weight="400" style="normal" postScriptName="NotoSansPhagsPa"> + NotoSansPhagsPa-Regular.ttf + </font> + </family> + <family lang="und-Hluw"> + <font weight="400" style="normal">NotoSansAnatolianHieroglyphs-Regular.otf</font> + </family> + <family lang="und-Bass"> + <font weight="400" style="normal">NotoSansBassaVah-Regular.otf</font> + </family> + <family lang="und-Bhks"> + <font weight="400" style="normal">NotoSansBhaiksuki-Regular.otf</font> + </family> + <family lang="und-Hatr"> + <font weight="400" style="normal">NotoSansHatran-Regular.otf</font> + </family> + <family lang="und-Lina"> + <font weight="400" style="normal">NotoSansLinearA-Regular.otf</font> + </family> + <family lang="und-Mani"> + <font weight="400" style="normal">NotoSansManichaean-Regular.otf</font> + </family> + <family lang="und-Marc"> + <font weight="400" style="normal">NotoSansMarchen-Regular.otf</font> + </family> + <family lang="und-Merc"> + <font weight="400" style="normal">NotoSansMeroitic-Regular.otf</font> + </family> + <family lang="und-Plrd"> + <font weight="400" style="normal">NotoSansMiao-Regular.otf</font> + </family> + <family lang="und-Mroo"> + <font weight="400" style="normal">NotoSansMro-Regular.otf</font> + </family> + <family lang="und-Mult"> + <font weight="400" style="normal">NotoSansMultani-Regular.otf</font> + </family> + <family lang="und-Nbat"> + <font weight="400" style="normal">NotoSansNabataean-Regular.otf</font> + </family> + <family lang="und-Newa"> + <font weight="400" style="normal">NotoSansNewa-Regular.otf</font> + </family> + <family lang="und-Narb"> + <font weight="400" style="normal">NotoSansOldNorthArabian-Regular.otf</font> + </family> + <family lang="und-Perm"> + <font weight="400" style="normal">NotoSansOldPermic-Regular.otf</font> + </family> + <family lang="und-Hmng"> + <font weight="400" style="normal">NotoSansPahawhHmong-Regular.otf</font> + </family> + <family lang="und-Palm"> + <font weight="400" style="normal">NotoSansPalmyrene-Regular.otf</font> + </family> + <family lang="und-Pauc"> + <font weight="400" style="normal">NotoSansPauCinHau-Regular.otf</font> + </family> + <family lang="und-Shrd"> + <font weight="400" style="normal">NotoSansSharada-Regular.otf</font> + </family> + <family lang="und-Sora"> + <font weight="400" style="normal">NotoSansSoraSompeng-Regular.otf</font> + </family> + <family lang="und-Gong"> + <font weight="400" style="normal">NotoSansGunjalaGondi-Regular.otf</font> + </family> + <family lang="und-Rohg"> + <font weight="400" style="normal">NotoSansHanifiRohingya-Regular.otf</font> + </family> + <family lang="und-Khoj"> + <font weight="400" style="normal">NotoSansKhojki-Regular.otf</font> + </family> + <family lang="und-Gonm"> + <font weight="400" style="normal">NotoSansMasaramGondi-Regular.otf</font> + </family> + <family lang="und-Wcho"> + <font weight="400" style="normal">NotoSansWancho-Regular.otf</font> + </family> + <family lang="und-Wara"> + <font weight="400" style="normal">NotoSansWarangCiti-Regular.otf</font> + </family> + <family lang="und-Gran"> + <font weight="400" style="normal">NotoSansGrantha-Regular.ttf</font> + </family> + <family lang="und-Modi"> + <font weight="400" style="normal">NotoSansModi-Regular.ttf</font> + </family> + <family lang="und-Dogr"> + <font weight="400" style="normal">NotoSerifDogra-Regular.ttf</font> + </family> + <family lang="und-Medf"> + <font weight="400" style="normal" postScriptName="NotoSansMedefaidrin-Regular"> + NotoSansMedefaidrin-VF.ttf + <axis tag="wght" stylevalue="400"/> + </font> + <font weight="500" style="normal" postScriptName="NotoSansMedefaidrin-Regular"> + NotoSansMedefaidrin-VF.ttf + <axis tag="wght" stylevalue="500"/> + </font> + <font weight="600" style="normal" postScriptName="NotoSansMedefaidrin-Regular"> + NotoSansMedefaidrin-VF.ttf + <axis tag="wght" stylevalue="600"/> + </font> + <font weight="700" style="normal" postScriptName="NotoSansMedefaidrin-Regular"> + NotoSansMedefaidrin-VF.ttf + <axis tag="wght" stylevalue="700"/> + </font> + </family> + <family lang="und-Soyo"> + <font weight="400" style="normal" postScriptName="NotoSansSoyombo-Regular"> + NotoSansSoyombo-VF.ttf + <axis tag="wght" stylevalue="400"/> + </font> + <font weight="500" style="normal" postScriptName="NotoSansSoyombo-Regular"> + NotoSansSoyombo-VF.ttf + <axis tag="wght" stylevalue="500"/> + </font> + <font weight="600" style="normal" postScriptName="NotoSansSoyombo-Regular"> + NotoSansSoyombo-VF.ttf + <axis tag="wght" stylevalue="600"/> + </font> + <font weight="700" style="normal" postScriptName="NotoSansSoyombo-Regular"> + NotoSansSoyombo-VF.ttf + <axis tag="wght" stylevalue="700"/> + </font> + </family> + <family lang="und-Takr"> + <font weight="400" style="normal" postScriptName="NotoSansTakri-Regular"> + NotoSansTakri-VF.ttf + <axis tag="wght" stylevalue="400"/> + </font> + <font weight="500" style="normal" postScriptName="NotoSansTakri-Regular"> + NotoSansTakri-VF.ttf + <axis tag="wght" stylevalue="500"/> + </font> + <font weight="600" style="normal" postScriptName="NotoSansTakri-Regular"> + NotoSansTakri-VF.ttf + <axis tag="wght" stylevalue="600"/> + </font> + <font weight="700" style="normal" postScriptName="NotoSansTakri-Regular"> + NotoSansTakri-VF.ttf + <axis tag="wght" stylevalue="700"/> + </font> + </family> + <family lang="und-Hmnp"> + <font weight="400" style="normal" postScriptName="NotoSerifHmongNyiakeng-Regular"> + NotoSerifNyiakengPuachueHmong-VF.ttf + <axis tag="wght" stylevalue="400"/> + </font> + <font weight="500" style="normal" postScriptName="NotoSerifHmongNyiakeng-Regular"> + NotoSerifNyiakengPuachueHmong-VF.ttf + <axis tag="wght" stylevalue="500"/> + </font> + <font weight="600" style="normal" postScriptName="NotoSerifHmongNyiakeng-Regular"> + NotoSerifNyiakengPuachueHmong-VF.ttf + <axis tag="wght" stylevalue="600"/> + </font> + <font weight="700" style="normal" postScriptName="NotoSerifHmongNyiakeng-Regular"> + NotoSerifNyiakengPuachueHmong-VF.ttf + <axis tag="wght" stylevalue="700"/> + </font> + </family> + <family lang="und-Yezi"> + <font weight="400" style="normal" postScriptName="NotoSerifYezidi-Regular"> + NotoSerifYezidi-VF.ttf + <axis tag="wght" stylevalue="400"/> + </font> + <font weight="500" style="normal" postScriptName="NotoSerifYezidi-Regular"> + NotoSerifYezidi-VF.ttf + <axis tag="wght" stylevalue="500"/> + </font> + <font weight="600" style="normal" postScriptName="NotoSerifYezidi-Regular"> + NotoSerifYezidi-VF.ttf + <axis tag="wght" stylevalue="600"/> + </font> + <font weight="700" style="normal" postScriptName="NotoSerifYezidi-Regular"> + NotoSerifYezidi-VF.ttf + <axis tag="wght" stylevalue="700"/> + </font> + </family> +</familyset> diff --git a/framework-jarjar-rules.txt b/framework-jarjar-rules.txt index 03b268d87d01..6339a8703f01 100644 --- a/framework-jarjar-rules.txt +++ b/framework-jarjar-rules.txt @@ -8,3 +8,6 @@ rule com.android.server.vcn.util.** com.android.server.vcn.repackaged.util.@1 # for modules-utils-build dependency rule com.android.modules.utils.build.** android.internal.modules.utils.build.@1 + +# For Perfetto proto dependencies +rule perfetto.protos.** android.internal.perfetto.protos.@1 diff --git a/graphics/java/android/graphics/Paint.java b/graphics/java/android/graphics/Paint.java index c5a2f983ae00..ae61a2d811d2 100644 --- a/graphics/java/android/graphics/Paint.java +++ b/graphics/java/android/graphics/Paint.java @@ -17,6 +17,7 @@ package android.graphics; import static com.android.text.flags.Flags.FLAG_FIX_LINE_HEIGHT_FOR_LOCALE; +import static com.android.text.flags.Flags.FLAG_INTER_CHARACTER_JUSTIFICATION; import android.annotation.ColorInt; import android.annotation.ColorLong; @@ -65,8 +66,6 @@ public class Paint { private long mNativeShader; private long mNativeColorFilter; - private static boolean sIsRobolectric = Build.FINGERPRINT.equals("robolectric"); - // Use a Holder to allow static initialization of Paint in the boot image. private static class NoImagePreloadHolder { public static final NativeAllocationRegistry sRegistry = @@ -135,7 +134,9 @@ public class Paint { FAKE_BOLD_TEXT_FLAG, LINEAR_TEXT_FLAG, SUBPIXEL_TEXT_FLAG, - EMBEDDED_BITMAP_TEXT_FLAG + EMBEDDED_BITMAP_TEXT_FLAG, + TEXT_RUN_FLAG_LEFT_EDGE, + TEXT_RUN_FLAG_RIGHT_EDGE }) public @interface PaintFlag{} @@ -266,6 +267,66 @@ public class Paint { /** @hide bit mask for the flag enabling vertical rendering for text */ public static final int VERTICAL_TEXT_FLAG = 0x1000; + /** + * A text run flag that indicates the run is located the visually most left segment of the line. + * <p> + * This flag is used for telling the underlying text layout engine that the text is located at + * the most left of the line. This flag is used for controlling the amount letter spacing + * added. If the text is in the middle of the line, the text layout engine assigns additional + * letter spacing to the both side of each letter. On the other hand, the letter spacing should + * not be added to the visually most left and right of the line. By setting this flag, text + * layout engine calculates the layout as it is located at the most visually left of the line + * and doesn't add letter spacing to the left of this run. + * <p> + * Note that the caller must resolve BiDi runs and reorder them visually and set this flag only + * if the target run is located visually most left position. This left does not always mean the + * beginning of the text. + * <p> + * If the run covers entire line, caller should set {@link #TEXT_RUN_FLAG_RIGHT_EDGE} as well. + * <p> + * Note that this flag is only effective for run based APIs. For example, this flag works for + * {@link Canvas#drawTextRun(CharSequence, int, int, int, int, float, float, boolean, Paint)} + * and + * {@link Paint#getRunCharacterAdvance(char[], int, int, int, int, boolean, int, float[], int)}. + * However, this doesn't work for + * {@link Canvas#drawText(CharSequence, int, int, float, float, Paint)} or + * {@link Paint#measureText(CharSequence, int, int)}. The non-run based APIs works as both + * {@link #TEXT_RUN_FLAG_LEFT_EDGE} and {@link #TEXT_RUN_FLAG_RIGHT_EDGE} are specified. + */ + @FlaggedApi(FLAG_INTER_CHARACTER_JUSTIFICATION) + public static final int TEXT_RUN_FLAG_LEFT_EDGE = 0x2000; + + + /** + * A text run flag that indicates the run is located the visually most right segment of the + * line. + * <p> + * This flag is used for telling the underlying text layout engine that the text is located at + * the most right of the line. This flag is used for controlling the amount letter spacing + * added. If the text is in the middle of the line, the text layout engine assigns additional + * letter spacing to the both side of each letter. On the other hand, the letter spacing should + * not be added to the visually most left and right of the line. By setting this flag, text + * layout engine calculates the layout as it is located at the most visually left of the line + * and doesn't add letter spacing to the left of this run. + * <p> + * Note that the caller must resolve BiDi runs and reorder them visually and set this flag only + * if the target run is located visually most right position. This right does not always mean + * the end of the text. + * <p> + * If the run covers entire line, caller should set {@link #TEXT_RUN_FLAG_LEFT_EDGE} as well. + * <p> + * Note that this flag is only effective for run based APIs. For example, this flag works for + * {@link Canvas#drawTextRun(CharSequence, int, int, int, int, float, float, boolean, Paint)} + * and + * {@link Paint#getRunCharacterAdvance(char[], int, int, int, int, boolean, int, float[], int)}. + * However, this doesn't work for + * {@link Canvas#drawText(CharSequence, int, int, float, float, Paint)} or + * {@link Paint#measureText(CharSequence, int, int)}. The non-run based APIs works as both + * {@link #TEXT_RUN_FLAG_LEFT_EDGE} and {@link #TEXT_RUN_FLAG_RIGHT_EDGE} are specified. + */ + @FlaggedApi(FLAG_INTER_CHARACTER_JUSTIFICATION) + public static final int TEXT_RUN_FLAG_RIGHT_EDGE = 0x4000; + // These flags are always set on a new/reset paint, even if flags 0 is passed. static final int HIDDEN_DEFAULT_PAINT_FLAGS = DEV_KERN_TEXT_FLAG | EMBEDDED_BITMAP_TEXT_FLAG | FILTER_BITMAP_FLAG; @@ -2522,17 +2583,24 @@ public class Paint { if (text.length == 0 || count == 0) { return 0f; } - if (!mHasCompatScaling) { - return (float) Math.ceil(nGetTextAdvances(mNativePaint, text, - index, count, index, count, mBidiFlags, null, 0)); - } + int oldFlag = getFlags(); + setFlags(getFlags() | (TEXT_RUN_FLAG_LEFT_EDGE | TEXT_RUN_FLAG_RIGHT_EDGE)); + try { - final float oldSize = getTextSize(); - setTextSize(oldSize * mCompatScaling); - final float w = nGetTextAdvances(mNativePaint, text, index, count, index, count, - mBidiFlags, null, 0); - setTextSize(oldSize); - return (float) Math.ceil(w*mInvCompatScaling); + if (!mHasCompatScaling) { + return (float) Math.ceil(nGetTextAdvances(mNativePaint, text, + index, count, index, count, mBidiFlags, null, 0)); + } + + final float oldSize = getTextSize(); + setTextSize(oldSize * mCompatScaling); + final float w = nGetTextAdvances(mNativePaint, text, index, count, index, count, + mBidiFlags, null, 0); + setTextSize(oldSize); + return (float) Math.ceil(w * mInvCompatScaling); + } finally { + setFlags(oldFlag); + } } /** @@ -2554,16 +2622,22 @@ public class Paint { if (text.length() == 0 || start == end) { return 0f; } - if (!mHasCompatScaling) { - return (float) Math.ceil(nGetTextAdvances(mNativePaint, text, - start, end, start, end, mBidiFlags, null, 0)); + int oldFlag = getFlags(); + setFlags(getFlags() | (TEXT_RUN_FLAG_LEFT_EDGE | TEXT_RUN_FLAG_RIGHT_EDGE)); + try { + if (!mHasCompatScaling) { + return (float) Math.ceil(nGetTextAdvances(mNativePaint, text, + start, end, start, end, mBidiFlags, null, 0)); + } + final float oldSize = getTextSize(); + setTextSize(oldSize * mCompatScaling); + final float w = nGetTextAdvances(mNativePaint, text, start, end, start, end, mBidiFlags, + null, 0); + setTextSize(oldSize); + return (float) Math.ceil(w * mInvCompatScaling); + } finally { + setFlags(oldFlag); } - final float oldSize = getTextSize(); - setTextSize(oldSize * mCompatScaling); - final float w = nGetTextAdvances(mNativePaint, text, start, end, start, end, mBidiFlags, - null, 0); - setTextSize(oldSize); - return (float) Math.ceil(w * mInvCompatScaling); } /** @@ -2768,19 +2842,26 @@ public class Paint { if (text.length == 0 || count == 0) { return 0; } - if (!mHasCompatScaling) { + int oldFlag = getFlags(); + setFlags(getFlags() | (TEXT_RUN_FLAG_LEFT_EDGE | TEXT_RUN_FLAG_RIGHT_EDGE)); + try { + if (!mHasCompatScaling) { + nGetTextAdvances(mNativePaint, text, index, count, index, count, mBidiFlags, widths, + 0); + return count; + } + + final float oldSize = getTextSize(); + setTextSize(oldSize * mCompatScaling); nGetTextAdvances(mNativePaint, text, index, count, index, count, mBidiFlags, widths, 0); + setTextSize(oldSize); + for (int i = 0; i < count; i++) { + widths[i] *= mInvCompatScaling; + } return count; + } finally { + setFlags(oldFlag); } - - final float oldSize = getTextSize(); - setTextSize(oldSize * mCompatScaling); - nGetTextAdvances(mNativePaint, text, index, count, index, count, mBidiFlags, widths, 0); - setTextSize(oldSize); - for (int i = 0; i < count; i++) { - widths[i] *= mInvCompatScaling; - } - return count; } /** @@ -2851,19 +2932,25 @@ public class Paint { if (text.length() == 0 || start == end) { return 0; } - if (!mHasCompatScaling) { + int oldFlag = getFlags(); + setFlags(getFlags() | (TEXT_RUN_FLAG_LEFT_EDGE | TEXT_RUN_FLAG_RIGHT_EDGE)); + try { + if (!mHasCompatScaling) { + nGetTextAdvances(mNativePaint, text, start, end, start, end, mBidiFlags, widths, 0); + return end - start; + } + + final float oldSize = getTextSize(); + setTextSize(oldSize * mCompatScaling); nGetTextAdvances(mNativePaint, text, start, end, start, end, mBidiFlags, widths, 0); + setTextSize(oldSize); + for (int i = 0; i < end - start; i++) { + widths[i] *= mInvCompatScaling; + } return end - start; + } finally { + setFlags(oldFlag); } - - final float oldSize = getTextSize(); - setTextSize(oldSize * mCompatScaling); - nGetTextAdvances(mNativePaint, text, start, end, start, end, mBidiFlags, widths, 0); - setTextSize(oldSize); - for (int i = 0; i < end - start; i++) { - widths[i] *= mInvCompatScaling; - } - return end - start; } /** @@ -3393,13 +3480,8 @@ public class Paint { return 0.0f; } - if (sIsRobolectric) { - return nGetRunCharacterAdvance(mNativePaint, text, start, end, - contextStart, contextEnd, isRtl, offset, advances, advancesIndex, drawBounds); - } else { - return nGetRunCharacterAdvance(mNativePaint, text, start, end, contextStart, contextEnd, - isRtl, offset, advances, advancesIndex, drawBounds, runInfo); - } + return nGetRunCharacterAdvance(mNativePaint, text, start, end, contextStart, contextEnd, + isRtl, offset, advances, advancesIndex, drawBounds, runInfo); } /** diff --git a/graphics/java/android/graphics/text/LineBreakConfig.java b/graphics/java/android/graphics/text/LineBreakConfig.java index ddae673e1084..b21bf11088e2 100644 --- a/graphics/java/android/graphics/text/LineBreakConfig.java +++ b/graphics/java/android/graphics/text/LineBreakConfig.java @@ -23,9 +23,7 @@ import android.annotation.FlaggedApi; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; -import android.app.compat.CompatChanges; -import android.compat.annotation.ChangeId; -import android.compat.annotation.EnabledSince; +import android.app.ActivityThread; import android.os.Build; import android.os.LocaleList; import android.os.Parcel; @@ -43,15 +41,6 @@ import java.util.Objects; * line-break property</a> for more information. */ public final class LineBreakConfig implements Parcelable { - - /** - * A feature ID for automatic line break word style. - * @hide - */ - @ChangeId - @EnabledSince(targetSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM) - public static final long WORD_STYLE_AUTO = 280005585L; - /** * No hyphenation preference is specified. * @@ -487,8 +476,15 @@ public final class LineBreakConfig implements Parcelable { * @hide */ public static @LineBreakStyle int getResolvedLineBreakStyle(@Nullable LineBreakConfig config) { - final int defaultStyle = CompatChanges.isChangeEnabled(WORD_STYLE_AUTO) - ? LINE_BREAK_STYLE_AUTO : LINE_BREAK_STYLE_NONE; + final int targetSdkVersion = ActivityThread.currentApplication().getApplicationInfo() + .targetSdkVersion; + final int defaultStyle; + final int vicVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM; + if (targetSdkVersion >= vicVersion) { + defaultStyle = LINE_BREAK_STYLE_AUTO; + } else { + defaultStyle = LINE_BREAK_STYLE_NONE; + } if (config == null) { return defaultStyle; } @@ -515,8 +511,15 @@ public final class LineBreakConfig implements Parcelable { */ public static @LineBreakWordStyle int getResolvedLineBreakWordStyle( @Nullable LineBreakConfig config) { - final int defaultWordStyle = CompatChanges.isChangeEnabled(WORD_STYLE_AUTO) - ? LINE_BREAK_WORD_STYLE_AUTO : LINE_BREAK_WORD_STYLE_NONE; + final int targetSdkVersion = ActivityThread.currentApplication().getApplicationInfo() + .targetSdkVersion; + final int defaultWordStyle; + final int vicVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM; + if (targetSdkVersion >= vicVersion) { + defaultWordStyle = LINE_BREAK_WORD_STYLE_AUTO; + } else { + defaultWordStyle = LINE_BREAK_WORD_STYLE_NONE; + } if (config == null) { return defaultWordStyle; } diff --git a/graphics/java/android/graphics/text/MeasuredText.java b/graphics/java/android/graphics/text/MeasuredText.java index 2d33e8d24ece..6da07198c3ad 100644 --- a/graphics/java/android/graphics/text/MeasuredText.java +++ b/graphics/java/android/graphics/text/MeasuredText.java @@ -269,6 +269,10 @@ public class MeasuredText { * offset is zero. After the style is applied the internal offset is moved to {@code offset * + length}, and next call will start from this new position. * + * <p> + * {@link Paint#TEXT_RUN_FLAG_RIGHT_EDGE} and {@link Paint#TEXT_RUN_FLAG_LEFT_EDGE} are + * ignored and treated as both of them are set. + * * @param paint a paint * @param length a length to be applied with a given paint, can not exceed the length of the * text 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 066f38b61eec..83d555cbdecd 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java @@ -822,11 +822,6 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen // Checks if container should be updated before apply new parentInfo. final boolean shouldUpdateContainer = taskContainer.shouldUpdateContainer(parentInfo); taskContainer.updateTaskFragmentParentInfo(parentInfo); - if (!taskContainer.isVisible()) { - // Don't update containers if the task is not visible. We only update containers when - // parentInfo#isVisibleRequested is true. - return; - } // If the last direct activity of the host task is dismissed and the overlay container is // the only taskFragment, the overlay container should also be dismissed. 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 bc921010b469..4e7b76057b5d 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 @@ -475,8 +475,10 @@ public class OverlayPresentationTest { @Test public void testOnTaskFragmentParentInfoChanged_positionOnlyChange_earlyReturn() { final TaskFragmentContainer overlayContainer = createTestOverlayContainer(TASK_ID, "test"); - final TaskContainer taskContainer = overlayContainer.getTaskContainer(); + + assertThat(taskContainer.getOverlayContainer()).isEqualTo(overlayContainer); + spyOn(taskContainer); final TaskContainer.TaskProperties taskProperties = taskContainer.getTaskProperties(); final TaskFragmentParentInfo parentInfo = new TaskFragmentParentInfo( @@ -495,6 +497,30 @@ public class OverlayPresentationTest { .that(taskContainer.getOverlayContainer()).isNull(); } + @Test + public void testOnTaskFragmentParentInfoChanged_invisibleTask_callDismissOverlayContainer() { + final TaskFragmentContainer overlayContainer = createTestOverlayContainer(TASK_ID, "test"); + final TaskContainer taskContainer = overlayContainer.getTaskContainer(); + + assertThat(taskContainer.getOverlayContainer()).isEqualTo(overlayContainer); + + spyOn(taskContainer); + final TaskContainer.TaskProperties taskProperties = taskContainer.getTaskProperties(); + final TaskFragmentParentInfo parentInfo = new TaskFragmentParentInfo( + new Configuration(taskProperties.getConfiguration()), taskProperties.getDisplayId(), + false /* visible */, false /* hasDirectActivity */, null /* decorSurface */); + + mSplitController.onTaskFragmentParentInfoChanged(mTransaction, TASK_ID, parentInfo); + + // The parent info must be applied to the task container + verify(taskContainer).updateTaskFragmentParentInfo(parentInfo); + verify(mSplitController, never()).updateContainer(any(), any()); + + assertWithMessage("The overlay container must still be dismissed even if " + + "#updateContainer is not called") + .that(taskContainer.getOverlayContainer()).isNull(); + } + /** * A simplified version of {@link SplitController.ActivityStartMonitor * #createOrUpdateOverlayTaskFragmentIfNeeded} diff --git a/libs/WindowManager/Shell/Android.bp b/libs/WindowManager/Shell/Android.bp index 45540e0fbbb8..4cdc06a999a7 100644 --- a/libs/WindowManager/Shell/Android.bp +++ b/libs/WindowManager/Shell/Android.bp @@ -162,6 +162,7 @@ android_library { "com_android_wm_shell_flags_lib", "com.android.window.flags.window-aconfig-java", "WindowManager-Shell-proto", + "perfetto_trace_java_protos", "dagger2", "jsr330", ], diff --git a/libs/WindowManager/Shell/aconfig/multitasking.aconfig b/libs/WindowManager/Shell/aconfig/multitasking.aconfig index 901d5fa0cd9a..0e046581cb48 100644 --- a/libs/WindowManager/Shell/aconfig/multitasking.aconfig +++ b/libs/WindowManager/Shell/aconfig/multitasking.aconfig @@ -1,13 +1,6 @@ package: "com.android.wm.shell" flag { - name: "example_flag" - namespace: "multitasking" - description: "An Example Flag" - bug: "300136750" -} - -flag { name: "enable_app_pairs" namespace: "multitasking" description: "Enables the ability to create and save app pairs to the Home screen" diff --git a/libs/WindowManager/Shell/res/values/config.xml b/libs/WindowManager/Shell/res/values/config.xml index e4abae48c8fd..9854e58dd7cf 100644 --- a/libs/WindowManager/Shell/res/values/config.xml +++ b/libs/WindowManager/Shell/res/values/config.xml @@ -134,6 +134,13 @@ <!-- Whether the additional education about reachability is enabled --> <bool name="config_letterboxIsReachabilityEducationEnabled">false</bool> + <!-- The minimum tolerance of the percentage of activity bounds within its task to hide + size compat restart button. Value lower than 0 or higher than 100 will be ignored. + 100 is the default value where the activity has to fit exactly within the task to allow + size compat restart button to be hidden. 0 means size compat restart button will always + be hidden. --> + <integer name="config_letterboxRestartButtonHideTolerance">100</integer> + <!-- Whether DragAndDrop capability is enabled --> <bool name="config_enableShellDragDrop">true</bool> diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java index bb433dbbd2ce..e7f6f0d61847 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java @@ -17,7 +17,7 @@ package com.android.wm.shell.back; import static com.android.internal.jank.InteractionJankMonitor.CUJ_PREDICTIVE_BACK_HOME; -import static com.android.window.flags.Flags.predictiveBackSystemAnimations; +import static com.android.window.flags.Flags.predictiveBackSystemAnims; import static com.android.wm.shell.common.ExecutorUtils.executeRemoteCallWithTaskPermission; import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_BACK_PREVIEW; import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_BACK_ANIMATION; @@ -244,7 +244,7 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont private void setupAnimationDeveloperSettingsObserver( @NonNull ContentResolver contentResolver, @NonNull @ShellBackgroundThread final Handler backgroundHandler) { - if (predictiveBackSystemAnimations()) { + if (predictiveBackSystemAnims()) { ProtoLog.d(WM_SHELL_BACK_PREVIEW, "Back animation aconfig flag is enabled, therefore " + "developer settings flag is ignored and no content observer registered"); return; @@ -267,7 +267,7 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont */ @ShellBackgroundThread private void updateEnableAnimationFromFlags() { - boolean isEnabled = predictiveBackSystemAnimations() || isDeveloperSettingEnabled(); + boolean isEnabled = predictiveBackSystemAnims() || isDeveloperSettingEnabled(); mEnableAnimations.set(isEnabled); ProtoLog.d(WM_SHELL_BACK_PREVIEW, "Back animation enabled=%s", isEnabled); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/StackAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/StackAnimationController.java index e48732801094..bb0dd95b042f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/StackAnimationController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/StackAnimationController.java @@ -769,8 +769,10 @@ public class StackAnimationController extends boolean swapped = false; for (int newIndex = 0; newIndex < bubbleViews.size(); newIndex++) { View view = bubbleViews.get(newIndex); - final int oldIndex = mLayout.indexOfChild(view); - swapped |= animateSwap(view, oldIndex, newIndex, updateAllIcons, after); + if (view != null) { + final int oldIndex = mLayout.indexOfChild(view); + swapped |= animateSwap(view, oldIndex, newIndex, updateAllIcons, after); + } } if (!swapped) { // All bubbles were at the right position. Make sure badges and z order is correct. diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java index 12114519d086..bd8ce803c591 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java @@ -26,6 +26,7 @@ import android.graphics.Point; import android.graphics.Rect; import android.graphics.Region; import android.graphics.drawable.ColorDrawable; +import android.view.Gravity; import android.view.TouchDelegate; import android.view.View; import android.view.ViewTreeObserver; @@ -74,10 +75,6 @@ public class BubbleBarLayerView extends FrameLayout private DismissView mDismissView; private @Nullable Consumer<String> mUnBubbleConversationCallback; - // TODO(b/273310265) - currently the view is always on the right, need to update for RTL. - /** Whether the expanded view is displaying on the left of the screen or not. */ - private boolean mOnLeft = false; - /** Whether a bubble is expanded. */ private boolean mIsExpanded = false; @@ -154,10 +151,10 @@ public class BubbleBarLayerView extends FrameLayout return mIsExpanded; } - // (TODO: b/273310265): BubblePositioner should be source of truth when this work is done. + // TODO(b/313661121) - when dragging is implemented, check user setting first /** Whether the expanded view is positioned on the left or right side of the screen. */ public boolean isOnLeft() { - return mOnLeft; + return getLayoutDirection() == LAYOUT_DIRECTION_RTL; } /** Shows the expanded view of the provided bubble. */ @@ -216,7 +213,7 @@ public class BubbleBarLayerView extends FrameLayout return Unit.INSTANCE; }); - addView(mExpandedView, new FrameLayout.LayoutParams(width, height)); + addView(mExpandedView, new LayoutParams(width, height, Gravity.LEFT)); } if (mEducationViewController.isEducationVisible()) { @@ -311,7 +308,7 @@ public class BubbleBarLayerView extends FrameLayout lp.width = width; lp.height = height; mExpandedView.setLayoutParams(lp); - if (mOnLeft) { + if (isOnLeft()) { mExpandedView.setX(mPositioner.getInsets().left + padding); } else { mExpandedView.setX(mPositioner.getAvailableRect().width() - width - padding); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIConfiguration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIConfiguration.java index 09d99b204bdb..fa2e23647a39 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIConfiguration.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIConfiguration.java @@ -71,6 +71,8 @@ public class CompatUIConfiguration implements DeviceConfig.OnPropertiesChangedLi private static final String HAS_SEEN_VERTICAL_REACHABILITY_EDUCATION_KEY_PREFIX = "has_seen_vertical_reachability_education"; + private static final int MAX_PERCENTAGE_VAL = 100; + /** * The {@link SharedPreferences} instance for the restart dialog and the reachability * education. @@ -82,6 +84,12 @@ public class CompatUIConfiguration implements DeviceConfig.OnPropertiesChangedLi */ private final SharedPreferences mLetterboxEduSharedPreferences; + /** + * The minimum tolerance of the percentage of activity bounds within its task to hide + * size compat restart button. + */ + private final int mHideSizeCompatRestartButtonTolerance; + // Whether the extended restart dialog is enabled private boolean mIsRestartDialogEnabled; @@ -106,6 +114,9 @@ public class CompatUIConfiguration implements DeviceConfig.OnPropertiesChangedLi R.bool.config_letterboxIsRestartDialogEnabled); mIsReachabilityEducationEnabled = context.getResources().getBoolean( R.bool.config_letterboxIsReachabilityEducationEnabled); + final int tolerance = context.getResources().getInteger( + R.integer.config_letterboxRestartButtonHideTolerance); + mHideSizeCompatRestartButtonTolerance = getHideSizeCompatRestartButtonTolerance(tolerance); mIsLetterboxRestartDialogAllowed = DeviceConfig.getBoolean( DeviceConfig.NAMESPACE_WINDOW_MANAGER, KEY_ENABLE_LETTERBOX_RESTART_DIALOG, DEFAULT_VALUE_ENABLE_LETTERBOX_RESTART_DIALOG); @@ -179,6 +190,10 @@ public class CompatUIConfiguration implements DeviceConfig.OnPropertiesChangedLi || !hasSeenVerticalReachabilityEducation(taskInfo)); } + int getHideSizeCompatRestartButtonTolerance() { + return mHideSizeCompatRestartButtonTolerance; + } + boolean getHasSeenLetterboxEducation(int userId) { return mLetterboxEduSharedPreferences .getBoolean(dontShowLetterboxEduKey(userId), /* default= */ false); @@ -218,6 +233,15 @@ public class CompatUIConfiguration implements DeviceConfig.OnPropertiesChangedLi } } + // Returns the minimum tolerance of the percentage of activity bounds within its task to hide + // size compat restart button. Value lower than 0 or higher than 100 will be ignored. + // 100 is the default value where the activity has to fit exactly within the task to allow + // size compat restart button to be hidden. 0 means size compat restart button will always + // be hidden. + private int getHideSizeCompatRestartButtonTolerance(int tolerance) { + return tolerance < 0 || tolerance > MAX_PERCENTAGE_VAL ? MAX_PERCENTAGE_VAL : tolerance; + } + private boolean isReachabilityEducationEnabled() { return mIsReachabilityEducationOverrideEnabled || (mIsReachabilityEducationEnabled && mIsLetterboxReachabilityEducationAllowed); 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 00e0cdb034b6..2dd27430e348 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 @@ -22,7 +22,9 @@ import static android.app.AppCompatTaskInfo.CAMERA_COMPAT_CONTROL_TREATMENT_APPL import static android.app.AppCompatTaskInfo.CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED; import static android.window.TaskConstants.TASK_CHILD_LAYER_COMPAT_UI; +import android.annotation.NonNull; import android.annotation.Nullable; +import android.app.AppCompatTaskInfo; import android.app.AppCompatTaskInfo.CameraCompatControlState; import android.app.TaskInfo; import android.content.Context; @@ -33,6 +35,7 @@ import android.view.LayoutInflater; import android.view.View; import com.android.internal.annotations.VisibleForTesting; +import com.android.window.flags.Flags; import com.android.wm.shell.R; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.common.DisplayLayout; @@ -68,6 +71,8 @@ class CompatUIWindowManager extends CompatUIWindowManagerAbstract { @VisibleForTesting CompatUILayout mLayout; + private final float mHideScmTolerance; + CompatUIWindowManager(Context context, TaskInfo taskInfo, SyncTransactionQueue syncQueue, CompatUICallback callback, ShellTaskOrganizer.TaskListener taskListener, DisplayLayout displayLayout, @@ -75,11 +80,13 @@ class CompatUIWindowManager extends CompatUIWindowManagerAbstract { Consumer<Pair<TaskInfo, ShellTaskOrganizer.TaskListener>> onRestartButtonClicked) { super(context, taskInfo, syncQueue, taskListener, displayLayout); mCallback = callback; - mHasSizeCompat = taskInfo.appCompatTaskInfo.topActivityInSizeCompat; + mHasSizeCompat = taskInfo.appCompatTaskInfo.topActivityInSizeCompat + && shouldShowSizeCompatRestartButton(taskInfo); mCameraCompatControlState = taskInfo.appCompatTaskInfo.cameraCompatControlState; mCompatUIHintsState = compatUIHintsState; mCompatUIConfiguration = compatUIConfiguration; mOnRestartButtonClicked = onRestartButtonClicked; + mHideScmTolerance = mCompatUIConfiguration.getHideSizeCompatRestartButtonTolerance(); } @Override @@ -107,6 +114,11 @@ class CompatUIWindowManager extends CompatUIWindowManagerAbstract { mLayout = inflateLayout(); mLayout.inject(this); + final TaskInfo taskInfo = getLastTaskInfo(); + if (taskInfo != null) { + mHasSizeCompat = mHasSizeCompat && shouldShowSizeCompatRestartButton(taskInfo); + } + updateVisibilityOfViews(); if (mHasSizeCompat) { @@ -127,7 +139,8 @@ class CompatUIWindowManager extends CompatUIWindowManagerAbstract { boolean canShow) { final boolean prevHasSizeCompat = mHasSizeCompat; final int prevCameraCompatControlState = mCameraCompatControlState; - mHasSizeCompat = taskInfo.appCompatTaskInfo.topActivityInSizeCompat; + mHasSizeCompat = taskInfo.appCompatTaskInfo.topActivityInSizeCompat + && shouldShowSizeCompatRestartButton(taskInfo); mCameraCompatControlState = taskInfo.appCompatTaskInfo.cameraCompatControlState; if (!super.updateCompatInfo(taskInfo, taskListener, canShow)) { @@ -208,6 +221,30 @@ class CompatUIWindowManager extends CompatUIWindowManagerAbstract { updateSurfacePosition(positionX, positionY); } + @VisibleForTesting + boolean shouldShowSizeCompatRestartButton(@NonNull TaskInfo taskInfo) { + if (!Flags.allowHideScmButton()) { + return true; + } + final AppCompatTaskInfo appCompatTaskInfo = taskInfo.appCompatTaskInfo; + final Rect taskBounds = taskInfo.configuration.windowConfiguration.getBounds(); + final int letterboxArea = computeArea(appCompatTaskInfo.topActivityLetterboxWidth, + appCompatTaskInfo.topActivityLetterboxHeight); + final int taskArea = computeArea(taskBounds.width(), taskBounds.height()); + if (letterboxArea == 0 || taskArea == 0) { + return false; + } + final float percentageAreaOfLetterboxInTask = (float) letterboxArea / taskArea * 100; + return percentageAreaOfLetterboxInTask < mHideScmTolerance; + } + + private int computeArea(int width, int height) { + if (width == 0 || height == 0) { + return 0; + } + return width * height; + } + private void updateVisibilityOfViews() { if (mLayout == null) { return; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipNotificationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipNotificationController.java index 1c94625ddde9..54e162bba2f3 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipNotificationController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipNotificationController.java @@ -54,6 +54,7 @@ public class TvPipNotificationController implements TvPipActionsProvider.Listene // Referenced in com.android.systemui.util.NotificationChannels. public static final String NOTIFICATION_CHANNEL = "TVPIP"; private static final String NOTIFICATION_TAG = "TvPip"; + private static final String EXTRA_COMPONENT_NAME = "TvPipComponentName"; private final Context mContext; private final PackageManager mPackageManager; @@ -176,6 +177,7 @@ public class TvPipNotificationController implements TvPipActionsProvider.Listene Bundle extras = new Bundle(); extras.putParcelable(Notification.EXTRA_MEDIA_SESSION, mMediaSessionToken); + extras.putParcelable(EXTRA_COMPONENT_NAME, PipUtils.getTopPipActivity(mContext).first); mNotificationBuilder.setExtras(extras); PendingIntent closeIntent = mTvPipActionsProvider.getCloseAction().getPendingIntent(); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/TaskSnapshotWindow.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/TaskSnapshotWindow.java index e6418f35a0b1..1a0c011205fb 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/TaskSnapshotWindow.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/TaskSnapshotWindow.java @@ -135,7 +135,6 @@ public class TaskSnapshotWindow { } catch (RemoteException e) { snapshotSurface.clearWindowSynced(); } - window.setOuter(snapshotSurface); try { Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "TaskSnapshot#relayout"); session.relayout(window, layoutParams, -1, -1, View.VISIBLE, 0, 0, 0, @@ -161,7 +160,7 @@ public class TaskSnapshotWindow { ShellExecutor splashScreenExecutor) { mSplashScreenExecutor = splashScreenExecutor; mSession = WindowManagerGlobal.getWindowSession(); - mWindow = new Window(); + mWindow = new Window(this); mWindow.setSession(mSession); int backgroundColor = taskDescription.getBackgroundColor(); mBackgroundPaint.setColor(backgroundColor != 0 ? backgroundColor : WHITE); @@ -204,9 +203,9 @@ public class TaskSnapshotWindow { } static class Window extends BaseIWindow { - private WeakReference<TaskSnapshotWindow> mOuter; + private final WeakReference<TaskSnapshotWindow> mOuter; - public void setOuter(TaskSnapshotWindow outer) { + Window(TaskSnapshotWindow outer) { mOuter = new WeakReference<>(outer); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java index bf783e6af36f..8c2203ef7a49 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java @@ -21,17 +21,9 @@ import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; import static android.view.Display.DEFAULT_DISPLAY; import static android.view.WindowManager.TRANSIT_CHANGE; -import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_UNOCCLUDING; import static android.view.WindowManager.TRANSIT_PIP; -import static android.view.WindowManager.TRANSIT_TO_BACK; import static android.window.TransitionInfo.FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY; -import static android.window.TransitionInfo.FLAG_IS_WALLPAPER; -import static com.android.wm.shell.common.split.SplitScreenConstants.FLAG_IS_DIVIDER_BAR; -import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_UNDEFINED; -import static com.android.wm.shell.pip.PipAnimationController.ANIM_TYPE_ALPHA; -import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_UNDEFINED; -import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_CHILD_TASK_ENTER_PIP; import static com.android.wm.shell.util.TransitionUtil.isOpeningType; import android.annotation.NonNull; @@ -56,7 +48,6 @@ import com.android.wm.shell.keyguard.KeyguardTransitionHandler; import com.android.wm.shell.pip.PipTransitionController; import com.android.wm.shell.protolog.ShellProtoLogGroup; import com.android.wm.shell.recents.RecentsTransitionHandler; -import com.android.wm.shell.splitscreen.SplitScreen; import com.android.wm.shell.splitscreen.SplitScreenController; import com.android.wm.shell.splitscreen.StageCoordinator; import com.android.wm.shell.sysui.ShellInit; @@ -84,7 +75,7 @@ public class DefaultMixedHandler implements Transitions.TransitionHandler, private UnfoldTransitionHandler mUnfoldHandler; private ActivityEmbeddingController mActivityEmbeddingController; - private static class MixedTransition { + abstract static class MixedTransition { static final int TYPE_ENTER_PIP_FROM_SPLIT = 1; /** Both the display and split-state (enter/exit) is changing */ @@ -124,15 +115,11 @@ public class DefaultMixedHandler implements Transitions.TransitionHandler, int mAnimType = ANIM_TYPE_DEFAULT; final IBinder mTransition; - private final Transitions mPlayer; - private final DefaultMixedHandler mMixedHandler; - private final PipTransitionController mPipHandler; - private final RecentsTransitionHandler mRecentsHandler; - private final StageCoordinator mSplitHandler; - private final KeyguardTransitionHandler mKeyguardHandler; - private final DesktopTasksController mDesktopTasksController; - private final UnfoldTransitionHandler mUnfoldHandler; - private final ActivityEmbeddingController mActivityEmbeddingController; + protected final Transitions mPlayer; + protected final DefaultMixedHandler mMixedHandler; + protected final PipTransitionController mPipHandler; + protected final StageCoordinator mSplitHandler; + protected final KeyguardTransitionHandler mKeyguardHandler; Transitions.TransitionHandler mLeftoversHandler = null; TransitionInfo mInfo = null; @@ -156,409 +143,33 @@ public class DefaultMixedHandler implements Transitions.TransitionHandler, MixedTransition(int type, IBinder transition, Transitions player, DefaultMixedHandler mixedHandler, PipTransitionController pipHandler, - RecentsTransitionHandler recentsHandler, StageCoordinator splitHandler, - KeyguardTransitionHandler keyguardHandler, - DesktopTasksController desktopTasksController, - UnfoldTransitionHandler unfoldHandler, - ActivityEmbeddingController activityEmbeddingController) { + StageCoordinator splitHandler, KeyguardTransitionHandler keyguardHandler) { mType = type; mTransition = transition; mPlayer = player; mMixedHandler = mixedHandler; mPipHandler = pipHandler; - mRecentsHandler = recentsHandler; mSplitHandler = splitHandler; mKeyguardHandler = keyguardHandler; - mDesktopTasksController = desktopTasksController; - mUnfoldHandler = unfoldHandler; - mActivityEmbeddingController = activityEmbeddingController; - - switch (type) { - case TYPE_RECENTS_DURING_DESKTOP: - case TYPE_RECENTS_DURING_KEYGUARD: - case TYPE_RECENTS_DURING_SPLIT: - mLeftoversHandler = mRecentsHandler; - break; - case TYPE_UNFOLD: - mLeftoversHandler = mUnfoldHandler; - break; - case TYPE_DISPLAY_AND_SPLIT_CHANGE: - case TYPE_ENTER_PIP_FROM_ACTIVITY_EMBEDDING: - case TYPE_ENTER_PIP_FROM_SPLIT: - case TYPE_KEYGUARD: - case TYPE_OPTIONS_REMOTE_AND_PIP_CHANGE: - default: - break; - } } - boolean startAnimation( + abstract boolean startAnimation( @NonNull IBinder transition, @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction startTransaction, @NonNull SurfaceControl.Transaction finishTransaction, - @NonNull Transitions.TransitionFinishCallback finishCallback) { - switch (mType) { - case TYPE_ENTER_PIP_FROM_SPLIT: - return animateEnterPipFromSplit(this, info, startTransaction, finishTransaction, - finishCallback, mPlayer, mMixedHandler, mPipHandler, mSplitHandler); - case TYPE_ENTER_PIP_FROM_ACTIVITY_EMBEDDING: - return animateEnterPipFromActivityEmbedding( - info, startTransaction, finishTransaction, finishCallback); - case TYPE_DISPLAY_AND_SPLIT_CHANGE: - return false; - case TYPE_OPTIONS_REMOTE_AND_PIP_CHANGE: - final boolean handledToPip = animateOpenIntentWithRemoteAndPip( - info, startTransaction, finishTransaction, finishCallback); - // Consume the transition on remote handler if the leftover handler already - // handle this transition. And if it cannot, the transition will be handled by - // remote handler, so don't consume here. - // Need to check leftOverHandler as it may change in - // #animateOpenIntentWithRemoteAndPip - if (handledToPip && mHasRequestToRemote - && mLeftoversHandler != mPlayer.getRemoteTransitionHandler()) { - mPlayer.getRemoteTransitionHandler().onTransitionConsumed( - transition, false, null); - } - return handledToPip; - case TYPE_RECENTS_DURING_SPLIT: - for (int i = info.getChanges().size() - 1; i >= 0; --i) { - final TransitionInfo.Change change = info.getChanges().get(i); - // Pip auto-entering info might be appended to recent transition like - // pressing home-key in 3-button navigation. This offers split handler the - // opportunity to handle split to pip animation. - if (mPipHandler.isEnteringPip(change, info.getType()) - && mSplitHandler.getSplitItemPosition(change.getLastParent()) - != SPLIT_POSITION_UNDEFINED) { - return animateEnterPipFromSplit( - this, info, startTransaction, finishTransaction, finishCallback, - mPlayer, mMixedHandler, mPipHandler, mSplitHandler); - } - } - - return animateRecentsDuringSplit( - info, startTransaction, finishTransaction, finishCallback); - case TYPE_KEYGUARD: - return animateKeyguard(this, info, startTransaction, finishTransaction, - finishCallback, mKeyguardHandler, mPipHandler); - case TYPE_RECENTS_DURING_KEYGUARD: - return animateRecentsDuringKeyguard( - info, startTransaction, finishTransaction, finishCallback); - case TYPE_RECENTS_DURING_DESKTOP: - return animateRecentsDuringDesktop( - info, startTransaction, finishTransaction, finishCallback); - case TYPE_UNFOLD: - return animateUnfold( - info, startTransaction, finishTransaction, finishCallback); - default: - throw new IllegalStateException( - "Starting mixed animation without a known mixed type? " + mType); - } - } - - private boolean animateEnterPipFromActivityEmbedding( - @NonNull TransitionInfo info, - @NonNull SurfaceControl.Transaction startTransaction, - @NonNull SurfaceControl.Transaction finishTransaction, - @NonNull Transitions.TransitionFinishCallback finishCallback) { - ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Animating a mixed transition for " - + "entering PIP from an Activity Embedding window"); - // Split into two transitions (wct) - TransitionInfo.Change pipChange = null; - final TransitionInfo everythingElse = - subCopy(info, TRANSIT_TO_BACK, true /* changes */); - for (int i = info.getChanges().size() - 1; i >= 0; --i) { - TransitionInfo.Change change = info.getChanges().get(i); - if (mPipHandler.isEnteringPip(change, info.getType())) { - if (pipChange != null) { - throw new IllegalStateException("More than 1 pip-entering changes in one" - + " transition? " + info); - } - pipChange = change; - // going backwards, so remove-by-index is fine. - everythingElse.getChanges().remove(i); - } - } - - final Transitions.TransitionFinishCallback finishCB = (wct) -> { - --mInFlightSubAnimations; - joinFinishArgs(wct); - if (mInFlightSubAnimations > 0) return; - finishCallback.onTransitionFinished(mFinishWCT); - }; - - if (!mActivityEmbeddingController.shouldAnimate(everythingElse)) { - // Fallback to dispatching to other handlers. - return false; - } - - // PIP window should always be on the highest Z order. - if (pipChange != null) { - mInFlightSubAnimations = 2; - mPipHandler.startEnterAnimation( - pipChange, - startTransaction.setLayer(pipChange.getLeash(), Integer.MAX_VALUE), - finishTransaction, - finishCB); - } else { - mInFlightSubAnimations = 1; - } - - mActivityEmbeddingController.startAnimation(mTransition, everythingElse, - startTransaction, finishTransaction, finishCB); - return true; - } + @NonNull Transitions.TransitionFinishCallback finishCallback); - private boolean animateOpenIntentWithRemoteAndPip( - @NonNull TransitionInfo info, - @NonNull SurfaceControl.Transaction startTransaction, - @NonNull SurfaceControl.Transaction finishTransaction, - @NonNull Transitions.TransitionFinishCallback finishCallback) { - TransitionInfo.Change pipChange = null; - for (int i = info.getChanges().size() - 1; i >= 0; --i) { - TransitionInfo.Change change = info.getChanges().get(i); - if (mPipHandler.isEnteringPip(change, info.getType())) { - if (pipChange != null) { - throw new IllegalStateException("More than 1 pip-entering changes in one" - + " transition? " + info); - } - pipChange = change; - info.getChanges().remove(i); - } - } - Transitions.TransitionFinishCallback finishCB = (wct) -> { - --mInFlightSubAnimations; - joinFinishArgs(wct); - if (mInFlightSubAnimations > 0) return; - finishCallback.onTransitionFinished(mFinishWCT); - }; - if (pipChange == null) { - if (mLeftoversHandler != null) { - mInFlightSubAnimations = 1; - if (mLeftoversHandler.startAnimation( - mTransition, info, startTransaction, finishTransaction, finishCB)) { - return true; - } - } - return false; - } - ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Splitting PIP into a separate" - + " animation because remote-animation likely doesn't support it"); - // Split the transition into 2 parts: the pip part and the rest. - mInFlightSubAnimations = 2; - // make a new startTransaction because pip's startEnterAnimation "consumes" it so - // we need a separate one to send over to launcher. - SurfaceControl.Transaction otherStartT = new SurfaceControl.Transaction(); - - mPipHandler.startEnterAnimation(pipChange, otherStartT, finishTransaction, finishCB); - - // Dispatch the rest of the transition normally. - if (mLeftoversHandler != null - && mLeftoversHandler.startAnimation( - mTransition, info, startTransaction, finishTransaction, finishCB)) { - return true; - } - mLeftoversHandler = mPlayer.dispatchTransition(mTransition, info, - startTransaction, finishTransaction, finishCB, mMixedHandler); - return true; - } - - private boolean animateRecentsDuringSplit( - @NonNull TransitionInfo info, - @NonNull SurfaceControl.Transaction startTransaction, - @NonNull SurfaceControl.Transaction finishTransaction, - @NonNull Transitions.TransitionFinishCallback finishCallback) { - // Split-screen is only interested in the recents transition finishing (and merging), so - // just wrap finish and start recents animation directly. - Transitions.TransitionFinishCallback finishCB = (wct) -> { - mInFlightSubAnimations = 0; - // If pair-to-pair switching, the post-recents clean-up isn't needed. - wct = wct != null ? wct : new WindowContainerTransaction(); - if (mAnimType != ANIM_TYPE_PAIR_TO_PAIR) { - mSplitHandler.onRecentsInSplitAnimationFinish(wct, finishTransaction); - } else { - // notify pair-to-pair recents animation finish - mSplitHandler.onRecentsPairToPairAnimationFinish(wct); - } - mSplitHandler.onTransitionAnimationComplete(); - finishCallback.onTransitionFinished(wct); - }; - mInFlightSubAnimations = 1; - mSplitHandler.onRecentsInSplitAnimationStart(info); - final boolean handled = mLeftoversHandler.startAnimation(mTransition, info, - startTransaction, finishTransaction, finishCB); - if (!handled) { - mSplitHandler.onRecentsInSplitAnimationCanceled(); - } - return handled; - } - - private boolean animateRecentsDuringKeyguard( - @NonNull TransitionInfo info, - @NonNull SurfaceControl.Transaction startTransaction, - @NonNull SurfaceControl.Transaction finishTransaction, - @NonNull Transitions.TransitionFinishCallback finishCallback) { - if (mInfo == null) { - mInfo = info; - mFinishT = finishTransaction; - mFinishCB = finishCallback; - } - return startSubAnimation(mRecentsHandler, info, startTransaction, finishTransaction); - } - - private boolean animateRecentsDuringDesktop( - @NonNull TransitionInfo info, - @NonNull SurfaceControl.Transaction startTransaction, - @NonNull SurfaceControl.Transaction finishTransaction, - @NonNull Transitions.TransitionFinishCallback finishCallback) { - Transitions.TransitionFinishCallback finishCB = wct -> { - mInFlightSubAnimations--; - if (mInFlightSubAnimations == 0) { - finishCallback.onTransitionFinished(wct); - } - }; - - mInFlightSubAnimations++; - boolean consumed = mRecentsHandler.startAnimation( - mTransition, info, startTransaction, finishTransaction, finishCB); - if (!consumed) { - mInFlightSubAnimations--; - return false; - } - if (mDesktopTasksController != null) { - mDesktopTasksController.syncSurfaceState(info, finishTransaction); - return true; - } - - return false; - } - - private boolean animateUnfold( - @NonNull TransitionInfo info, - @NonNull SurfaceControl.Transaction startTransaction, - @NonNull SurfaceControl.Transaction finishTransaction, - @NonNull Transitions.TransitionFinishCallback finishCallback) { - final Transitions.TransitionFinishCallback finishCB = (wct) -> { - mInFlightSubAnimations--; - if (mInFlightSubAnimations > 0) return; - finishCallback.onTransitionFinished(wct); - }; - mInFlightSubAnimations = 1; - // Sync pip state. - if (mPipHandler != null) { - mPipHandler.syncPipSurfaceState(info, startTransaction, finishTransaction); - } - if (mSplitHandler != null && mSplitHandler.isSplitActive()) { - mSplitHandler.updateSurfaces(startTransaction); - } - return mUnfoldHandler.startAnimation( - mTransition, info, startTransaction, finishTransaction, finishCB); - } - - void mergeAnimation( + abstract void mergeAnimation( @NonNull IBinder transition, @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction t, @NonNull IBinder mergeTarget, - @NonNull Transitions.TransitionFinishCallback finishCallback) { - switch (mType) { - case TYPE_DISPLAY_AND_SPLIT_CHANGE: - // queue since no actual animation. - break; - case TYPE_ENTER_PIP_FROM_SPLIT: - if (mAnimType == ANIM_TYPE_GOING_HOME) { - boolean ended = mSplitHandler.end(); - // If split couldn't end (because it is remote), then don't end everything - // else since we have to play out the animation anyways. - if (!ended) return; - mPipHandler.end(); - if (mLeftoversHandler != null) { - mLeftoversHandler.mergeAnimation( - transition, info, t, mergeTarget, finishCallback); - } - } else { - mPipHandler.end(); - } - break; - case TYPE_ENTER_PIP_FROM_ACTIVITY_EMBEDDING: - mPipHandler.end(); - mActivityEmbeddingController.mergeAnimation(transition, info, t, mergeTarget, - finishCallback); - break; - case TYPE_OPTIONS_REMOTE_AND_PIP_CHANGE: - mPipHandler.end(); - if (mLeftoversHandler != null) { - mLeftoversHandler.mergeAnimation(transition, info, t, mergeTarget, - finishCallback); - } - break; - case TYPE_RECENTS_DURING_SPLIT: - if (mSplitHandler.isPendingEnter(transition)) { - // Recents -> enter-split means that we are switching from one pair to - // another pair. - mAnimType = ANIM_TYPE_PAIR_TO_PAIR; - } - mLeftoversHandler.mergeAnimation( - transition, info, t, mergeTarget, finishCallback); - break; - case TYPE_KEYGUARD: - mKeyguardHandler.mergeAnimation( - transition, info, t, mergeTarget, finishCallback); - break; - case TYPE_RECENTS_DURING_KEYGUARD: - if ((info.getFlags() & TRANSIT_FLAG_KEYGUARD_UNOCCLUDING) != 0) { - DefaultMixedHandler.handoverTransitionLeashes(mInfo, info, t, mFinishT); - if (animateKeyguard(this, info, t, mFinishT, mFinishCB, mKeyguardHandler, - mPipHandler)) { - finishCallback.onTransitionFinished(null); - } - } - mLeftoversHandler.mergeAnimation( - transition, info, t, mergeTarget, finishCallback); - break; - case TYPE_RECENTS_DURING_DESKTOP: - mLeftoversHandler.mergeAnimation( - transition, info, t, mergeTarget, finishCallback); - break; - case TYPE_UNFOLD: - mUnfoldHandler.mergeAnimation(transition, info, t, mergeTarget, finishCallback); - break; - default: - throw new IllegalStateException( - "Playing a mixed transition with unknown type? " + mType); - } - } + @NonNull Transitions.TransitionFinishCallback finishCallback); - void onTransitionConsumed( + abstract void onTransitionConsumed( @NonNull IBinder transition, boolean aborted, - @Nullable SurfaceControl.Transaction finishT) { - switch (mType) { - case TYPE_ENTER_PIP_FROM_SPLIT: - mPipHandler.onTransitionConsumed(transition, aborted, finishT); - break; - case TYPE_ENTER_PIP_FROM_ACTIVITY_EMBEDDING: - mPipHandler.onTransitionConsumed(transition, aborted, finishT); - mActivityEmbeddingController.onTransitionConsumed(transition, aborted, finishT); - break; - case TYPE_RECENTS_DURING_SPLIT: - case TYPE_OPTIONS_REMOTE_AND_PIP_CHANGE: - case TYPE_RECENTS_DURING_DESKTOP: - mLeftoversHandler.onTransitionConsumed(transition, aborted, finishT); - break; - case TYPE_KEYGUARD: - mKeyguardHandler.onTransitionConsumed(transition, aborted, finishT); - break; - case TYPE_UNFOLD: - mUnfoldHandler.onTransitionConsumed(transition, aborted, finishT); - break; - default: - break; - } - - if (mHasRequestToRemote) { - mPlayer.getRemoteTransitionHandler().onTransitionConsumed( - transition, aborted, finishT); - } - } + @Nullable SurfaceControl.Transaction finishT); - boolean startSubAnimation(Transitions.TransitionHandler handler, TransitionInfo info, + protected boolean startSubAnimation( + Transitions.TransitionHandler handler, TransitionInfo info, SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT) { if (mInfo != null) { ProtoLog.d(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, @@ -573,7 +184,7 @@ public class DefaultMixedHandler implements Transitions.TransitionHandler, return true; } - void onSubAnimationFinished(TransitionInfo info, WindowContainerTransaction wct) { + private void onSubAnimationFinished(TransitionInfo info, WindowContainerTransaction wct) { mInFlightSubAnimations--; if (mInfo != null) { ProtoLog.d(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, @@ -644,7 +255,7 @@ public class DefaultMixedHandler implements Transitions.TransitionHandler, throw new IllegalStateException("Unexpected remote transition in" + "pip-enter-from-split request"); } - mActiveTransitions.add(createMixedTransition( + mActiveTransitions.add(createDefaultMixedTransition( MixedTransition.TYPE_ENTER_PIP_FROM_SPLIT, transition)); WindowContainerTransaction out = new WindowContainerTransaction(); @@ -656,7 +267,7 @@ public class DefaultMixedHandler implements Transitions.TransitionHandler, mActivityEmbeddingController != null)) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Got a PiP-enter request from an Activity Embedding split"); - mActiveTransitions.add(createMixedTransition( + mActiveTransitions.add(createDefaultMixedTransition( MixedTransition.TYPE_ENTER_PIP_FROM_ACTIVITY_EMBEDDING, transition)); // Postpone transition splitting to later. WindowContainerTransaction out = new WindowContainerTransaction(); @@ -675,7 +286,7 @@ public class DefaultMixedHandler implements Transitions.TransitionHandler, if (handler == null) { return null; } - final MixedTransition mixed = createMixedTransition( + final MixedTransition mixed = createDefaultMixedTransition( MixedTransition.TYPE_OPTIONS_REMOTE_AND_PIP_CHANGE, transition); mixed.mLeftoversHandler = handler.first; mActiveTransitions.add(mixed); @@ -701,7 +312,7 @@ public class DefaultMixedHandler implements Transitions.TransitionHandler, mPlayer.getRemoteTransitionHandler(), new WindowContainerTransaction()); } - final MixedTransition mixed = createMixedTransition( + final MixedTransition mixed = createRecentsMixedTransition( MixedTransition.TYPE_RECENTS_DURING_SPLIT, transition); mixed.mLeftoversHandler = handler.first; mActiveTransitions.add(mixed); @@ -710,7 +321,7 @@ public class DefaultMixedHandler implements Transitions.TransitionHandler, final WindowContainerTransaction wct = mUnfoldHandler.handleRequest(transition, request); if (wct != null) { - mActiveTransitions.add(createMixedTransition( + mActiveTransitions.add(createDefaultMixedTransition( MixedTransition.TYPE_UNFOLD, transition)); } return wct; @@ -718,6 +329,12 @@ public class DefaultMixedHandler implements Transitions.TransitionHandler, return null; } + private DefaultMixedTransition createDefaultMixedTransition(int type, IBinder transition) { + return new DefaultMixedTransition( + type, transition, mPlayer, this, mPipHandler, mSplitHandler, mKeyguardHandler, + mUnfoldHandler, mActivityEmbeddingController); + } + @Override public Consumer<IBinder> handleRecentsRequest(WindowContainerTransaction outWCT) { if (mRecentsHandler != null) { @@ -737,31 +354,30 @@ public class DefaultMixedHandler implements Transitions.TransitionHandler, private void setRecentsTransitionDuringSplit(IBinder transition) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Got a recents request while " + "Split-Screen is foreground, so treat it as Mixed."); - mActiveTransitions.add(createMixedTransition( + mActiveTransitions.add(createRecentsMixedTransition( MixedTransition.TYPE_RECENTS_DURING_SPLIT, transition)); } private void setRecentsTransitionDuringKeyguard(IBinder transition) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Got a recents request while " + "keyguard is visible, so treat it as Mixed."); - mActiveTransitions.add(createMixedTransition( + mActiveTransitions.add(createRecentsMixedTransition( MixedTransition.TYPE_RECENTS_DURING_KEYGUARD, transition)); } private void setRecentsTransitionDuringDesktop(IBinder transition) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Got a recents request while " + "desktop mode is active, so treat it as Mixed."); - mActiveTransitions.add(createMixedTransition( + mActiveTransitions.add(createRecentsMixedTransition( MixedTransition.TYPE_RECENTS_DURING_DESKTOP, transition)); } - private MixedTransition createMixedTransition(int type, IBinder transition) { - return new MixedTransition(type, transition, mPlayer, this, mPipHandler, mRecentsHandler, - mSplitHandler, mKeyguardHandler, mDesktopTasksController, mUnfoldHandler, - mActivityEmbeddingController); + private MixedTransition createRecentsMixedTransition(int type, IBinder transition) { + return new RecentsMixedTransition(type, transition, mPlayer, this, mPipHandler, + mSplitHandler, mKeyguardHandler, mRecentsHandler, mDesktopTasksController); } - private static TransitionInfo subCopy(@NonNull TransitionInfo info, + static TransitionInfo subCopy(@NonNull TransitionInfo info, @WindowManager.TransitionType int newType, boolean withChanges) { final TransitionInfo out = new TransitionInfo(newType, withChanges ? info.getFlags() : 0); out.setTrack(info.getTrack()); @@ -778,15 +394,6 @@ public class DefaultMixedHandler implements Transitions.TransitionHandler, return out; } - private static boolean isHomeOpening(@NonNull TransitionInfo.Change change) { - return change.getTaskInfo() != null - && change.getTaskInfo().getActivityType() == ACTIVITY_TYPE_HOME; - } - - private static boolean isWallpaper(@NonNull TransitionInfo.Change change) { - return (change.getFlags() & FLAG_IS_WALLPAPER) != 0; - } - @Override public boolean startAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction startTransaction, @@ -805,7 +412,7 @@ public class DefaultMixedHandler implements Transitions.TransitionHandler, if (KeyguardTransitionHandler.handles(info)) { if (mixed != null && mixed.mType != MixedTransition.TYPE_KEYGUARD) { final MixedTransition keyguardMixed = - createMixedTransition(MixedTransition.TYPE_KEYGUARD, transition); + createDefaultMixedTransition(MixedTransition.TYPE_KEYGUARD, transition); mActiveTransitions.add(keyguardMixed); Transitions.TransitionFinishCallback callback = wct -> { mActiveTransitions.remove(keyguardMixed); @@ -845,117 +452,6 @@ public class DefaultMixedHandler implements Transitions.TransitionHandler, return handled; } - private static boolean animateEnterPipFromSplit(@NonNull final MixedTransition mixed, - @NonNull TransitionInfo info, - @NonNull SurfaceControl.Transaction startTransaction, - @NonNull SurfaceControl.Transaction finishTransaction, - @NonNull Transitions.TransitionFinishCallback finishCallback, - @NonNull Transitions player, @NonNull DefaultMixedHandler mixedHandler, - @NonNull PipTransitionController pipHandler, @NonNull StageCoordinator splitHandler) { - ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Animating a mixed transition for " - + "entering PIP while Split-Screen is foreground."); - TransitionInfo.Change pipChange = null; - TransitionInfo.Change wallpaper = null; - final TransitionInfo everythingElse = subCopy(info, TRANSIT_TO_BACK, true /* changes */); - boolean homeIsOpening = false; - for (int i = info.getChanges().size() - 1; i >= 0; --i) { - TransitionInfo.Change change = info.getChanges().get(i); - if (pipHandler.isEnteringPip(change, info.getType())) { - if (pipChange != null) { - throw new IllegalStateException("More than 1 pip-entering changes in one" - + " transition? " + info); - } - pipChange = change; - // going backwards, so remove-by-index is fine. - everythingElse.getChanges().remove(i); - } else if (isHomeOpening(change)) { - homeIsOpening = true; - } else if (isWallpaper(change)) { - wallpaper = change; - } - } - if (pipChange == null) { - // um, something probably went wrong. - return false; - } - final boolean isGoingHome = homeIsOpening; - Transitions.TransitionFinishCallback finishCB = (wct) -> { - --mixed.mInFlightSubAnimations; - mixed.joinFinishArgs(wct); - if (mixed.mInFlightSubAnimations > 0) return; - if (isGoingHome) { - splitHandler.onTransitionAnimationComplete(); - } - finishCallback.onTransitionFinished(mixed.mFinishWCT); - }; - if (isGoingHome || splitHandler.getSplitItemPosition(pipChange.getLastParent()) - != SPLIT_POSITION_UNDEFINED) { - ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Animation is actually mixed " - + "since entering-PiP caused us to leave split and return home."); - // We need to split the transition into 2 parts: the pip part (animated by pip) - // and the dismiss-part (animated by launcher). - mixed.mInFlightSubAnimations = 2; - // immediately make the wallpaper visible (so that we don't see it pop-in during - // the time it takes to start recents animation (which is remote). - if (wallpaper != null) { - startTransaction.show(wallpaper.getLeash()).setAlpha(wallpaper.getLeash(), 1.f); - } - // make a new startTransaction because pip's startEnterAnimation "consumes" it so - // we need a separate one to send over to launcher. - SurfaceControl.Transaction otherStartT = new SurfaceControl.Transaction(); - @SplitScreen.StageType int topStageToKeep = STAGE_TYPE_UNDEFINED; - if (splitHandler.isSplitScreenVisible()) { - // The non-going home case, we could be pip-ing one of the split stages and keep - // showing the other - for (int i = info.getChanges().size() - 1; i >= 0; --i) { - TransitionInfo.Change change = info.getChanges().get(i); - if (change == pipChange) { - // Ignore the change/task that's going into Pip - continue; - } - @SplitScreen.StageType int splitItemStage = - splitHandler.getSplitItemStage(change.getLastParent()); - if (splitItemStage != STAGE_TYPE_UNDEFINED) { - topStageToKeep = splitItemStage; - break; - } - } - } - // Let split update internal state for dismiss. - splitHandler.prepareDismissAnimation(topStageToKeep, - EXIT_REASON_CHILD_TASK_ENTER_PIP, everythingElse, otherStartT, - finishTransaction); - - // We are trying to accommodate launcher's close animation which can't handle the - // divider-bar, so if split-handler is closing the divider-bar, just hide it and remove - // from transition info. - for (int i = everythingElse.getChanges().size() - 1; i >= 0; --i) { - if ((everythingElse.getChanges().get(i).getFlags() & FLAG_IS_DIVIDER_BAR) != 0) { - everythingElse.getChanges().remove(i); - break; - } - } - - pipHandler.setEnterAnimationType(ANIM_TYPE_ALPHA); - pipHandler.startEnterAnimation(pipChange, startTransaction, finishTransaction, - finishCB); - // Dispatch the rest of the transition normally. This will most-likely be taken by - // recents or default handler. - mixed.mLeftoversHandler = player.dispatchTransition(mixed.mTransition, everythingElse, - otherStartT, finishTransaction, finishCB, mixedHandler); - } else { - ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Not leaving split, so just " - + "forward animation to Pip-Handler."); - // This happens if the pip-ing activity is in a multi-activity task (and thus a - // new pip task is spawned). In this case, we don't actually exit split so we can - // just let pip transition handle the animation verbatim. - mixed.mInFlightSubAnimations = 1; - pipHandler.startAnimation(mixed.mTransition, info, startTransaction, finishTransaction, - finishCB); - } - return true; - } - private void unlinkMissingParents(TransitionInfo from) { for (int i = 0; i < from.getChanges().size(); ++i) { final TransitionInfo.Change chg = from.getChanges().get(i); @@ -987,15 +483,14 @@ public class DefaultMixedHandler implements Transitions.TransitionHandler, public boolean animatePendingEnterPipFromSplit(IBinder transition, TransitionInfo info, SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT, Transitions.TransitionFinishCallback finishCallback) { - final MixedTransition mixed = createMixedTransition( + final MixedTransition mixed = createDefaultMixedTransition( MixedTransition.TYPE_ENTER_PIP_FROM_SPLIT, transition); mActiveTransitions.add(mixed); Transitions.TransitionFinishCallback callback = wct -> { mActiveTransitions.remove(mixed); finishCallback.onTransitionFinished(wct); }; - return animateEnterPipFromSplit(mixed, info, startT, finishT, finishCallback, mPlayer, this, - mPipHandler, mSplitHandler); + return mixed.startAnimation(transition, info, startT, finishT, callback); } /** @@ -1018,7 +513,7 @@ public class DefaultMixedHandler implements Transitions.TransitionHandler, } if (displayPart.getChanges().isEmpty()) return false; unlinkMissingParents(everythingElse); - final MixedTransition mixed = createMixedTransition( + final MixedTransition mixed = createDefaultMixedTransition( MixedTransition.TYPE_DISPLAY_AND_SPLIT_CHANGE, transition); mActiveTransitions.add(mixed); ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Animation is a mix of display change " @@ -1135,7 +630,7 @@ public class DefaultMixedHandler implements Transitions.TransitionHandler, * {@link TransitionInfo} so that it can take over some parts of the animation without * reparenting to new transition roots. */ - private static void handoverTransitionLeashes( + static void handoverTransitionLeashes( @NonNull TransitionInfo from, @NonNull TransitionInfo to, @NonNull SurfaceControl.Transaction startT, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedTransition.java new file mode 100644 index 000000000000..9ce46d69815b --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedTransition.java @@ -0,0 +1,315 @@ +/* + * 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.wm.shell.transition; + +import static android.view.WindowManager.TRANSIT_TO_BACK; + +import static com.android.wm.shell.transition.DefaultMixedHandler.subCopy; +import static com.android.wm.shell.transition.MixedTransitionHelper.animateEnterPipFromSplit; +import static com.android.wm.shell.transition.MixedTransitionHelper.animateKeyguard; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.os.IBinder; +import android.view.SurfaceControl; +import android.window.TransitionInfo; + +import com.android.internal.protolog.common.ProtoLog; +import com.android.wm.shell.activityembedding.ActivityEmbeddingController; +import com.android.wm.shell.keyguard.KeyguardTransitionHandler; +import com.android.wm.shell.pip.PipTransitionController; +import com.android.wm.shell.protolog.ShellProtoLogGroup; +import com.android.wm.shell.splitscreen.StageCoordinator; +import com.android.wm.shell.unfold.UnfoldTransitionHandler; + +class DefaultMixedTransition extends DefaultMixedHandler.MixedTransition { + private final UnfoldTransitionHandler mUnfoldHandler; + private final ActivityEmbeddingController mActivityEmbeddingController; + + DefaultMixedTransition(int type, IBinder transition, Transitions player, + DefaultMixedHandler mixedHandler, PipTransitionController pipHandler, + StageCoordinator splitHandler, KeyguardTransitionHandler keyguardHandler, + UnfoldTransitionHandler unfoldHandler, + ActivityEmbeddingController activityEmbeddingController) { + super(type, transition, player, mixedHandler, pipHandler, splitHandler, keyguardHandler); + mUnfoldHandler = unfoldHandler; + mActivityEmbeddingController = activityEmbeddingController; + + switch (type) { + case TYPE_UNFOLD: + mLeftoversHandler = mUnfoldHandler; + break; + case TYPE_DISPLAY_AND_SPLIT_CHANGE: + case TYPE_ENTER_PIP_FROM_ACTIVITY_EMBEDDING: + case TYPE_ENTER_PIP_FROM_SPLIT: + case TYPE_KEYGUARD: + case TYPE_OPTIONS_REMOTE_AND_PIP_CHANGE: + default: + break; + } + } + + @Override + boolean startAnimation( + @NonNull IBinder transition, @NonNull TransitionInfo info, + @NonNull SurfaceControl.Transaction startTransaction, + @NonNull SurfaceControl.Transaction finishTransaction, + @NonNull Transitions.TransitionFinishCallback finishCallback) { + return switch (mType) { + case TYPE_DISPLAY_AND_SPLIT_CHANGE -> false; + case TYPE_ENTER_PIP_FROM_ACTIVITY_EMBEDDING -> + animateEnterPipFromActivityEmbedding( + info, startTransaction, finishTransaction, finishCallback); + case TYPE_ENTER_PIP_FROM_SPLIT -> + animateEnterPipFromSplit(this, info, startTransaction, finishTransaction, + finishCallback, mPlayer, mMixedHandler, mPipHandler, mSplitHandler); + case TYPE_KEYGUARD -> + animateKeyguard(this, info, startTransaction, finishTransaction, finishCallback, + mKeyguardHandler, mPipHandler); + case TYPE_OPTIONS_REMOTE_AND_PIP_CHANGE -> + animateOpenIntentWithRemoteAndPip(transition, info, startTransaction, + finishTransaction, finishCallback); + case TYPE_UNFOLD -> + animateUnfold(info, startTransaction, finishTransaction, finishCallback); + default -> throw new IllegalStateException( + "Starting default mixed animation with unknown or illegal type: " + mType); + }; + } + + private boolean animateEnterPipFromActivityEmbedding( + @NonNull TransitionInfo info, + @NonNull SurfaceControl.Transaction startTransaction, + @NonNull SurfaceControl.Transaction finishTransaction, + @NonNull Transitions.TransitionFinishCallback finishCallback) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Animating a mixed transition for " + + "entering PIP from an Activity Embedding window"); + // Split into two transitions (wct) + TransitionInfo.Change pipChange = null; + final TransitionInfo everythingElse = subCopy(info, TRANSIT_TO_BACK, true /* changes */); + for (int i = info.getChanges().size() - 1; i >= 0; --i) { + TransitionInfo.Change change = info.getChanges().get(i); + if (mPipHandler.isEnteringPip(change, info.getType())) { + if (pipChange != null) { + throw new IllegalStateException("More than 1 pip-entering changes in one" + + " transition? " + info); + } + pipChange = change; + // going backwards, so remove-by-index is fine. + everythingElse.getChanges().remove(i); + } + } + + final Transitions.TransitionFinishCallback finishCB = (wct) -> { + --mInFlightSubAnimations; + joinFinishArgs(wct); + if (mInFlightSubAnimations > 0) return; + finishCallback.onTransitionFinished(mFinishWCT); + }; + + if (!mActivityEmbeddingController.shouldAnimate(everythingElse)) { + // Fallback to dispatching to other handlers. + return false; + } + + // PIP window should always be on the highest Z order. + if (pipChange != null) { + mInFlightSubAnimations = 2; + mPipHandler.startEnterAnimation( + pipChange, startTransaction.setLayer(pipChange.getLeash(), Integer.MAX_VALUE), + finishTransaction, + finishCB); + } else { + mInFlightSubAnimations = 1; + } + + mActivityEmbeddingController.startAnimation( + mTransition, everythingElse, startTransaction, finishTransaction, finishCB); + return true; + } + + private boolean animateOpenIntentWithRemoteAndPip( + @NonNull IBinder transition, @NonNull TransitionInfo info, + @NonNull SurfaceControl.Transaction startTransaction, + @NonNull SurfaceControl.Transaction finishTransaction, + @NonNull Transitions.TransitionFinishCallback finishCallback) { + boolean handledToPip = tryAnimateOpenIntentWithRemoteAndPip( + info, startTransaction, finishTransaction, finishCallback); + // Consume the transition on remote handler if the leftover handler already handle this + // transition. And if it cannot, the transition will be handled by remote handler, so don't + // consume here. + // Need to check leftOverHandler as it may change in #animateOpenIntentWithRemoteAndPip + if (handledToPip && mHasRequestToRemote + && mLeftoversHandler != mPlayer.getRemoteTransitionHandler()) { + mPlayer.getRemoteTransitionHandler().onTransitionConsumed(transition, false, null); + } + return handledToPip; + } + + private boolean tryAnimateOpenIntentWithRemoteAndPip( + @NonNull TransitionInfo info, + @NonNull SurfaceControl.Transaction startTransaction, + @NonNull SurfaceControl.Transaction finishTransaction, + @NonNull Transitions.TransitionFinishCallback finishCallback) { + TransitionInfo.Change pipChange = null; + for (int i = info.getChanges().size() - 1; i >= 0; --i) { + TransitionInfo.Change change = info.getChanges().get(i); + if (mPipHandler.isEnteringPip(change, info.getType())) { + if (pipChange != null) { + throw new IllegalStateException("More than 1 pip-entering changes in one" + + " transition? " + info); + } + pipChange = change; + info.getChanges().remove(i); + } + } + Transitions.TransitionFinishCallback finishCB = (wct) -> { + --mInFlightSubAnimations; + joinFinishArgs(wct); + if (mInFlightSubAnimations > 0) return; + finishCallback.onTransitionFinished(mFinishWCT); + }; + if (pipChange == null) { + if (mLeftoversHandler != null) { + mInFlightSubAnimations = 1; + if (mLeftoversHandler.startAnimation( + mTransition, info, startTransaction, finishTransaction, finishCB)) { + return true; + } + } + return false; + } + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Splitting PIP into a separate" + + " animation because remote-animation likely doesn't support it"); + // Split the transition into 2 parts: the pip part and the rest. + mInFlightSubAnimations = 2; + // make a new startTransaction because pip's startEnterAnimation "consumes" it so + // we need a separate one to send over to launcher. + SurfaceControl.Transaction otherStartT = new SurfaceControl.Transaction(); + + mPipHandler.startEnterAnimation(pipChange, otherStartT, finishTransaction, finishCB); + + // Dispatch the rest of the transition normally. + if (mLeftoversHandler != null + && mLeftoversHandler.startAnimation(mTransition, info, + startTransaction, finishTransaction, finishCB)) { + return true; + } + mLeftoversHandler = mPlayer.dispatchTransition( + mTransition, info, startTransaction, finishTransaction, finishCB, mMixedHandler); + return true; + } + + private boolean animateUnfold( + @NonNull TransitionInfo info, + @NonNull SurfaceControl.Transaction startTransaction, + @NonNull SurfaceControl.Transaction finishTransaction, + @NonNull Transitions.TransitionFinishCallback finishCallback) { + final Transitions.TransitionFinishCallback finishCB = (wct) -> { + mInFlightSubAnimations--; + if (mInFlightSubAnimations > 0) return; + finishCallback.onTransitionFinished(wct); + }; + mInFlightSubAnimations = 1; + // Sync pip state. + if (mPipHandler != null) { + mPipHandler.syncPipSurfaceState(info, startTransaction, finishTransaction); + } + if (mSplitHandler != null && mSplitHandler.isSplitActive()) { + mSplitHandler.updateSurfaces(startTransaction); + } + return mUnfoldHandler.startAnimation( + mTransition, info, startTransaction, finishTransaction, finishCB); + } + + @Override + void mergeAnimation( + @NonNull IBinder transition, @NonNull TransitionInfo info, + @NonNull SurfaceControl.Transaction t, @NonNull IBinder mergeTarget, + @NonNull Transitions.TransitionFinishCallback finishCallback) { + switch (mType) { + case TYPE_DISPLAY_AND_SPLIT_CHANGE: + // queue since no actual animation. + return; + case TYPE_ENTER_PIP_FROM_ACTIVITY_EMBEDDING: + mPipHandler.end(); + mActivityEmbeddingController.mergeAnimation( + transition, info, t, mergeTarget, finishCallback); + return; + case TYPE_ENTER_PIP_FROM_SPLIT: + if (mAnimType == ANIM_TYPE_GOING_HOME) { + boolean ended = mSplitHandler.end(); + // If split couldn't end (because it is remote), then don't end everything else + // since we have to play out the animation anyways. + if (!ended) return; + mPipHandler.end(); + if (mLeftoversHandler != null) { + mLeftoversHandler.mergeAnimation( + transition, info, t, mergeTarget, finishCallback); + } + } else { + mPipHandler.end(); + } + return; + case TYPE_KEYGUARD: + mKeyguardHandler.mergeAnimation(transition, info, t, mergeTarget, finishCallback); + return; + case TYPE_OPTIONS_REMOTE_AND_PIP_CHANGE: + mPipHandler.end(); + if (mLeftoversHandler != null) { + mLeftoversHandler.mergeAnimation( + transition, info, t, mergeTarget, finishCallback); + } + return; + case TYPE_UNFOLD: + mUnfoldHandler.mergeAnimation(transition, info, t, mergeTarget, finishCallback); + return; + default: + throw new IllegalStateException("Playing a default mixed transition with unknown or" + + " illegal type: " + mType); + } + } + + @Override + void onTransitionConsumed( + @NonNull IBinder transition, boolean aborted, + @Nullable SurfaceControl.Transaction finishT) { + switch (mType) { + case TYPE_ENTER_PIP_FROM_ACTIVITY_EMBEDDING: + mPipHandler.onTransitionConsumed(transition, aborted, finishT); + mActivityEmbeddingController.onTransitionConsumed(transition, aborted, finishT); + break; + case TYPE_ENTER_PIP_FROM_SPLIT: + mPipHandler.onTransitionConsumed(transition, aborted, finishT); + break; + case TYPE_KEYGUARD: + mKeyguardHandler.onTransitionConsumed(transition, aborted, finishT); + break; + case TYPE_OPTIONS_REMOTE_AND_PIP_CHANGE: + mLeftoversHandler.onTransitionConsumed(transition, aborted, finishT); + break; + case TYPE_UNFOLD: + mUnfoldHandler.onTransitionConsumed(transition, aborted, finishT); + break; + default: + break; + } + + if (mHasRequestToRemote) { + mPlayer.getRemoteTransitionHandler().onTransitionConsumed(transition, aborted, finishT); + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/MixedTransitionHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/MixedTransitionHelper.java new file mode 100644 index 000000000000..0974cd13f249 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/MixedTransitionHelper.java @@ -0,0 +1,181 @@ +/* + * 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.wm.shell.transition; + +import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; +import static android.view.WindowManager.TRANSIT_TO_BACK; +import static android.window.TransitionInfo.FLAG_IS_WALLPAPER; + +import static com.android.wm.shell.common.split.SplitScreenConstants.FLAG_IS_DIVIDER_BAR; +import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_UNDEFINED; +import static com.android.wm.shell.pip.PipAnimationController.ANIM_TYPE_ALPHA; +import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_UNDEFINED; +import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_CHILD_TASK_ENTER_PIP; +import static com.android.wm.shell.transition.DefaultMixedHandler.subCopy; + +import android.annotation.NonNull; +import android.view.SurfaceControl; +import android.window.TransitionInfo; + +import com.android.internal.protolog.common.ProtoLog; +import com.android.wm.shell.keyguard.KeyguardTransitionHandler; +import com.android.wm.shell.pip.PipTransitionController; +import com.android.wm.shell.protolog.ShellProtoLogGroup; +import com.android.wm.shell.splitscreen.SplitScreen; +import com.android.wm.shell.splitscreen.StageCoordinator; + +public class MixedTransitionHelper { + static boolean animateEnterPipFromSplit( + @NonNull DefaultMixedHandler.MixedTransition mixed, @NonNull TransitionInfo info, + @NonNull SurfaceControl.Transaction startTransaction, + @NonNull SurfaceControl.Transaction finishTransaction, + @NonNull Transitions.TransitionFinishCallback finishCallback, + @NonNull Transitions player, @NonNull DefaultMixedHandler mixedHandler, + @NonNull PipTransitionController pipHandler, @NonNull StageCoordinator splitHandler) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Animating a mixed transition for " + + "entering PIP while Split-Screen is foreground."); + TransitionInfo.Change pipChange = null; + TransitionInfo.Change wallpaper = null; + final TransitionInfo everythingElse = + subCopy(info, TRANSIT_TO_BACK, true /* changes */); + boolean homeIsOpening = false; + for (int i = info.getChanges().size() - 1; i >= 0; --i) { + TransitionInfo.Change change = info.getChanges().get(i); + if (pipHandler.isEnteringPip(change, info.getType())) { + if (pipChange != null) { + throw new IllegalStateException("More than 1 pip-entering changes in one" + + " transition? " + info); + } + pipChange = change; + // going backwards, so remove-by-index is fine. + everythingElse.getChanges().remove(i); + } else if (isHomeOpening(change)) { + homeIsOpening = true; + } else if (isWallpaper(change)) { + wallpaper = change; + } + } + if (pipChange == null) { + // um, something probably went wrong. + return false; + } + final boolean isGoingHome = homeIsOpening; + Transitions.TransitionFinishCallback finishCB = (wct) -> { + --mixed.mInFlightSubAnimations; + mixed.joinFinishArgs(wct); + if (mixed.mInFlightSubAnimations > 0) return; + if (isGoingHome) { + splitHandler.onTransitionAnimationComplete(); + } + finishCallback.onTransitionFinished(mixed.mFinishWCT); + }; + if (isGoingHome || splitHandler.getSplitItemPosition(pipChange.getLastParent()) + != SPLIT_POSITION_UNDEFINED) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Animation is actually mixed " + + "since entering-PiP caused us to leave split and return home."); + // We need to split the transition into 2 parts: the pip part (animated by pip) + // and the dismiss-part (animated by launcher). + mixed.mInFlightSubAnimations = 2; + // immediately make the wallpaper visible (so that we don't see it pop-in during + // the time it takes to start recents animation (which is remote). + if (wallpaper != null) { + startTransaction.show(wallpaper.getLeash()).setAlpha(wallpaper.getLeash(), 1.f); + } + // make a new startTransaction because pip's startEnterAnimation "consumes" it so + // we need a separate one to send over to launcher. + SurfaceControl.Transaction otherStartT = new SurfaceControl.Transaction(); + @SplitScreen.StageType int topStageToKeep = STAGE_TYPE_UNDEFINED; + if (splitHandler.isSplitScreenVisible()) { + // The non-going home case, we could be pip-ing one of the split stages and keep + // showing the other + for (int i = info.getChanges().size() - 1; i >= 0; --i) { + TransitionInfo.Change change = info.getChanges().get(i); + if (change == pipChange) { + // Ignore the change/task that's going into Pip + continue; + } + @SplitScreen.StageType int splitItemStage = + splitHandler.getSplitItemStage(change.getLastParent()); + if (splitItemStage != STAGE_TYPE_UNDEFINED) { + topStageToKeep = splitItemStage; + break; + } + } + } + // Let split update internal state for dismiss. + splitHandler.prepareDismissAnimation(topStageToKeep, + EXIT_REASON_CHILD_TASK_ENTER_PIP, everythingElse, otherStartT, + finishTransaction); + + // We are trying to accommodate launcher's close animation which can't handle the + // divider-bar, so if split-handler is closing the divider-bar, just hide it and + // remove from transition info. + for (int i = everythingElse.getChanges().size() - 1; i >= 0; --i) { + if ((everythingElse.getChanges().get(i).getFlags() & FLAG_IS_DIVIDER_BAR) + != 0) { + everythingElse.getChanges().remove(i); + break; + } + } + + pipHandler.setEnterAnimationType(ANIM_TYPE_ALPHA); + pipHandler.startEnterAnimation(pipChange, startTransaction, finishTransaction, + finishCB); + // Dispatch the rest of the transition normally. This will most-likely be taken by + // recents or default handler. + mixed.mLeftoversHandler = player.dispatchTransition(mixed.mTransition, everythingElse, + otherStartT, finishTransaction, finishCB, mixedHandler); + } else { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Not leaving split, so just " + + "forward animation to Pip-Handler."); + // This happens if the pip-ing activity is in a multi-activity task (and thus a + // new pip task is spawned). In this case, we don't actually exit split so we can + // just let pip transition handle the animation verbatim. + mixed.mInFlightSubAnimations = 1; + pipHandler.startAnimation( + mixed.mTransition, info, startTransaction, finishTransaction, finishCB); + } + return true; + } + + private static boolean isHomeOpening(@NonNull TransitionInfo.Change change) { + return change.getTaskInfo() != null + && change.getTaskInfo().getActivityType() == ACTIVITY_TYPE_HOME; + } + + private static boolean isWallpaper(@NonNull TransitionInfo.Change change) { + return (change.getFlags() & FLAG_IS_WALLPAPER) != 0; + } + + static boolean animateKeyguard( + @NonNull DefaultMixedHandler.MixedTransition mixed, @NonNull TransitionInfo info, + @NonNull SurfaceControl.Transaction startTransaction, + @NonNull SurfaceControl.Transaction finishTransaction, + @NonNull Transitions.TransitionFinishCallback finishCallback, + @NonNull KeyguardTransitionHandler keyguardHandler, + PipTransitionController pipHandler) { + if (mixed.mFinishT == null) { + mixed.mFinishT = finishTransaction; + mixed.mFinishCB = finishCallback; + } + // Sync pip state. + if (pipHandler != null) { + pipHandler.syncPipSurfaceState(info, startTransaction, finishTransaction); + } + return mixed.startSubAnimation(keyguardHandler, info, startTransaction, finishTransaction); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RecentsMixedTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RecentsMixedTransition.java new file mode 100644 index 000000000000..643e0266d7df --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RecentsMixedTransition.java @@ -0,0 +1,214 @@ +/* + * 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.wm.shell.transition; + +import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_UNOCCLUDING; + +import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_UNDEFINED; +import static com.android.wm.shell.transition.DefaultMixedHandler.handoverTransitionLeashes; +import static com.android.wm.shell.transition.MixedTransitionHelper.animateEnterPipFromSplit; +import static com.android.wm.shell.transition.MixedTransitionHelper.animateKeyguard; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.os.IBinder; +import android.view.SurfaceControl; +import android.window.TransitionInfo; +import android.window.WindowContainerTransaction; + +import com.android.wm.shell.desktopmode.DesktopTasksController; +import com.android.wm.shell.keyguard.KeyguardTransitionHandler; +import com.android.wm.shell.pip.PipTransitionController; +import com.android.wm.shell.recents.RecentsTransitionHandler; +import com.android.wm.shell.splitscreen.StageCoordinator; + +class RecentsMixedTransition extends DefaultMixedHandler.MixedTransition { + private final RecentsTransitionHandler mRecentsHandler; + private final DesktopTasksController mDesktopTasksController; + + RecentsMixedTransition(int type, IBinder transition, Transitions player, + DefaultMixedHandler mixedHandler, PipTransitionController pipHandler, + StageCoordinator splitHandler, KeyguardTransitionHandler keyguardHandler, + RecentsTransitionHandler recentsHandler, + DesktopTasksController desktopTasksController) { + super(type, transition, player, mixedHandler, pipHandler, splitHandler, keyguardHandler); + mRecentsHandler = recentsHandler; + mDesktopTasksController = desktopTasksController; + mLeftoversHandler = mRecentsHandler; + } + + @Override + boolean startAnimation( + @NonNull IBinder transition, @NonNull TransitionInfo info, + @NonNull SurfaceControl.Transaction startTransaction, + @NonNull SurfaceControl.Transaction finishTransaction, + @NonNull Transitions.TransitionFinishCallback finishCallback) { + return switch (mType) { + case TYPE_RECENTS_DURING_DESKTOP -> + animateRecentsDuringDesktop( + info, startTransaction, finishTransaction, finishCallback); + case TYPE_RECENTS_DURING_KEYGUARD -> + animateRecentsDuringKeyguard( + info, startTransaction, finishTransaction, finishCallback); + case TYPE_RECENTS_DURING_SPLIT -> + animateRecentsDuringSplit( + info, startTransaction, finishTransaction, finishCallback); + default -> throw new IllegalStateException( + "Starting Recents mixed animation with unknown or illegal type: " + mType); + }; + } + + private boolean animateRecentsDuringDesktop( + @NonNull TransitionInfo info, + @NonNull SurfaceControl.Transaction startTransaction, + @NonNull SurfaceControl.Transaction finishTransaction, + @NonNull Transitions.TransitionFinishCallback finishCallback) { + if (mInfo == null) { + mInfo = info; + mFinishT = finishTransaction; + mFinishCB = finishCallback; + } + Transitions.TransitionFinishCallback finishCB = wct -> { + mInFlightSubAnimations--; + if (mInFlightSubAnimations == 0) { + finishCallback.onTransitionFinished(wct); + } + }; + + mInFlightSubAnimations++; + boolean consumed = mRecentsHandler.startAnimation( + mTransition, info, startTransaction, finishTransaction, finishCB); + if (!consumed) { + mInFlightSubAnimations--; + return false; + } + if (mDesktopTasksController != null) { + mDesktopTasksController.syncSurfaceState(info, finishTransaction); + return true; + } + + return false; + } + + private boolean animateRecentsDuringKeyguard( + @NonNull TransitionInfo info, + @NonNull SurfaceControl.Transaction startTransaction, + @NonNull SurfaceControl.Transaction finishTransaction, + @NonNull Transitions.TransitionFinishCallback finishCallback) { + if (mInfo == null) { + mInfo = info; + mFinishT = finishTransaction; + mFinishCB = finishCallback; + } + return startSubAnimation(mRecentsHandler, info, startTransaction, finishTransaction); + } + + private boolean animateRecentsDuringSplit( + @NonNull TransitionInfo info, + @NonNull SurfaceControl.Transaction startTransaction, + @NonNull SurfaceControl.Transaction finishTransaction, + @NonNull Transitions.TransitionFinishCallback finishCallback) { + for (int i = info.getChanges().size() - 1; i >= 0; --i) { + final TransitionInfo.Change change = info.getChanges().get(i); + // Pip auto-entering info might be appended to recent transition like pressing + // home-key in 3-button navigation. This offers split handler the opportunity to + // handle split to pip animation. + if (mPipHandler.isEnteringPip(change, info.getType()) + && mSplitHandler.getSplitItemPosition(change.getLastParent()) + != SPLIT_POSITION_UNDEFINED) { + return animateEnterPipFromSplit(this, info, startTransaction, finishTransaction, + finishCallback, mPlayer, mMixedHandler, mPipHandler, mSplitHandler); + } + } + + // Split-screen is only interested in the recents transition finishing (and merging), so + // just wrap finish and start recents animation directly. + Transitions.TransitionFinishCallback finishCB = (wct) -> { + mInFlightSubAnimations = 0; + // If pair-to-pair switching, the post-recents clean-up isn't needed. + wct = wct != null ? wct : new WindowContainerTransaction(); + if (mAnimType != ANIM_TYPE_PAIR_TO_PAIR) { + mSplitHandler.onRecentsInSplitAnimationFinish(wct, finishTransaction); + } else { + // notify pair-to-pair recents animation finish + mSplitHandler.onRecentsPairToPairAnimationFinish(wct); + } + mSplitHandler.onTransitionAnimationComplete(); + finishCallback.onTransitionFinished(wct); + }; + mInFlightSubAnimations = 1; + mSplitHandler.onRecentsInSplitAnimationStart(info); + final boolean handled = mLeftoversHandler.startAnimation( + mTransition, info, startTransaction, finishTransaction, finishCB); + if (!handled) { + mSplitHandler.onRecentsInSplitAnimationCanceled(); + } + return handled; + } + + @Override + void mergeAnimation( + @NonNull IBinder transition, @NonNull TransitionInfo info, + @NonNull SurfaceControl.Transaction t, @NonNull IBinder mergeTarget, + @NonNull Transitions.TransitionFinishCallback finishCallback) { + switch (mType) { + case TYPE_RECENTS_DURING_DESKTOP: + mLeftoversHandler.mergeAnimation(transition, info, t, mergeTarget, finishCallback); + return; + case TYPE_RECENTS_DURING_KEYGUARD: + if ((info.getFlags() & TRANSIT_FLAG_KEYGUARD_UNOCCLUDING) != 0) { + handoverTransitionLeashes(mInfo, info, t, mFinishT); + if (animateKeyguard( + this, info, t, mFinishT, mFinishCB, mKeyguardHandler, mPipHandler)) { + finishCallback.onTransitionFinished(null); + } + } + mLeftoversHandler.mergeAnimation(transition, info, t, mergeTarget, + finishCallback); + return; + case TYPE_RECENTS_DURING_SPLIT: + if (mSplitHandler.isPendingEnter(transition)) { + // Recents -> enter-split means that we are switching from one pair to + // another pair. + mAnimType = DefaultMixedHandler.MixedTransition.ANIM_TYPE_PAIR_TO_PAIR; + } + mLeftoversHandler.mergeAnimation(transition, info, t, mergeTarget, finishCallback); + return; + default: + throw new IllegalStateException("Playing a Recents mixed transition with unknown or" + + " illegal type: " + mType); + } + } + + @Override + void onTransitionConsumed( + @NonNull IBinder transition, boolean aborted, + @Nullable SurfaceControl.Transaction finishT) { + switch (mType) { + case TYPE_RECENTS_DURING_DESKTOP: + case TYPE_RECENTS_DURING_SPLIT: + mLeftoversHandler.onTransitionConsumed(transition, aborted, finishT); + break; + default: + break; + } + + if (mHasRequestToRemote) { + mPlayer.getRemoteTransitionHandler().onTransitionConsumed(transition, aborted, finishT); + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java index b0d8b47b170a..3fb0dbfaa63d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java @@ -83,6 +83,9 @@ import com.android.wm.shell.protolog.ShellProtoLogGroup; import com.android.wm.shell.sysui.ShellCommandHandler; import com.android.wm.shell.sysui.ShellController; import com.android.wm.shell.sysui.ShellInit; +import com.android.wm.shell.transition.tracing.LegacyTransitionTracer; +import com.android.wm.shell.transition.tracing.PerfettoTransitionTracer; +import com.android.wm.shell.transition.tracing.TransitionTracer; import com.android.wm.shell.util.TransitionUtil; import java.io.PrintWriter; @@ -184,7 +187,7 @@ public class Transitions implements RemoteCallable<Transitions>, private final ShellController mShellController; private final ShellTransitionImpl mImpl = new ShellTransitionImpl(); private final SleepHandler mSleepHandler = new SleepHandler(); - private final Tracer mTracer = new Tracer(); + private final TransitionTracer mTransitionTracer; private boolean mIsRegistered = false; /** List of possible handlers. Ordered by specificity (eg. tapped back to front). */ @@ -307,6 +310,12 @@ public class Transitions implements RemoteCallable<Transitions>, ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "addHandler: Remote"); shellInit.addInitCallback(this::onInit, this); mHomeTransitionObserver = observer; + + if (android.tracing.Flags.perfettoTransitionTracing()) { + mTransitionTracer = new PerfettoTransitionTracer(); + } else { + mTransitionTracer = new LegacyTransitionTracer(); + } } private void onInit() { @@ -868,7 +877,7 @@ public class Transitions implements RemoteCallable<Transitions>, ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Transition %s ready while" + " %s is still animating. Notify the animating transition" + " in case they can be merged", ready, playing); - mTracer.logMergeRequested(ready.mInfo.getDebugId(), playing.mInfo.getDebugId()); + mTransitionTracer.logMergeRequested(ready.mInfo.getDebugId(), playing.mInfo.getDebugId()); playing.mHandler.mergeAnimation(ready.mToken, ready.mInfo, ready.mStartT, playing.mToken, (wct) -> onMerged(playing, ready)); } @@ -902,7 +911,7 @@ public class Transitions implements RemoteCallable<Transitions>, for (int i = 0; i < mObservers.size(); ++i) { mObservers.get(i).onTransitionMerged(merged.mToken, playing.mToken); } - mTracer.logMerged(merged.mInfo.getDebugId(), playing.mInfo.getDebugId()); + mTransitionTracer.logMerged(merged.mInfo.getDebugId(), playing.mInfo.getDebugId()); // See if we should merge another transition. processReadyQueue(track); } @@ -923,7 +932,7 @@ public class Transitions implements RemoteCallable<Transitions>, active.mStartT, active.mFinishT, (wct) -> onFinish(active, wct)); if (consumed) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " animated by firstHandler"); - mTracer.logDispatched(active.mInfo.getDebugId(), active.mHandler); + mTransitionTracer.logDispatched(active.mInfo.getDebugId(), active.mHandler); return; } } @@ -948,7 +957,7 @@ public class Transitions implements RemoteCallable<Transitions>, if (consumed) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " animated by %s", mHandlers.get(i)); - mTracer.logDispatched(info.getDebugId(), mHandlers.get(i)); + mTransitionTracer.logDispatched(info.getDebugId(), mHandlers.get(i)); return mHandlers.get(i); } } @@ -978,7 +987,7 @@ public class Transitions implements RemoteCallable<Transitions>, final Track track = mTracks.get(transition.getTrack()); transition.mAborted = true; - mTracer.logAborted(transition.mInfo.getDebugId()); + mTransitionTracer.logAborted(transition.mInfo.getDebugId()); if (transition.mHandler != null) { // Notifies to clean-up the aborted transition. @@ -1506,12 +1515,18 @@ public class Transitions implements RemoteCallable<Transitions>, } } - @Override public boolean onShellCommand(String[] args, PrintWriter pw) { switch (args[0]) { case "tracing": { - mTracer.onShellCommand(Arrays.copyOfRange(args, 1, args.length), pw); + if (!android.tracing.Flags.perfettoTransitionTracing()) { + ((LegacyTransitionTracer) mTransitionTracer) + .onShellCommand(Arrays.copyOfRange(args, 1, args.length), pw); + } else { + pw.println("Command not supported. Use the Perfetto command instead to start " + + "and stop this trace instead."); + return false; + } return true; } default: { @@ -1524,8 +1539,10 @@ public class Transitions implements RemoteCallable<Transitions>, @Override public void printShellCommandHelp(PrintWriter pw, String prefix) { - pw.println(prefix + "tracing"); - mTracer.printShellCommandHelp(pw, prefix + " "); + if (!android.tracing.Flags.perfettoTransitionTracing()) { + pw.println(prefix + "tracing"); + ((LegacyTransitionTracer) mTransitionTracer).printShellCommandHelp(pw, prefix + " "); + } } private void dump(@NonNull PrintWriter pw, String prefix) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Tracer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/tracing/LegacyTransitionTracer.java index 5919aad133c7..9c848869e0f8 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Tracer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/tracing/LegacyTransitionTracer.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell.transition; +package com.android.wm.shell.transition.tracing; import static android.os.Build.IS_USER; @@ -29,6 +29,7 @@ import android.util.Log; import com.android.internal.util.TraceBuffer; import com.android.wm.shell.sysui.ShellCommandHandler; +import com.android.wm.shell.transition.Transitions; import com.google.protobuf.nano.MessageNano; @@ -45,7 +46,8 @@ import java.util.concurrent.TimeUnit; /** * Helper class to collect and dump transition traces. */ -public class Tracer implements ShellCommandHandler.ShellCommandActionHandler { +public class LegacyTransitionTracer + implements ShellCommandHandler.ShellCommandActionHandler, TransitionTracer { private static final int ALWAYS_ON_TRACING_CAPACITY = 15 * 1024; // 15 KB private static final int ACTIVE_TRACING_BUFFER_CAPACITY = 5000 * 1024; // 5 MB @@ -60,33 +62,33 @@ public class Tracer implements ShellCommandHandler.ShellCommandActionHandler { private final TraceBuffer.ProtoProvider mProtoProvider = new TraceBuffer.ProtoProvider<MessageNano, - com.android.wm.shell.nano.WmShellTransitionTraceProto, - com.android.wm.shell.nano.Transition>() { - @Override - public int getItemSize(MessageNano proto) { - return proto.getCachedSize(); - } - - @Override - public byte[] getBytes(MessageNano proto) { - return MessageNano.toByteArray(proto); - } - - @Override - public void write( - com.android.wm.shell.nano.WmShellTransitionTraceProto encapsulatingProto, - Queue<com.android.wm.shell.nano.Transition> buffer, OutputStream os) + com.android.wm.shell.nano.WmShellTransitionTraceProto, + com.android.wm.shell.nano.Transition>() { + @Override + public int getItemSize(MessageNano proto) { + return proto.getCachedSize(); + } + + @Override + public byte[] getBytes(MessageNano proto) { + return MessageNano.toByteArray(proto); + } + + @Override + public void write( + com.android.wm.shell.nano.WmShellTransitionTraceProto encapsulatingProto, + Queue<com.android.wm.shell.nano.Transition> buffer, OutputStream os) throws IOException { - encapsulatingProto.transitions = buffer.toArray( - new com.android.wm.shell.nano.Transition[0]); - os.write(getBytes(encapsulatingProto)); - } - }; + encapsulatingProto.transitions = buffer.toArray( + new com.android.wm.shell.nano.Transition[0]); + os.write(getBytes(encapsulatingProto)); + } + }; private final TraceBuffer<MessageNano, com.android.wm.shell.nano.WmShellTransitionTraceProto, - com.android.wm.shell.nano.Transition> mTraceBuffer - = new TraceBuffer(ALWAYS_ON_TRACING_CAPACITY, mProtoProvider, - (proto) -> handleOnEntryRemovedFromTrace(proto)); + com.android.wm.shell.nano.Transition> mTraceBuffer = + new TraceBuffer(ALWAYS_ON_TRACING_CAPACITY, mProtoProvider, + this::handleOnEntryRemovedFromTrace); private final Map<Object, Runnable> mRemovedFromTraceCallbacks = new HashMap<>(); private final Map<Transitions.TransitionHandler, Integer> mHandlerIds = new HashMap<>(); @@ -99,6 +101,7 @@ public class Tracer implements ShellCommandHandler.ShellCommandActionHandler { * @param transitionId The id of the transition being dispatched. * @param handler The handler the transition is being dispatched to. */ + @Override public void logDispatched(int transitionId, Transitions.TransitionHandler handler) { final int handlerId; if (mHandlerIds.containsKey(handler)) { @@ -130,6 +133,7 @@ public class Tracer implements ShellCommandHandler.ShellCommandActionHandler { * * @param mergeRequestedTransitionId The id of the transition we are requesting to be merged. */ + @Override public void logMergeRequested(int mergeRequestedTransitionId, int playingTransitionId) { com.android.wm.shell.nano.Transition proto = new com.android.wm.shell.nano.Transition(); proto.id = mergeRequestedTransitionId; @@ -145,6 +149,7 @@ public class Tracer implements ShellCommandHandler.ShellCommandActionHandler { * @param mergedTransitionId The id of the transition that was merged. * @param playingTransitionId The id of the transition the transition was merged into. */ + @Override public void logMerged(int mergedTransitionId, int playingTransitionId) { com.android.wm.shell.nano.Transition proto = new com.android.wm.shell.nano.Transition(); proto.id = mergedTransitionId; @@ -159,6 +164,7 @@ public class Tracer implements ShellCommandHandler.ShellCommandActionHandler { * * @param transitionId The id of the transition that was aborted. */ + @Override public void logAborted(int transitionId) { com.android.wm.shell.nano.Transition proto = new com.android.wm.shell.nano.Transition(); proto.id = transitionId; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/tracing/PerfettoTransitionTracer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/tracing/PerfettoTransitionTracer.java new file mode 100644 index 000000000000..99df6a31beac --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/tracing/PerfettoTransitionTracer.java @@ -0,0 +1,174 @@ +/* + * 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.wm.shell.transition.tracing; + +import android.internal.perfetto.protos.PerfettoTrace; +import android.os.SystemClock; +import android.tracing.perfetto.DataSourceInstance; +import android.tracing.perfetto.DataSourceParams; +import android.tracing.perfetto.InitArguments; +import android.tracing.perfetto.Producer; +import android.tracing.perfetto.TracingContext; +import android.tracing.transition.TransitionDataSource; +import android.util.proto.ProtoOutputStream; + +import com.android.wm.shell.transition.Transitions; + +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * Helper class to collect and dump transition traces. + */ +public class PerfettoTransitionTracer implements TransitionTracer { + private final AtomicInteger mActiveTraces = new AtomicInteger(0); + private final TransitionDataSource mDataSource = new TransitionDataSource( + mActiveTraces::incrementAndGet, + this::onFlush, + mActiveTraces::decrementAndGet); + + public PerfettoTransitionTracer() { + Producer.init(InitArguments.DEFAULTS); + mDataSource.register(DataSourceParams.DEFAULTS); + } + + /** + * Adds an entry in the trace to log that a transition has been dispatched to a handler. + * + * @param transitionId The id of the transition being dispatched. + * @param handler The handler the transition is being dispatched to. + */ + @Override + public void logDispatched(int transitionId, Transitions.TransitionHandler handler) { + if (!isTracing()) { + return; + } + + mDataSource.trace(ctx -> { + final int handlerId = getHandlerId(handler, ctx); + + final ProtoOutputStream os = ctx.newTracePacket(); + final long token = os.start(PerfettoTrace.TracePacket.SHELL_TRANSITION); + os.write(PerfettoTrace.ShellTransition.ID, transitionId); + os.write(PerfettoTrace.ShellTransition.DISPATCH_TIME_NS, + SystemClock.elapsedRealtimeNanos()); + os.write(PerfettoTrace.ShellTransition.HANDLER, handlerId); + os.end(token); + }); + } + + private static int getHandlerId(Transitions.TransitionHandler handler, + TracingContext<DataSourceInstance, TransitionDataSource.TlsState, Void> ctx) { + final Map<String, Integer> handlerMapping = + ctx.getCustomTlsState().handlerMapping; + final int handlerId; + if (handlerMapping.containsKey(handler.getClass().getName())) { + handlerId = handlerMapping.get(handler.getClass().getName()); + } else { + // + 1 to avoid 0 ids which can be confused with missing value when dumped to proto + handlerId = handlerMapping.size() + 1; + handlerMapping.put(handler.getClass().getName(), handlerId); + } + return handlerId; + } + + /** + * Adds an entry in the trace to log that a request to merge a transition was made. + * + * @param mergeRequestedTransitionId The id of the transition we are requesting to be merged. + */ + @Override + public void logMergeRequested(int mergeRequestedTransitionId, int playingTransitionId) { + if (!isTracing()) { + return; + } + + mDataSource.trace(ctx -> { + final ProtoOutputStream os = ctx.newTracePacket(); + final long token = os.start(PerfettoTrace.TracePacket.SHELL_TRANSITION); + os.write(PerfettoTrace.ShellTransition.ID, mergeRequestedTransitionId); + os.write(PerfettoTrace.ShellTransition.MERGE_REQUEST_TIME_NS, + SystemClock.elapsedRealtimeNanos()); + os.write(PerfettoTrace.ShellTransition.MERGE_TARGET, playingTransitionId); + os.end(token); + }); + } + + /** + * Adds an entry in the trace to log that a transition was merged by the handler. + * + * @param mergedTransitionId The id of the transition that was merged. + * @param playingTransitionId The id of the transition the transition was merged into. + */ + @Override + public void logMerged(int mergedTransitionId, int playingTransitionId) { + if (!isTracing()) { + return; + } + + mDataSource.trace(ctx -> { + final ProtoOutputStream os = ctx.newTracePacket(); + final long token = os.start(PerfettoTrace.TracePacket.SHELL_TRANSITION); + os.write(PerfettoTrace.ShellTransition.ID, mergedTransitionId); + os.write(PerfettoTrace.ShellTransition.MERGE_TIME_NS, + SystemClock.elapsedRealtimeNanos()); + os.write(PerfettoTrace.ShellTransition.MERGE_TARGET, playingTransitionId); + os.end(token); + }); + } + + /** + * Adds an entry in the trace to log that a transition was aborted. + * + * @param transitionId The id of the transition that was aborted. + */ + @Override + public void logAborted(int transitionId) { + if (!isTracing()) { + return; + } + + mDataSource.trace(ctx -> { + final ProtoOutputStream os = ctx.newTracePacket(); + final long token = os.start(PerfettoTrace.TracePacket.SHELL_TRANSITION); + os.write(PerfettoTrace.ShellTransition.ID, transitionId); + os.write(PerfettoTrace.ShellTransition.SHELL_ABORT_TIME_NS, + SystemClock.elapsedRealtimeNanos()); + os.end(token); + }); + } + + private boolean isTracing() { + return mActiveTraces.get() > 0; + } + + private void onFlush() { + mDataSource.trace(ctx -> { + final ProtoOutputStream os = ctx.newTracePacket(); + + final Map<String, Integer> handlerMapping = ctx.getCustomTlsState().handlerMapping; + for (String handler : handlerMapping.keySet()) { + final long token = os.start(PerfettoTrace.TracePacket.SHELL_HANDLER_MAPPINGS); + os.write(PerfettoTrace.ShellHandlerMapping.ID, handlerMapping.get(handler)); + os.write(PerfettoTrace.ShellHandlerMapping.NAME, handler); + os.end(token); + } + + ctx.flush(); + }); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/tracing/TransitionTracer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/tracing/TransitionTracer.java new file mode 100644 index 000000000000..5857ad88e9e6 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/tracing/TransitionTracer.java @@ -0,0 +1,51 @@ +/* + * 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.wm.shell.transition.tracing; + +import com.android.wm.shell.transition.Transitions; + +public interface TransitionTracer { + /** + * Adds an entry in the trace to log that a transition has been dispatched to a handler. + * + * @param transitionId The id of the transition being dispatched. + * @param handler The handler the transition is being dispatched to. + */ + void logDispatched(int transitionId, Transitions.TransitionHandler handler); + + /** + * Adds an entry in the trace to log that a request to merge a transition was made. + * + * @param mergeRequestedTransitionId The id of the transition we are requesting to be merged. + */ + void logMergeRequested(int mergeRequestedTransitionId, int playingTransitionId); + + /** + * Adds an entry in the trace to log that a transition was merged by the handler. + * + * @param mergedTransitionId The id of the transition that was merged. + * @param playingTransitionId The id of the transition the transition was merged into. + */ + void logMerged(int mergedTransitionId, int playingTransitionId); + + /** + * Adds an entry in the trace to log that a transition was aborted. + * + * @param transitionId The id of the transition that was aborted. + */ + void logAborted(int transitionId); +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java index 1a793a16f254..b2eeea7048bc 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java @@ -271,6 +271,9 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel { if (e.findPointerIndex(mDragPointerId) == -1) { mDragPointerId = e.getPointerId(0); } + final CaptionWindowDecoration decoration = mWindowDecorByTaskId.get(mTaskId); + // If a decor's resize drag zone is active, don't also try to reposition it. + if (decoration.isHandlingDragResize()) break; final int dragPointerIdx = e.findPointerIndex(mDragPointerId); mDragPositioningCallback.onDragPositioningMove( e.getRawX(dragPointerIdx), e.getRawY(dragPointerIdx)); 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 1debb02e86af..5a74255df49a 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 @@ -286,6 +286,10 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL closeBackground.setTintList(buttonTintColor); } + boolean isHandlingDragResize() { + return mDragResizeListener.isHandlingDragResize(); + } + private void closeDragResizeListener() { if (mDragResizeListener == null) { return; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java index aabc1cfb5875..554b1fb99550 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java @@ -491,8 +491,11 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { return true; } case MotionEvent.ACTION_MOVE: { + mShouldClick = false; final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(mTaskId); + // If a decor's resize drag zone is active, don't also try to reposition it. + if (decoration.isHandlingDragResize()) break; decoration.closeMaximizeMenu(); if (e.findPointerIndex(mDragPointerId) == -1) { mDragPointerId = e.getPointerId(0); @@ -505,7 +508,6 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { e.getRawX(dragPointerIdx), newTaskBounds)); mIsDragging = true; - mShouldClick = false; return true; } case MotionEvent.ACTION_UP: diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java index 53f806ccabee..20233331997f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java @@ -387,6 +387,10 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin return mHandleMenu != null; } + boolean isHandlingDragResize() { + return mDragResizeListener.isHandlingDragResize(); + } + private void loadAppInfo() { String packageName = mTaskInfo.realActivity.getPackageName(); PackageManager pm = mContext.getApplicationContext().getPackageManager(); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeInputListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeInputListener.java index 8511a21d4294..8b38f991a2db 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeInputListener.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeInputListener.java @@ -320,6 +320,10 @@ class DragResizeInputListener implements AutoCloseable { } } + boolean isHandlingDragResize() { + return mInputEventReceiver.isHandlingEvents(); + } + @Override public void close() { mInputEventReceiver.dispose(); @@ -386,6 +390,10 @@ class DragResizeInputListener implements AutoCloseable { finishInputEvent(inputEvent, handleInputEvent(inputEvent)); } + boolean isHandlingEvents() { + return mShouldHandleEvents; + } + private boolean handleInputEvent(InputEvent inputEvent) { if (!(inputEvent instanceof MotionEvent)) { return false; @@ -409,7 +417,6 @@ class DragResizeInputListener implements AutoCloseable { mShouldHandleEvents = isInResizeHandleBounds(x, y); } if (mShouldHandleEvents) { - mInputManager.pilferPointers(mInputChannel.getToken()); mDragPointerId = e.getPointerId(0); float rawX = e.getRawX(0); float rawY = e.getRawY(0); @@ -427,6 +434,7 @@ class DragResizeInputListener implements AutoCloseable { if (!mShouldHandleEvents) { break; } + mInputManager.pilferPointers(mInputChannel.getToken()); int dragPointerIndex = e.findPointerIndex(mDragPointerId); float rawX = e.getRawX(dragPointerIndex); float rawY = e.getRawY(dragPointerIndex); @@ -437,6 +445,7 @@ class DragResizeInputListener implements AutoCloseable { } case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: { + mInputManager.pilferPointers(mInputChannel.getToken()); if (mShouldHandleEvents) { int dragPointerIndex = e.findPointerIndex(mDragPointerId); final Rect taskBounds = mCallback.onDragPositioningEnd( diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositioner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositioner.java index c1b18f959641..7c6e69eb1ec9 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositioner.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositioner.java @@ -61,6 +61,7 @@ public class VeiledResizeTaskPositioner implements DragPositioningCallback, private int mCtrlType; private boolean mIsResizingOrAnimatingResize; @Surface.Rotation private int mRotation; + private boolean mVeilIsVisible; public VeiledResizeTaskPositioner(ShellTaskOrganizer taskOrganizer, DesktopModeWindowDecoration windowDecoration, @@ -94,7 +95,6 @@ public class VeiledResizeTaskPositioner implements DragPositioningCallback, mDesktopWindowDecoration.mTaskInfo.configuration.windowConfiguration.getBounds()); mRepositionStartPoint.set(x, y); if (isResizing()) { - mDesktopWindowDecoration.showResizeVeil(mTaskBoundsAtDragStart); if (!mDesktopWindowDecoration.mTaskInfo.isFocused) { WindowContainerTransaction wct = new WindowContainerTransaction(); wct.reorder(mDesktopWindowDecoration.mTaskInfo.token, true); @@ -119,8 +119,13 @@ public class VeiledResizeTaskPositioner implements DragPositioningCallback, if (isResizing() && DragPositioningCallbackUtility.changeBounds(mCtrlType, mRepositionTaskBounds, mTaskBoundsAtDragStart, mStableBounds, delta, mDisplayController, mDesktopWindowDecoration)) { - mDesktopWindowDecoration.updateResizeVeil(mRepositionTaskBounds); mIsResizingOrAnimatingResize = true; + if (!mVeilIsVisible) { + mDesktopWindowDecoration.showResizeVeil(mRepositionTaskBounds); + mVeilIsVisible = true; + } else { + mDesktopWindowDecoration.updateResizeVeil(mRepositionTaskBounds); + } } else if (mCtrlType == CTRL_TYPE_UNDEFINED) { final SurfaceControl.Transaction t = mTransactionSupplier.get(); DragPositioningCallbackUtility.setPositionOnDrag(mDesktopWindowDecoration, @@ -143,7 +148,7 @@ public class VeiledResizeTaskPositioner implements DragPositioningCallback, final WindowContainerTransaction wct = new WindowContainerTransaction(); wct.setBounds(mDesktopWindowDecoration.mTaskInfo.token, mRepositionTaskBounds); mTransitions.startTransition(TRANSIT_CHANGE, wct, this); - } else { + } else if (mVeilIsVisible) { // If bounds haven't changed, perform necessary veil reset here as startAnimation // won't be called. mDesktopWindowDecoration.hideResizeVeil(); @@ -163,6 +168,7 @@ public class VeiledResizeTaskPositioner implements DragPositioningCallback, mCtrlType = CTRL_TYPE_UNDEFINED; mTaskBoundsAtDragStart.setEmpty(); mRepositionStartPoint.set(0, 0); + mVeilIsVisible = false; return new Rect(mRepositionTaskBounds); } 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 d4b97ed55192..2acfd83084ab 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,6 +20,8 @@ import static android.app.AppCompatTaskInfo.CAMERA_COMPAT_CONTROL_DISMISSED; 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; import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; @@ -27,6 +29,7 @@ import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.doReturn; @@ -39,6 +42,7 @@ import android.app.AppCompatTaskInfo; import android.app.TaskInfo; import android.content.res.Configuration; import android.graphics.Rect; +import android.platform.test.flag.junit.SetFlagsRule; import android.testing.AndroidTestingRunner; import android.util.Pair; import android.view.DisplayInfo; @@ -50,6 +54,7 @@ import android.view.View; import androidx.test.filters.SmallTest; +import com.android.window.flags.Flags; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.common.DisplayLayout; @@ -59,6 +64,7 @@ import com.android.wm.shell.compatui.CompatUIController.CompatUIHintsState; import junit.framework.Assert; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; @@ -76,6 +82,8 @@ import java.util.function.Consumer; @RunWith(AndroidTestingRunner.class) @SmallTest public class CompatUIWindowManagerTest extends ShellTestCase { + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(DEVICE_DEFAULT); private static final int TASK_ID = 1; @@ -107,6 +115,7 @@ public class CompatUIWindowManagerTest extends ShellTestCase { 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(); @@ -199,6 +208,7 @@ public class CompatUIWindowManagerTest extends ShellTestCase { // No diff clearInvocations(mWindowManager); TaskInfo taskInfo = createTaskInfo(/* hasSizeCompat= */ true, CAMERA_COMPAT_CONTROL_HIDDEN); + doReturn(true).when(mWindowManager).shouldShowSizeCompatRestartButton(any()); assertTrue(mWindowManager.updateCompatInfo(taskInfo, mTaskListener, /* canShow= */ true)); verify(mWindowManager, never()).updateSurfacePosition(); @@ -284,6 +294,7 @@ public class CompatUIWindowManagerTest extends ShellTestCase { @Test public void testUpdateCompatInfoLayoutNotInflatedYet() { mWindowManager.mHasSizeCompat = true; + doReturn(true).when(mWindowManager).shouldShowSizeCompatRestartButton(any()); mWindowManager.createLayout(/* canShow= */ false); verify(mWindowManager, never()).inflateLayout(); @@ -353,6 +364,7 @@ public class CompatUIWindowManagerTest extends ShellTestCase { // 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); @@ -464,6 +476,37 @@ public class CompatUIWindowManagerTest extends ShellTestCase { Assert.assertTrue(mWindowManager.needsToBeRecreated(newTaskInfo, mTaskListener)); } + @Test + public void testShouldShowSizeCompatRestartButton() { + mSetFlagsRule.enableFlags(Flags.FLAG_ALLOW_HIDE_SCM_BUTTON); + + doReturn(86).when(mCompatUIConfiguration).getHideSizeCompatRestartButtonTolerance(); + mWindowManager = new CompatUIWindowManager(mContext, mTaskInfo, mSyncTransactionQueue, + mCallback, mTaskListener, new DisplayLayout(), new CompatUIHintsState(), + mCompatUIConfiguration, mOnRestartButtonClicked); + + // Simulate rotation of activity in square display + 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)); + + // Simulate exiting split screen/folding + taskInfo.appCompatTaskInfo.topActivityLetterboxWidth = 1000; + assertTrue(mWindowManager.shouldShowSizeCompatRestartButton(taskInfo)); + + // Simulate folding + taskInfo.configuration.windowConfiguration.setBounds(new Rect(0, 0, 1000, 2000)); + assertFalse(mWindowManager.shouldShowSizeCompatRestartButton(taskInfo)); + + taskInfo.appCompatTaskInfo.topActivityLetterboxWidth = 1000; + taskInfo.appCompatTaskInfo.topActivityLetterboxHeight = 500; + assertTrue(mWindowManager.shouldShowSizeCompatRestartButton(taskInfo)); + } + private static TaskInfo createTaskInfo(boolean hasSizeCompat, @AppCompatTaskInfo.CameraCompatControlState int cameraCompatControlState) { ActivityManager.RunningTaskInfo taskInfo = new ActivityManager.RunningTaskInfo(); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositionerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositionerTest.kt index 08412101c30c..86253f35a51d 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositionerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositionerTest.kt @@ -144,13 +144,13 @@ class VeiledResizeTaskPositionerTest : ShellTestCase() { } @Test - fun testDragResize_noMove_showsResizeVeil() { + fun testDragResize_noMove_doesNotShowResizeVeil() { taskPositioner.onDragPositioningStart( CTRL_TYPE_TOP or CTRL_TYPE_RIGHT, STARTING_BOUNDS.left.toFloat(), STARTING_BOUNDS.top.toFloat() ) - verify(mockDesktopWindowDecoration).showResizeVeil(STARTING_BOUNDS) + verify(mockDesktopWindowDecoration, never()).showResizeVeil(STARTING_BOUNDS) taskPositioner.onDragPositioningEnd( STARTING_BOUNDS.left.toFloat(), @@ -162,7 +162,7 @@ class VeiledResizeTaskPositionerTest : ShellTestCase() { (change.windowSetMask and WindowConfiguration.WINDOW_CONFIG_BOUNDS) != 0 && change.configuration.windowConfiguration.bounds == STARTING_BOUNDS}}, eq(taskPositioner)) - verify(mockDesktopWindowDecoration).hideResizeVeil() + verify(mockDesktopWindowDecoration, never()).hideResizeVeil() } @Test @@ -212,7 +212,6 @@ class VeiledResizeTaskPositionerTest : ShellTestCase() { STARTING_BOUNDS.right.toFloat(), STARTING_BOUNDS.top.toFloat() ) - verify(mockDesktopWindowDecoration).showResizeVeil(STARTING_BOUNDS) taskPositioner.onDragPositioningMove( STARTING_BOUNDS.right.toFloat() + 10, @@ -222,6 +221,7 @@ class VeiledResizeTaskPositionerTest : ShellTestCase() { val rectAfterMove = Rect(STARTING_BOUNDS) rectAfterMove.right += 10 rectAfterMove.top += 10 + verify(mockDesktopWindowDecoration).showResizeVeil(rectAfterMove) verify(mockShellTaskOrganizer, never()).applyTransaction(argThat { wct -> return@argThat wct.changes.any { (token, change) -> token == taskBinder && @@ -237,7 +237,7 @@ class VeiledResizeTaskPositionerTest : ShellTestCase() { val rectAfterEnd = Rect(rectAfterMove) rectAfterEnd.right += 10 rectAfterEnd.top += 10 - verify(mockDesktopWindowDecoration, times(2)).updateResizeVeil(any()) + verify(mockDesktopWindowDecoration).updateResizeVeil(any()) verify(mockTransitions).startTransition(eq(TRANSIT_CHANGE), argThat { wct -> return@argThat wct.changes.any { (token, change) -> token == taskBinder && @@ -253,7 +253,6 @@ class VeiledResizeTaskPositionerTest : ShellTestCase() { STARTING_BOUNDS.left.toFloat(), STARTING_BOUNDS.top.toFloat() ) - verify(mockDesktopWindowDecoration).showResizeVeil(STARTING_BOUNDS) taskPositioner.onDragPositioningMove( STARTING_BOUNDS.left.toFloat(), diff --git a/libs/androidfw/LoadedArsc.cpp b/libs/androidfw/LoadedArsc.cpp index c9d5e074271b..d9166a16cdea 100644 --- a/libs/androidfw/LoadedArsc.cpp +++ b/libs/androidfw/LoadedArsc.cpp @@ -21,6 +21,7 @@ #include <algorithm> #include <cstddef> #include <limits> +#include <optional> #include "android-base/logging.h" #include "android-base/stringprintf.h" @@ -50,7 +51,9 @@ namespace { // contiguous block of memory to store both the TypeSpec struct and // the Type structs. struct TypeSpecBuilder { - explicit TypeSpecBuilder(incfs::verified_map_ptr<ResTable_typeSpec> header) : header_(header) {} + explicit TypeSpecBuilder(incfs::verified_map_ptr<ResTable_typeSpec> header) : header_(header) { + type_entries.reserve(dtohs(header_->typesCount)); + } void AddType(incfs::verified_map_ptr<ResTable_type> type) { TypeSpec::TypeEntry& entry = type_entries.emplace_back(); @@ -59,6 +62,7 @@ struct TypeSpecBuilder { } TypeSpec Build() { + type_entries.shrink_to_fit(); return {header_, std::move(type_entries)}; } @@ -450,7 +454,8 @@ const LoadedPackage* LoadedArsc::GetPackageById(uint8_t package_id) const { std::unique_ptr<const LoadedPackage> LoadedPackage::Load(const Chunk& chunk, package_property_t property_flags) { ATRACE_NAME("LoadedPackage::Load"); - std::unique_ptr<LoadedPackage> loaded_package(new LoadedPackage()); + const bool optimize_name_lookups = (property_flags & PROPERTY_OPTIMIZE_NAME_LOOKUPS) != 0; + std::unique_ptr<LoadedPackage> loaded_package(new LoadedPackage(optimize_name_lookups)); // typeIdOffset was added at some point, but we still must recognize apps built before this // was added. @@ -499,7 +504,7 @@ std::unique_ptr<const LoadedPackage> LoadedPackage::Load(const Chunk& chunk, // A map of TypeSpec builders, each associated with an type index. // We use these to accumulate the set of Types available for a TypeSpec, and later build a single, // contiguous block of memory that holds all the Types together with the TypeSpec. - std::unordered_map<int, std::unique_ptr<TypeSpecBuilder>> type_builder_map; + std::unordered_map<int, std::optional<TypeSpecBuilder>> type_builder_map; ChunkIterator iter(chunk.data_ptr(), chunk.data_size()); while (iter.HasNext()) { @@ -567,14 +572,14 @@ std::unique_ptr<const LoadedPackage> LoadedPackage::Load(const Chunk& chunk, return {}; } - if (entry_count * sizeof(uint32_t) > chunk.data_size()) { + if (entry_count * sizeof(uint32_t) > child_chunk.data_size()) { LOG(ERROR) << "RES_TABLE_TYPE_SPEC_TYPE too small to hold entries."; return {}; } - std::unique_ptr<TypeSpecBuilder>& builder_ptr = type_builder_map[type_spec->id]; - if (builder_ptr == nullptr) { - builder_ptr = util::make_unique<TypeSpecBuilder>(type_spec.verified()); + auto& maybe_type_builder = type_builder_map[type_spec->id]; + if (!maybe_type_builder) { + maybe_type_builder.emplace(type_spec.verified()); loaded_package->resource_ids_.set(type_spec->id, entry_count); } else { LOG(WARNING) << StringPrintf("RES_TABLE_TYPE_SPEC_TYPE already defined for ID %02x", @@ -594,9 +599,9 @@ std::unique_ptr<const LoadedPackage> LoadedPackage::Load(const Chunk& chunk, } // Type chunks must be preceded by their TypeSpec chunks. - std::unique_ptr<TypeSpecBuilder>& builder_ptr = type_builder_map[type->id]; - if (builder_ptr != nullptr) { - builder_ptr->AddType(type.verified()); + auto& maybe_type_builder = type_builder_map[type->id]; + if (maybe_type_builder) { + maybe_type_builder->AddType(type.verified()); } else { LOG(ERROR) << StringPrintf( "RES_TABLE_TYPE_TYPE with ID %02x found without preceding RES_TABLE_TYPE_SPEC_TYPE.", diff --git a/libs/androidfw/ResourceTypes.cpp b/libs/androidfw/ResourceTypes.cpp index 4c992becda7c..2c99f1aa3675 100644 --- a/libs/androidfw/ResourceTypes.cpp +++ b/libs/androidfw/ResourceTypes.cpp @@ -447,15 +447,19 @@ Res_png_9patch* Res_png_9patch::deserialize(void* inData) // -------------------------------------------------------------------- // -------------------------------------------------------------------- -ResStringPool::ResStringPool() - : mError(NO_INIT), mOwnedData(NULL), mHeader(NULL), mCache(NULL) -{ +ResStringPool::ResStringPool() : mError(NO_INIT), mOwnedData(NULL), mHeader(NULL), mCache(NULL) { } -ResStringPool::ResStringPool(const void* data, size_t size, bool copyData) - : mError(NO_INIT), mOwnedData(NULL), mHeader(NULL), mCache(NULL) -{ - setTo(data, size, copyData); +ResStringPool::ResStringPool(bool optimize_name_lookups) : ResStringPool() { + if (optimize_name_lookups) { + mIndexLookupCache.emplace(); + } +} + +ResStringPool::ResStringPool(const void* data, size_t size, bool copyData, + bool optimize_name_lookups) + : ResStringPool(optimize_name_lookups) { + setTo(data, size, copyData); } ResStringPool::~ResStringPool() @@ -683,6 +687,14 @@ status_t ResStringPool::setTo(incfs::map_ptr<void> data, size_t size, bool copyD mStylePoolSize = 0; } + if (mIndexLookupCache) { + if ((mHeader->flags & ResStringPool_header::UTF8_FLAG) != 0) { + mIndexLookupCache->first.reserve(mHeader->stringCount); + } else { + mIndexLookupCache->second.reserve(mHeader->stringCount); + } + } + return (mError=NO_ERROR); } @@ -708,6 +720,10 @@ void ResStringPool::uninit() free(mOwnedData); mOwnedData = NULL; } + if (mIndexLookupCache) { + mIndexLookupCache->first.clear(); + mIndexLookupCache->second.clear(); + } } /** @@ -824,11 +840,11 @@ base::expected<StringPiece16, NullOrIOError> ResStringPool::stringAt(size_t idx) // encLen must be less than 0x7FFF due to encoding. if ((uint32_t)(u8str+*u8len-strings) < mStringPoolSize) { - AutoMutex lock(mDecodeLock); + AutoMutex lock(mCachesLock); - if (mCache != NULL && mCache[idx] != NULL) { - return StringPiece16(mCache[idx], *u16len); - } + if (mCache != NULL && mCache[idx] != NULL) { + return StringPiece16(mCache[idx], *u16len); + } // Retrieve the actual length of the utf8 string if the // encoded length was truncated @@ -1093,12 +1109,24 @@ base::expected<size_t, NullOrIOError> ResStringPool::indexOfString(const char16_ // block, start searching at the back. String8 str8(str, strLen); const size_t str8Len = str8.size(); + std::optional<AutoMutex> cacheLock; + if (mIndexLookupCache) { + cacheLock.emplace(mCachesLock); + if (auto it = mIndexLookupCache->first.find(std::string_view(str8)); + it != mIndexLookupCache->first.end()) { + return it->second; + } + } + for (int i=mHeader->stringCount-1; i>=0; i--) { const base::expected<StringPiece, NullOrIOError> s = string8At(i); if (UNLIKELY(IsIOError(s))) { return base::unexpected(s.error()); } if (s.has_value()) { + if (mIndexLookupCache) { + mIndexLookupCache->first.insert({*s, i}); + } if (kDebugStringPoolNoisy) { ALOGI("Looking at %s, i=%d\n", s->data(), i); } @@ -1151,20 +1179,32 @@ base::expected<size_t, NullOrIOError> ResStringPool::indexOfString(const char16_ // most often this happens because we want to get IDs for style // span tags; since those always appear at the end of the string // block, start searching at the back. + std::optional<AutoMutex> cacheLock; + if (mIndexLookupCache) { + cacheLock.emplace(mCachesLock); + if (auto it = mIndexLookupCache->second.find({str, strLen}); + it != mIndexLookupCache->second.end()) { + return it->second; + } + } for (int i=mHeader->stringCount-1; i>=0; i--) { const base::expected<StringPiece16, NullOrIOError> s = stringAt(i); if (UNLIKELY(IsIOError(s))) { return base::unexpected(s.error()); } if (kDebugStringPoolNoisy) { - ALOGI("Looking at %s, i=%d\n", String8(s->data(), s->size()).c_str(), i); + ALOGI("Looking16 at %s, i=%d\n", String8(s->data(), s->size()).c_str(), i); } - if (s.has_value() && strLen == s->size() && - strzcmp16(s->data(), s->size(), str, strLen) == 0) { + if (s.has_value()) { + if (mIndexLookupCache) { + mIndexLookupCache->second.insert({*s, i}); + } + if (strLen == s->size() && strzcmp16(s->data(), s->size(), str, strLen) == 0) { if (kDebugStringPoolNoisy) { - ALOGI("MATCH!"); + ALOGI("MATCH16!"); } return i; + } } } } diff --git a/libs/androidfw/include/androidfw/Idmap.h b/libs/androidfw/include/androidfw/Idmap.h index 60689128dffb..d9f7c2a1ac19 100644 --- a/libs/androidfw/include/androidfw/Idmap.h +++ b/libs/androidfw/include/androidfw/Idmap.h @@ -71,7 +71,7 @@ class OverlayDynamicRefTable : public DynamicRefTable { // Rewrites a compile-time overlay resource id to the runtime resource id of corresponding target // resource. - virtual status_t lookupResourceIdNoRewrite(uint32_t* resId) const; + status_t lookupResourceIdNoRewrite(uint32_t* resId) const; const Idmap_data_header* data_header_; const Idmap_overlay_entry* entries_; diff --git a/libs/androidfw/include/androidfw/LoadedArsc.h b/libs/androidfw/include/androidfw/LoadedArsc.h index 3a7287187781..413b27829474 100644 --- a/libs/androidfw/include/androidfw/LoadedArsc.h +++ b/libs/androidfw/include/androidfw/LoadedArsc.h @@ -99,6 +99,9 @@ enum : package_property_t { // The apk assets only contain the overlayable declarations information. PROPERTY_ONLY_OVERLAYABLES = 1U << 5U, + + // Optimize the resource lookups by name via an in-memory lookup table. + PROPERTY_OPTIMIZE_NAME_LOOKUPS = 1U << 6U, }; struct OverlayableInfo { @@ -285,7 +288,9 @@ class LoadedPackage { private: DISALLOW_COPY_AND_ASSIGN(LoadedPackage); - LoadedPackage() = default; + explicit LoadedPackage(bool optimize_name_lookups = false) + : type_string_pool_(optimize_name_lookups), key_string_pool_(optimize_name_lookups) { + } ResStringPool type_string_pool_; ResStringPool key_string_pool_; diff --git a/libs/androidfw/include/androidfw/ResourceTypes.h b/libs/androidfw/include/androidfw/ResourceTypes.h index c0514fdff469..3d1403d0e039 100644 --- a/libs/androidfw/include/androidfw/ResourceTypes.h +++ b/libs/androidfw/include/androidfw/ResourceTypes.h @@ -22,27 +22,28 @@ #include <android-base/expected.h> #include <android-base/unique_fd.h> - +#include <android/configuration.h> #include <androidfw/Asset.h> #include <androidfw/Errors.h> #include <androidfw/LocaleData.h> #include <androidfw/StringPiece.h> #include <utils/ByteOrder.h> #include <utils/Errors.h> +#include <utils/KeyedVector.h> #include <utils/String16.h> #include <utils/Vector.h> -#include <utils/KeyedVector.h> - #include <utils/threads.h> #include <stdint.h> #include <sys/types.h> -#include <android/configuration.h> - #include <array> #include <map> #include <memory> +#include <optional> +#include <string_view> +#include <unordered_map> +#include <utility> namespace android { @@ -512,23 +513,24 @@ struct ResStringPool_span class ResStringPool { public: - ResStringPool(); - ResStringPool(const void* data, size_t size, bool copyData=false); - virtual ~ResStringPool(); + ResStringPool(); + explicit ResStringPool(bool optimize_name_lookups); + ResStringPool(const void* data, size_t size, bool copyData = false, + bool optimize_name_lookups = false); + virtual ~ResStringPool(); - void setToEmpty(); - status_t setTo(incfs::map_ptr<void> data, size_t size, bool copyData=false); + void setToEmpty(); + status_t setTo(incfs::map_ptr<void> data, size_t size, bool copyData = false); - status_t getError() const; + status_t getError() const; - void uninit(); + void uninit(); - // Return string entry as UTF16; if the pool is UTF8, the string will - // be converted before returning. - inline base::expected<StringPiece16, NullOrIOError> stringAt( - const ResStringPool_ref& ref) const { - return stringAt(ref.index); - } + // Return string entry as UTF16; if the pool is UTF8, the string will + // be converted before returning. + inline base::expected<StringPiece16, NullOrIOError> stringAt(const ResStringPool_ref& ref) const { + return stringAt(ref.index); + } virtual base::expected<StringPiece16, NullOrIOError> stringAt(size_t idx) const; // Note: returns null if the string pool is not UTF8. @@ -557,7 +559,7 @@ private: void* mOwnedData; incfs::verified_map_ptr<ResStringPool_header> mHeader; size_t mSize; - mutable Mutex mDecodeLock; + mutable Mutex mCachesLock; incfs::map_ptr<uint32_t> mEntries; incfs::map_ptr<uint32_t> mEntryStyles; incfs::map_ptr<void> mStrings; @@ -566,6 +568,10 @@ private: incfs::map_ptr<uint32_t> mStyles; uint32_t mStylePoolSize; // number of uint32_t + mutable std::optional<std::pair<std::unordered_map<std::string_view, int>, + std::unordered_map<std::u16string_view, int>>> + mIndexLookupCache; + base::expected<StringPiece, NullOrIOError> stringDecodeAt( size_t idx, incfs::map_ptr<uint8_t> str, size_t encLen) const; }; @@ -1401,8 +1407,8 @@ struct ResTable_typeSpec // Must be 0. uint8_t res0; - // Must be 0. - uint16_t res1; + // Used to be reserved, if >0 specifies the number of ResTable_type entries for this spec. + uint16_t typesCount; // Number of uint32_t entry configuration masks that follow. uint32_t entryCount; diff --git a/libs/hostgraphics/gui/Surface.h b/libs/hostgraphics/gui/Surface.h index de1ba00211d3..2573931c8543 100644 --- a/libs/hostgraphics/gui/Surface.h +++ b/libs/hostgraphics/gui/Surface.h @@ -50,6 +50,8 @@ public: virtual int unlockAndPost() { return 0; } virtual int query(int what, int* value) const { return 0; } + virtual void destroy() {} + protected: virtual ~Surface() {} diff --git a/libs/hwui/Android.bp b/libs/hwui/Android.bp index eebf8aabd89c..b40b73c111d0 100644 --- a/libs/hwui/Android.bp +++ b/libs/hwui/Android.bp @@ -716,7 +716,6 @@ cc_test { ], shared_libs: [ "libmemunreachable", - "server_configurable_flags", ], srcs: [ "tests/unit/main.cpp", diff --git a/libs/hwui/DeviceInfo.h b/libs/hwui/DeviceInfo.h index d4af0872e31e..a5a841e07d7a 100644 --- a/libs/hwui/DeviceInfo.h +++ b/libs/hwui/DeviceInfo.h @@ -23,6 +23,7 @@ #include <mutex> +#include "Properties.h" #include "utils/Macros.h" namespace android { @@ -60,7 +61,13 @@ public: static void setWideColorDataspace(ADataSpace dataspace); static void setSupportFp16ForHdr(bool supportFp16ForHdr); - static bool isSupportFp16ForHdr() { return get()->mSupportFp16ForHdr; }; + static bool isSupportFp16ForHdr() { + if (!Properties::hdr10bitPlus) { + return false; + } + + return get()->mSupportFp16ForHdr; + }; static void setSupportMixedColorSpaces(bool supportMixedColorSpaces); static bool isSupportMixedColorSpaces() { return get()->mSupportMixedColorSpaces; }; diff --git a/libs/hwui/FeatureFlags.h b/libs/hwui/FeatureFlags.h index 00d049cde925..6ebfc63bdf26 100644 --- a/libs/hwui/FeatureFlags.h +++ b/libs/hwui/FeatureFlags.h @@ -41,6 +41,14 @@ inline bool deprecate_ui_fonts() { #endif // __ANDROID__ } +inline bool inter_character_justification() { +#ifdef __ANDROID__ + return com_android_text_flags_inter_character_justification(); +#else + return true; +#endif // __ANDROID__ +} + } // namespace text_feature } // namespace android diff --git a/libs/hwui/HardwareBitmapUploader.cpp b/libs/hwui/HardwareBitmapUploader.cpp index 16de21def0e3..71f7926930fc 100644 --- a/libs/hwui/HardwareBitmapUploader.cpp +++ b/libs/hwui/HardwareBitmapUploader.cpp @@ -379,7 +379,7 @@ static FormatInfo determineFormat(const SkBitmap& skBitmap, bool usingGL) { case kAlpha_8_SkColorType: formatInfo.isSupported = HardwareBitmapUploader::hasAlpha8Support(); formatInfo.bufferFormat = AHARDWAREBUFFER_FORMAT_R8_UNORM; - formatInfo.format = GL_R8; + formatInfo.format = GL_RED; formatInfo.type = GL_UNSIGNED_BYTE; formatInfo.vkFormat = VK_FORMAT_R8_UNORM; break; diff --git a/libs/hwui/Properties.cpp b/libs/hwui/Properties.cpp index d58c872dbc56..755332ff66fd 100644 --- a/libs/hwui/Properties.cpp +++ b/libs/hwui/Properties.cpp @@ -38,6 +38,9 @@ namespace hwui_flags { constexpr bool clip_surfaceviews() { return false; } +constexpr bool hdr_10bit_plus() { + return false; +} } // namespace hwui_flags #endif @@ -105,6 +108,7 @@ bool Properties::isSystemOrPersistent = false; float Properties::maxHdrHeadroomOn8bit = 5.f; // TODO: Refine this number bool Properties::clipSurfaceViews = false; +bool Properties::hdr10bitPlus = false; StretchEffectBehavior Properties::stretchEffectBehavior = StretchEffectBehavior::ShaderHWUI; @@ -177,6 +181,7 @@ bool Properties::load() { clipSurfaceViews = base::GetBoolProperty("debug.hwui.clip_surfaceviews", hwui_flags::clip_surfaceviews()); + hdr10bitPlus = hwui_flags::hdr_10bit_plus(); return (prevDebugLayersUpdates != debugLayersUpdates) || (prevDebugOverdraw != debugOverdraw); } diff --git a/libs/hwui/Properties.h b/libs/hwui/Properties.h index b956facf6f90..ec53070f6cb8 100644 --- a/libs/hwui/Properties.h +++ b/libs/hwui/Properties.h @@ -336,6 +336,7 @@ public: static float maxHdrHeadroomOn8bit; static bool clipSurfaceViews; + static bool hdr10bitPlus; static StretchEffectBehavior getStretchEffectBehavior() { return stretchEffectBehavior; diff --git a/libs/hwui/hwui/MinikinSkia.cpp b/libs/hwui/hwui/MinikinSkia.cpp index f4ee36ec66d1..bbb142014ed8 100644 --- a/libs/hwui/hwui/MinikinSkia.cpp +++ b/libs/hwui/hwui/MinikinSkia.cpp @@ -131,11 +131,6 @@ std::shared_ptr<minikin::MinikinFont> MinikinFontSkia::createFontWithVariation( const std::vector<minikin::FontVariation>& variations) const { SkFontArguments args; - int ttcIndex; - std::unique_ptr<SkStreamAsset> stream(mTypeface->openStream(&ttcIndex)); - LOG_ALWAYS_FATAL_IF(stream == nullptr, "openStream failed"); - - args.setCollectionIndex(ttcIndex); std::vector<SkFontArguments::VariationPosition::Coordinate> skVariation; skVariation.resize(variations.size()); for (size_t i = 0; i < variations.size(); i++) { @@ -143,11 +138,10 @@ std::shared_ptr<minikin::MinikinFont> MinikinFontSkia::createFontWithVariation( skVariation[i].value = SkFloatToScalar(variations[i].value); } args.setVariationDesignPosition({skVariation.data(), static_cast<int>(skVariation.size())}); - sk_sp<SkFontMgr> fm = android::FreeTypeFontMgr(); - sk_sp<SkTypeface> face(fm->makeFromStream(std::move(stream), args)); + sk_sp<SkTypeface> face = mTypeface->makeClone(args); return std::make_shared<MinikinFontSkia>(std::move(face), mSourceId, mFontData, mFontSize, - mFilePath, ttcIndex, variations); + mFilePath, mTtcIndex, variations); } // hinting<<16 | edging<<8 | bools:5bits diff --git a/libs/hwui/hwui/MinikinUtils.cpp b/libs/hwui/hwui/MinikinUtils.cpp index 833069f363c8..56133699d5f5 100644 --- a/libs/hwui/hwui/MinikinUtils.cpp +++ b/libs/hwui/hwui/MinikinUtils.cpp @@ -72,10 +72,13 @@ minikin::Layout MinikinUtils::doLayout(const Paint* paint, minikin::Bidi bidiFla const minikin::Range contextRange(contextStart, contextStart + contextCount); const minikin::StartHyphenEdit startHyphen = paint->getStartHyphenEdit(); const minikin::EndHyphenEdit endHyphen = paint->getEndHyphenEdit(); + const minikin::RunFlag minikinRunFlag = text_feature::inter_character_justification() + ? paint->getRunFlag() + : minikin::RunFlag::NONE; if (mt == nullptr) { return minikin::Layout(textBuf.substr(contextRange), range - contextStart, bidiFlags, - minikinPaint, startHyphen, endHyphen); + minikinPaint, startHyphen, endHyphen, minikinRunFlag); } else { return mt->buildLayout(textBuf, range, contextRange, minikinPaint, startHyphen, endHyphen); } @@ -102,9 +105,12 @@ float MinikinUtils::measureText(const Paint* paint, minikin::Bidi bidiFlags, const minikin::Range range(start, start + count); const minikin::StartHyphenEdit startHyphen = paint->getStartHyphenEdit(); const minikin::EndHyphenEdit endHyphen = paint->getEndHyphenEdit(); + const minikin::RunFlag minikinRunFlag = text_feature::inter_character_justification() + ? paint->getRunFlag() + : minikin::RunFlag::NONE; return minikin::Layout::measureText(textBuf, range, bidiFlags, minikinPaint, startHyphen, - endHyphen, advances, bounds, clusterCount); + endHyphen, advances, bounds, clusterCount, minikinRunFlag); } minikin::MinikinExtent MinikinUtils::getFontExtent(const Paint* paint, minikin::Bidi bidiFlags, diff --git a/libs/hwui/hwui/Paint.h b/libs/hwui/hwui/Paint.h index ef4dce57bf46..708f96e5a070 100644 --- a/libs/hwui/hwui/Paint.h +++ b/libs/hwui/hwui/Paint.h @@ -25,6 +25,7 @@ #include <minikin/FontFamily.h> #include <minikin/FontFeature.h> #include <minikin/Hyphenator.h> +#include <minikin/Layout.h> #include <string> @@ -144,6 +145,9 @@ public: bool isDevKern() const { return mDevKern; } void setDevKern(bool d) { mDevKern = d; } + minikin::RunFlag getRunFlag() const { return mRunFlag; } + void setRunFlag(minikin::RunFlag runFlag) { mRunFlag = runFlag; } + // Deprecated -- bitmapshaders will be taking this flag explicitly bool isFilterBitmap() const { return mFilterBitmap; } void setFilterBitmap(bool filter) { mFilterBitmap = filter; } @@ -188,6 +192,7 @@ private: bool mStrikeThru = false; bool mUnderline = false; bool mDevKern = false; + minikin::RunFlag mRunFlag = minikin::RunFlag::NONE; }; } // namespace android diff --git a/libs/hwui/hwui/PaintImpl.cpp b/libs/hwui/hwui/PaintImpl.cpp index aac928f85924..c32ea01db7b7 100644 --- a/libs/hwui/hwui/PaintImpl.cpp +++ b/libs/hwui/hwui/PaintImpl.cpp @@ -47,8 +47,8 @@ Paint::Paint(const Paint& paint) , mFilterBitmap(paint.mFilterBitmap) , mStrikeThru(paint.mStrikeThru) , mUnderline(paint.mUnderline) - , mDevKern(paint.mDevKern) {} - + , mDevKern(paint.mDevKern) + , mRunFlag(paint.mRunFlag) {} Paint::~Paint() {} @@ -68,21 +68,19 @@ Paint& Paint::operator=(const Paint& other) { mStrikeThru = other.mStrikeThru; mUnderline = other.mUnderline; mDevKern = other.mDevKern; + mRunFlag = other.mRunFlag; return *this; } bool operator==(const Paint& a, const Paint& b) { - return static_cast<const SkPaint&>(a) == static_cast<const SkPaint&>(b) && - a.mFont == b.mFont && - a.mLooper == b.mLooper && - a.mLetterSpacing == b.mLetterSpacing && a.mWordSpacing == b.mWordSpacing && - a.mFontFeatureSettings == b.mFontFeatureSettings && + return static_cast<const SkPaint&>(a) == static_cast<const SkPaint&>(b) && a.mFont == b.mFont && + a.mLooper == b.mLooper && a.mLetterSpacing == b.mLetterSpacing && + a.mWordSpacing == b.mWordSpacing && a.mFontFeatureSettings == b.mFontFeatureSettings && a.mMinikinLocaleListId == b.mMinikinLocaleListId && a.mFamilyVariant == b.mFamilyVariant && a.mHyphenEdit == b.mHyphenEdit && a.mTypeface == b.mTypeface && a.mAlign == b.mAlign && - a.mFilterBitmap == b.mFilterBitmap && - a.mStrikeThru == b.mStrikeThru && a.mUnderline == b.mUnderline && - a.mDevKern == b.mDevKern; + a.mFilterBitmap == b.mFilterBitmap && a.mStrikeThru == b.mStrikeThru && + a.mUnderline == b.mUnderline && a.mDevKern == b.mDevKern && a.mRunFlag == b.mRunFlag; } void Paint::reset() { @@ -96,6 +94,7 @@ void Paint::reset() { mStrikeThru = false; mUnderline = false; mDevKern = false; + mRunFlag = minikin::RunFlag::NONE; } void Paint::setLooper(sk_sp<BlurDrawLooper> looper) { @@ -133,6 +132,8 @@ static const uint32_t sForceAutoHinting = 0x800; // flags related to minikin::Paint static const uint32_t sUnderlineFlag = 0x08; static const uint32_t sStrikeThruFlag = 0x10; +static const uint32_t sTextRunLeftEdge = 0x2000; +static const uint32_t sTextRunRightEdge = 0x4000; // flags no longer supported on native side (but mirrored for compatibility) static const uint32_t sDevKernFlag = 0x100; @@ -186,6 +187,12 @@ uint32_t Paint::getJavaFlags() const { flags |= -(int)mUnderline & sUnderlineFlag; flags |= -(int)mDevKern & sDevKernFlag; flags |= -(int)mFilterBitmap & sFilterBitmapFlag; + if (mRunFlag & minikin::RunFlag::LEFT_EDGE) { + flags |= sTextRunLeftEdge; + } + if (mRunFlag & minikin::RunFlag::RIGHT_EDGE) { + flags |= sTextRunRightEdge; + } return flags; } @@ -196,6 +203,15 @@ void Paint::setJavaFlags(uint32_t flags) { mUnderline = (flags & sUnderlineFlag) != 0; mDevKern = (flags & sDevKernFlag) != 0; mFilterBitmap = (flags & sFilterBitmapFlag) != 0; + + std::underlying_type<minikin::RunFlag>::type rawFlag = minikin::RunFlag::NONE; + if (flags & sTextRunLeftEdge) { + rawFlag |= minikin::RunFlag::LEFT_EDGE; + } + if (flags & sTextRunRightEdge) { + rawFlag |= minikin::RunFlag::RIGHT_EDGE; + } + mRunFlag = static_cast<minikin::RunFlag>(rawFlag); } } // namespace android diff --git a/libs/hwui/jni/Paint.cpp b/libs/hwui/jni/Paint.cpp index 58d9d8b9def3..286f06a6bad8 100644 --- a/libs/hwui/jni/Paint.cpp +++ b/libs/hwui/jni/Paint.cpp @@ -578,16 +578,6 @@ namespace PaintGlue { return result; } - // This method is kept for old Robolectric JNI signature used by SystemUIGoogleRoboRNGTests. - static jfloat getRunCharacterAdvance___CIIIIZI_FI_F_ForRobolectric( - JNIEnv* env, jclass cls, jlong paintHandle, jcharArray text, jint start, jint end, - jint contextStart, jint contextEnd, jboolean isRtl, jint offset, jfloatArray advances, - jint advancesIndex, jobject drawBounds) { - return getRunCharacterAdvance___CIIIIZI_FI_F(env, cls, paintHandle, text, start, end, - contextStart, contextEnd, isRtl, offset, - advances, advancesIndex, drawBounds, nullptr); - } - static jint doOffsetForAdvance(const Paint* paint, const Typeface* typeface, const jchar buf[], jint start, jint count, jint bufSize, jboolean isRtl, jfloat advance) { minikin::Bidi bidiFlags = isRtl ? minikin::Bidi::FORCE_RTL : minikin::Bidi::FORCE_LTR; @@ -1163,8 +1153,6 @@ static const JNINativeMethod methods[] = { {"nGetRunCharacterAdvance", "(J[CIIIIZI[FILandroid/graphics/RectF;Landroid/graphics/Paint$RunInfo;)F", (void*)PaintGlue::getRunCharacterAdvance___CIIIIZI_FI_F}, - {"nGetRunCharacterAdvance", "(J[CIIIIZI[FILandroid/graphics/RectF;)F", - (void*)PaintGlue::getRunCharacterAdvance___CIIIIZI_FI_F_ForRobolectric}, {"nGetOffsetForAdvance", "(J[CIIIIZF)I", (void*)PaintGlue::getOffsetForAdvance___CIIIIZF_I}, {"nGetFontMetricsIntForText", "(J[CIIIIZLandroid/graphics/Paint$FontMetricsInt;)V", (void*)PaintGlue::getFontMetricsIntForText___C}, diff --git a/libs/hwui/jni/android_graphics_Canvas.cpp b/libs/hwui/jni/android_graphics_Canvas.cpp index 8ba750372d18..d5725935551a 100644 --- a/libs/hwui/jni/android_graphics_Canvas.cpp +++ b/libs/hwui/jni/android_graphics_Canvas.cpp @@ -613,6 +613,12 @@ static void drawTextChars(JNIEnv* env, jobject, jlong canvasHandle, jcharArray c Paint* paint = reinterpret_cast<Paint*>(paintHandle); const Typeface* typeface = paint->getAndroidTypeface(); ScopedCharArrayRO text(env, charArray); + + // The drawText API is designed to draw entire line, so ignore the text run flag and draw the + // text as entire line mode. + const minikin::RunFlag originalRunFlag = paint->getRunFlag(); + paint->setRunFlag(minikin::RunFlag::WHOLE_LINE); + // drawTextString and drawTextChars doesn't use context info get_canvas(canvasHandle)->drawText( text.get() + index, count, // text buffer @@ -620,6 +626,7 @@ static void drawTextChars(JNIEnv* env, jobject, jlong canvasHandle, jcharArray c 0, count, // context range x, y, // draw position static_cast<minikin::Bidi>(bidiFlags), *paint, typeface, nullptr /* measured text */); + paint->setRunFlag(originalRunFlag); } static void drawTextString(JNIEnv* env, jobject, jlong canvasHandle, jstring strObj, @@ -629,6 +636,12 @@ static void drawTextString(JNIEnv* env, jobject, jlong canvasHandle, jstring str Paint* paint = reinterpret_cast<Paint*>(paintHandle); const Typeface* typeface = paint->getAndroidTypeface(); const int count = end - start; + + // The drawText API is designed to draw entire line, so ignore the text run flag and draw the + // text as entire line mode. + const minikin::RunFlag originalRunFlag = paint->getRunFlag(); + paint->setRunFlag(minikin::RunFlag::WHOLE_LINE); + // drawTextString and drawTextChars doesn't use context info get_canvas(canvasHandle)->drawText( text.get() + start, count, // text buffer @@ -636,6 +649,7 @@ static void drawTextString(JNIEnv* env, jobject, jlong canvasHandle, jstring str 0, count, // context range x, y, // draw position static_cast<minikin::Bidi>(bidiFlags), *paint, typeface, nullptr /* measured text */); + paint->setRunFlag(originalRunFlag); } static void drawTextRunChars(JNIEnv* env, jobject, jlong canvasHandle, jcharArray charArray, @@ -681,9 +695,15 @@ static void drawTextOnPathChars(JNIEnv* env, jobject, jlong canvasHandle, jcharA jchar* jchars = env->GetCharArrayElements(text, NULL); + // The drawText API is designed to draw entire line, so ignore the text run flag and draw the + // text as entire line mode. + const minikin::RunFlag originalRunFlag = paint->getRunFlag(); + paint->setRunFlag(minikin::RunFlag::WHOLE_LINE); + get_canvas(canvasHandle)->drawTextOnPath(jchars + index, count, static_cast<minikin::Bidi>(bidiFlags), *path, hOffset, vOffset, *paint, typeface); + paint->setRunFlag(originalRunFlag); env->ReleaseCharArrayElements(text, jchars, 0); } @@ -697,9 +717,15 @@ static void drawTextOnPathString(JNIEnv* env, jobject, jlong canvasHandle, jstri const jchar* jchars = env->GetStringChars(text, NULL); int count = env->GetStringLength(text); + // The drawText API is designed to draw entire line, so ignore the text run flag and draw the + // text as entire line mode. + const minikin::RunFlag originalRunFlag = paint->getRunFlag(); + paint->setRunFlag(minikin::RunFlag::WHOLE_LINE); + get_canvas(canvasHandle)->drawTextOnPath(jchars, count, static_cast<minikin::Bidi>(bidiFlags), *path, hOffset, vOffset, *paint, typeface); + paint->setRunFlag(originalRunFlag); env->ReleaseStringChars(text, jchars); } diff --git a/libs/hwui/pipeline/skia/SkiaPipeline.cpp b/libs/hwui/pipeline/skia/SkiaPipeline.cpp index e0f1f6ef44be..326b6ed77fe0 100644 --- a/libs/hwui/pipeline/skia/SkiaPipeline.cpp +++ b/libs/hwui/pipeline/skia/SkiaPipeline.cpp @@ -650,9 +650,14 @@ void SkiaPipeline::setSurfaceColorProperties(ColorMode colorMode) { mSurfaceColorSpace = DeviceInfo::get()->getWideColorSpace(); break; case ColorMode::Hdr: - mSurfaceColorType = SkColorType::kN32_SkColorType; - mSurfaceColorSpace = SkColorSpace::MakeRGB( - GetExtendedTransferFunction(mTargetSdrHdrRatio), SkNamedGamut::kDisplayP3); + if (DeviceInfo::get()->isSupportFp16ForHdr()) { + mSurfaceColorType = SkColorType::kRGBA_F16_SkColorType; + mSurfaceColorSpace = SkColorSpace::MakeSRGB(); + } else { + mSurfaceColorType = SkColorType::kN32_SkColorType; + mSurfaceColorSpace = SkColorSpace::MakeRGB( + GetExtendedTransferFunction(mTargetSdrHdrRatio), SkNamedGamut::kDisplayP3); + } break; case ColorMode::Hdr10: mSurfaceColorType = SkColorType::kRGBA_1010102_SkColorType; @@ -669,8 +674,13 @@ void SkiaPipeline::setSurfaceColorProperties(ColorMode colorMode) { void SkiaPipeline::setTargetSdrHdrRatio(float ratio) { if (mColorMode == ColorMode::Hdr || mColorMode == ColorMode::Hdr10) { mTargetSdrHdrRatio = ratio; - mSurfaceColorSpace = SkColorSpace::MakeRGB(GetExtendedTransferFunction(mTargetSdrHdrRatio), - SkNamedGamut::kDisplayP3); + + if (mColorMode == ColorMode::Hdr && DeviceInfo::get()->isSupportFp16ForHdr()) { + mSurfaceColorSpace = SkColorSpace::MakeSRGB(); + } else { + mSurfaceColorSpace = SkColorSpace::MakeRGB( + GetExtendedTransferFunction(mTargetSdrHdrRatio), SkNamedGamut::kDisplayP3); + } } else { mTargetSdrHdrRatio = 1.f; } diff --git a/libs/hwui/renderthread/CacheManager.cpp b/libs/hwui/renderthread/CacheManager.cpp index eb4d4948dc53..ac2a9366a1f6 100644 --- a/libs/hwui/renderthread/CacheManager.cpp +++ b/libs/hwui/renderthread/CacheManager.cpp @@ -124,24 +124,19 @@ void CacheManager::trimMemory(TrimLevel mode) { // flush and submit all work to the gpu and wait for it to finish mGrContext->flushAndSubmit(GrSyncCpu::kYes); - switch (mode) { - case TrimLevel::BACKGROUND: - mGrContext->freeGpuResources(); - SkGraphics::PurgeAllCaches(); - mRenderThread.destroyRenderingContext(); - break; - case TrimLevel::UI_HIDDEN: - // Here we purge all the unlocked scratch resources and then toggle the resources cache - // limits between the background and max amounts. This causes the unlocked resources - // that have persistent data to be purged in LRU order. - mGrContext->setResourceCacheLimit(mBackgroundResourceBytes); - SkGraphics::SetFontCacheLimit(mBackgroundCpuFontCacheBytes); - mGrContext->purgeUnlockedResources(toSkiaEnum(mMemoryPolicy.purgeScratchOnly)); - mGrContext->setResourceCacheLimit(mMaxResourceBytes); - SkGraphics::SetFontCacheLimit(mMaxCpuFontCacheBytes); - break; - default: - break; + if (mode >= TrimLevel::BACKGROUND) { + mGrContext->freeGpuResources(); + SkGraphics::PurgeAllCaches(); + mRenderThread.destroyRenderingContext(); + } else if (mode == TrimLevel::UI_HIDDEN) { + // Here we purge all the unlocked scratch resources and then toggle the resources cache + // limits between the background and max amounts. This causes the unlocked resources + // that have persistent data to be purged in LRU order. + mGrContext->setResourceCacheLimit(mBackgroundResourceBytes); + SkGraphics::SetFontCacheLimit(mBackgroundCpuFontCacheBytes); + mGrContext->purgeUnlockedResources(toSkiaEnum(mMemoryPolicy.purgeScratchOnly)); + mGrContext->setResourceCacheLimit(mMaxResourceBytes); + SkGraphics::SetFontCacheLimit(mMaxCpuFontCacheBytes); } } diff --git a/libs/hwui/renderthread/EglManager.cpp b/libs/hwui/renderthread/EglManager.cpp index facf30b83b07..2904dfe76f40 100644 --- a/libs/hwui/renderthread/EglManager.cpp +++ b/libs/hwui/renderthread/EglManager.cpp @@ -441,22 +441,32 @@ Result<EGLSurface, EGLint> EglManager::createSurface(EGLNativeWindowType window, colorMode = ColorMode::Default; } - if (DeviceInfo::get()->getWideColorType() == kRGBA_F16_SkColorType) { + // TODO: maybe we want to get rid of the WCG check if overlay properties just works? + const bool canUseFp16 = DeviceInfo::get()->isSupportFp16ForHdr() || + DeviceInfo::get()->getWideColorType() == kRGBA_F16_SkColorType; + + if (canUseFp16) { if (mEglConfigF16 == EGL_NO_CONFIG_KHR) { colorMode = ColorMode::Default; } else { config = mEglConfigF16; } } + if (EglExtensions.glColorSpace) { attribs[0] = EGL_GL_COLORSPACE_KHR; switch (colorMode) { case ColorMode::Default: attribs[1] = EGL_GL_COLORSPACE_LINEAR_KHR; break; + case ColorMode::Hdr: + if (canUseFp16) { + attribs[1] = EGL_GL_COLORSPACE_SCRGB_EXT; + break; + // No fp16 support so fallthrough to HDR10 + } // We don't have an EGL colorspace for extended range P3 that's used for HDR // So override it after configuring the EGL context - case ColorMode::Hdr: case ColorMode::Hdr10: overrideWindowDataSpaceForHdr = true; attribs[1] = EGL_GL_COLORSPACE_DISPLAY_P3_PASSTHROUGH_EXT; diff --git a/media/java/android/media/AudioFormat.java b/media/java/android/media/AudioFormat.java index b002bbf20c08..ea136edf46be 100644 --- a/media/java/android/media/AudioFormat.java +++ b/media/java/android/media/AudioFormat.java @@ -1079,7 +1079,7 @@ public final class AudioFormat implements Parcelable { * @return one of the values that can be set in {@link Builder#setEncoding(int)} or * {@link AudioFormat#ENCODING_INVALID} if not set. */ - public int getEncoding() { + public @Encoding int getEncoding() { return mEncoding; } diff --git a/media/java/android/media/MediaFormat.java b/media/java/android/media/MediaFormat.java index 46db77708521..587e35b4b1fc 100644 --- a/media/java/android/media/MediaFormat.java +++ b/media/java/android/media/MediaFormat.java @@ -16,6 +16,9 @@ package android.media; +import static com.android.media.codec.flags.Flags.FLAG_CODEC_IMPORTANCE; + +import android.annotation.FlaggedApi; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; @@ -1635,6 +1638,34 @@ public final class MediaFormat { */ public static final String KEY_ALLOW_FRAME_DROP = "allow-frame-drop"; + /** + * A key describing the desired codec importance for the application. + * <p> + * The associated value is a positive integer including zero. + * Higher value means lesser importance. + * <p> + * The resource manager may use the codec importance, along with other factors + * when reclaiming codecs from an application. + * The specifics of reclaim policy is device dependent, but specifying the codec importance, + * will allow the resource manager to prioritize reclaiming less important codecs + * (assigned higher values) from the (reclaim) requesting application first. + * So, the codec importance is only relevant within the context of that application. + * <p> + * The codec importance can be set: + * <ul> + * <li>through {@link MediaCodec#configure}. </li> + * <li>through {@link MediaCodec#setParameters} if the codec has been configured already, + * which allows the users to change the codec importance multiple times. + * </ul> + * Any change/update in codec importance is guaranteed upon the completion of the function call + * that sets the codec importance. So, in case of concurrent codec operations, + * make sure to wait for the change in codec importance, before using another codec. + * Note that unless specified, by default the codecs will have highest importance (of value 0). + * + */ + @FlaggedApi(FLAG_CODEC_IMPORTANCE) + public static final String KEY_IMPORTANCE = "importance"; + /* package private */ MediaFormat(@NonNull Map<String, Object> map) { mMap = map; } diff --git a/media/java/android/media/MediaRouter2.java b/media/java/android/media/MediaRouter2.java index 9616b5d44540..62412df97043 100644 --- a/media/java/android/media/MediaRouter2.java +++ b/media/java/android/media/MediaRouter2.java @@ -734,7 +734,7 @@ public final class MediaRouter2 { * request. * @param transferInitiatorPackageName the package name of the app that initiated the transfer. * This value is used with the user handle to populate {@link - * RoutingController#wasTransferRequestedBySelf()}. + * RoutingController#wasTransferInitiatedBySelf()}. * @hide */ @FlaggedApi(FLAG_ENABLE_BUILT_IN_SPEAKER_ROUTE_SUITABILITY_STATUSES) @@ -1550,11 +1550,11 @@ public final class MediaRouter2 { } /** - * Returns whether the transfer was requested by the calling app (as determined by comparing + * Returns whether the transfer was initiated by the calling app (as determined by comparing * {@link UserHandle} and package name). */ @FlaggedApi(FLAG_ENABLE_BUILT_IN_SPEAKER_ROUTE_SUITABILITY_STATUSES) - public boolean wasTransferRequestedBySelf() { + public boolean wasTransferInitiatedBySelf() { RoutingSessionInfo sessionInfo = getRoutingSessionInfo(); UserHandle transferInitiatorUserHandle = sessionInfo.getTransferInitiatorUserHandle(); diff --git a/media/java/android/media/projection/IMediaProjectionManager.aidl b/media/java/android/media/projection/IMediaProjectionManager.aidl index 8ce1b6d5264d..3d927d36a369 100644 --- a/media/java/android/media/projection/IMediaProjectionManager.aidl +++ b/media/java/android/media/projection/IMediaProjectionManager.aidl @@ -121,7 +121,7 @@ interface IMediaProjectionManager { @EnforcePermission("MANAGE_MEDIA_PROJECTION") @JavaPassthrough(annotation = "@android.annotation.RequiresPermission(android.Manifest" + ".permission.MANAGE_MEDIA_PROJECTION)") - void addCallback(IMediaProjectionWatcherCallback callback); + MediaProjectionInfo addCallback(IMediaProjectionWatcherCallback callback); @JavaPassthrough(annotation = "@android.annotation.RequiresPermission(android.Manifest" + ".permission.MANAGE_MEDIA_PROJECTION)") diff --git a/media/java/android/media/projection/MediaProjectionInfo.java b/media/java/android/media/projection/MediaProjectionInfo.java index ff608565d2e5..c82039297d6e 100644 --- a/media/java/android/media/projection/MediaProjectionInfo.java +++ b/media/java/android/media/projection/MediaProjectionInfo.java @@ -16,6 +16,7 @@ package android.media.projection; +import android.os.IBinder; import android.os.Parcel; import android.os.Parcelable; import android.os.UserHandle; @@ -26,15 +27,18 @@ import java.util.Objects; public final class MediaProjectionInfo implements Parcelable { private final String mPackageName; private final UserHandle mUserHandle; + private final IBinder mLaunchCookie; - public MediaProjectionInfo(String packageName, UserHandle handle) { + public MediaProjectionInfo(String packageName, UserHandle handle, IBinder launchCookie) { mPackageName = packageName; mUserHandle = handle; + mLaunchCookie = launchCookie; } public MediaProjectionInfo(Parcel in) { mPackageName = in.readString(); mUserHandle = UserHandle.readFromParcel(in); + mLaunchCookie = in.readStrongBinder(); } public String getPackageName() { @@ -45,12 +49,16 @@ public final class MediaProjectionInfo implements Parcelable { return mUserHandle; } + public IBinder getLaunchCookie() { + return mLaunchCookie; + } + @Override public boolean equals(Object o) { - if (o instanceof MediaProjectionInfo) { - final MediaProjectionInfo other = (MediaProjectionInfo) o; + if (o instanceof MediaProjectionInfo other) { return Objects.equals(other.mPackageName, mPackageName) - && Objects.equals(other.mUserHandle, mUserHandle); + && Objects.equals(other.mUserHandle, mUserHandle) + && Objects.equals(other.mLaunchCookie, mLaunchCookie); } return false; } @@ -64,7 +72,8 @@ public final class MediaProjectionInfo implements Parcelable { public String toString() { return "MediaProjectionInfo{mPackageName=" + mPackageName + ", mUserHandle=" - + mUserHandle + "}"; + + mUserHandle + ", mLaunchCookie" + + mLaunchCookie + "}"; } @Override @@ -76,6 +85,7 @@ public final class MediaProjectionInfo implements Parcelable { public void writeToParcel(Parcel out, int flags) { out.writeString(mPackageName); UserHandle.writeToParcel(mUserHandle, out); + out.writeStrongBinder(mLaunchCookie); } public static final @android.annotation.NonNull Parcelable.Creator<MediaProjectionInfo> CREATOR = diff --git a/media/java/android/media/tv/TvContract.java b/media/java/android/media/tv/TvContract.java index db0195056074..7f8f1a3f22ef 100644 --- a/media/java/android/media/tv/TvContract.java +++ b/media/java/android/media/tv/TvContract.java @@ -16,6 +16,7 @@ package android.media.tv; +import android.annotation.FlaggedApi; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; @@ -29,6 +30,7 @@ import android.content.ContentResolver; import android.content.ContentUris; import android.content.Context; import android.content.Intent; +import android.media.tv.flags.Flags; import android.net.Uri; import android.os.Bundle; import android.os.IBinder; @@ -2540,9 +2542,9 @@ public final class TvContract { * <p>This is used to indicate the broadcast visibility type defined in the underlying * broadcast standard or country/operator profile, if applicable. For example, * {@code visible_service_flag} and {@code numeric_selection_flag} of - * {@code service_attribute_descriptor} in D-Book, {@code visible_service_flag} and - * {@code selectable_service_flag} of {@code ciplus_service_descriptor} in CI Plus 1.3 - * specification. + * {@code service_attribute_descriptor} in D-Book, the specification for UK-based TV + * products, {@code visible_service_flag} and {@code selectable_service_flag} of + * {@code ciplus_service_descriptor} in the CI Plus 1.3 specification. * * <p>The value should match one of the following: * {@link #BROADCAST_VISIBILITY_TYPE_VISIBLE}, @@ -2553,8 +2555,8 @@ public final class TvContract { * by default. * * <p>Type: INTEGER - * @hide */ + @FlaggedApi(Flags.FLAG_BROADCAST_VISIBILITY_TYPES) public static final String COLUMN_BROADCAST_VISIBILITY_TYPE = "broadcast_visibility_type"; /** @hide */ @@ -2571,8 +2573,8 @@ public final class TvContract { * visible from users and selectable by users via normal service navigation mechanisms. * * @see #COLUMN_BROADCAST_VISIBILITY_TYPE - * @hide */ + @FlaggedApi(Flags.FLAG_BROADCAST_VISIBILITY_TYPES) public static final int BROADCAST_VISIBILITY_TYPE_VISIBLE = 0; /** @@ -2581,18 +2583,18 @@ public final class TvContract { * the logical channel number. * * @see #COLUMN_BROADCAST_VISIBILITY_TYPE - * @hide */ + @FlaggedApi(Flags.FLAG_BROADCAST_VISIBILITY_TYPES) public static final int BROADCAST_VISIBILITY_TYPE_NUMERIC_SELECTABLE_ONLY = 1; /** * The broadcast visibility type for invisible services. Use this type when the service - * is invisible from users and unselectable by users via any of normal service navigation - * mechanisms. + * is invisible from users and not able to be selected by users via any of the normal + * service navigation mechanisms. * * @see #COLUMN_BROADCAST_VISIBILITY_TYPE - * @hide */ + @FlaggedApi(Flags.FLAG_BROADCAST_VISIBILITY_TYPES) public static final int BROADCAST_VISIBILITY_TYPE_INVISIBLE = 2; private Channels() {} diff --git a/media/java/android/media/tv/TvTrackInfo.java b/media/java/android/media/tv/TvTrackInfo.java index 78d7d7631145..2ebb19a7767e 100644 --- a/media/java/android/media/tv/TvTrackInfo.java +++ b/media/java/android/media/tv/TvTrackInfo.java @@ -55,6 +55,27 @@ public final class TvTrackInfo implements Parcelable { */ public static final int TYPE_SUBTITLE = 2; + /** + * The component tag identifies a component carried by a MPEG-2 TS. + * + * This corresponds to the component_tag in the component descriptor in the + * Elementary Stream loop of the stream in the Program Map Table + * (PMT) [EN 300 468], or undefined if the component is not carried in an + * MPEG-2 TS. + * + * @hide + */ + public static final String EXTRA_BUNDLE_KEY_COMPONENT_TAG = "component_tag"; + + /** + * The MPEG Program ID (PID) of the component in the MPEG2-TS in + * which it is carried, or undefined if the component is not carried in an + * MPEG-2 TS. + * + * @hide + */ + public static final String EXTRA_BUNDLE_KEY_PID = "pid"; + private final int mType; private final String mId; private final String mLanguage; diff --git a/media/java/android/media/tv/ad/ITvAdClient.aidl b/media/java/android/media/tv/ad/ITvAdClient.aidl new file mode 100644 index 000000000000..34d96b374a35 --- /dev/null +++ b/media/java/android/media/tv/ad/ITvAdClient.aidl @@ -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 android.media.tv.ad; + +import android.view.InputChannel; + +/** + * Interface a client of the ITvAdManager implements, to identify itself and receive + * information about changes to the state of each TV AD service. + * @hide + */ +oneway interface ITvAdClient { + void onSessionCreated(in String serviceId, IBinder token, in InputChannel channel, int seq); + void onSessionReleased(int seq); + void onLayoutSurface(int left, int top, int right, int bottom, int seq); +}
\ No newline at end of file diff --git a/media/java/android/media/tv/ad/ITvAdManager.aidl b/media/java/android/media/tv/ad/ITvAdManager.aidl index 92cc923dc9ab..a747e4995869 100644 --- a/media/java/android/media/tv/ad/ITvAdManager.aidl +++ b/media/java/android/media/tv/ad/ITvAdManager.aidl @@ -16,10 +16,25 @@ package android.media.tv.ad; +import android.media.tv.ad.ITvAdClient; +import android.media.tv.ad.ITvAdManagerCallback; +import android.media.tv.ad.TvAdServiceInfo; +import android.view.Surface; + /** * Interface to the TV AD service. * @hide */ interface ITvAdManager { + List<TvAdServiceInfo> getTvAdServiceList(int userId); + void createSession( + 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 setSurface(in IBinder sessionToken, in Surface surface, int userId); + void dispatchSurfaceChanged(in IBinder sessionToken, int format, int width, int height, + int userId); + + void registerCallback(in ITvAdManagerCallback callback, int userId); + void unregisterCallback(in ITvAdManagerCallback callback, int userId); } diff --git a/media/java/android/media/tv/ad/ITvAdManagerCallback.aidl b/media/java/android/media/tv/ad/ITvAdManagerCallback.aidl new file mode 100644 index 000000000000..f55f67e616db --- /dev/null +++ b/media/java/android/media/tv/ad/ITvAdManagerCallback.aidl @@ -0,0 +1,27 @@ +/* + * 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.media.tv.ad; + +/** + * Interface to receive callbacks from ITvAdManager regardless of sessions. + * @hide + */ +oneway interface ITvAdManagerCallback { + void onAdServiceAdded(in String serviceId); + void onAdServiceRemoved(in String serviceId); + void onAdServiceUpdated(in String serviceId); +}
\ No newline at end of file diff --git a/media/java/android/media/tv/ad/ITvAdService.aidl b/media/java/android/media/tv/ad/ITvAdService.aidl new file mode 100644 index 000000000000..3bb04097abca --- /dev/null +++ b/media/java/android/media/tv/ad/ITvAdService.aidl @@ -0,0 +1,35 @@ +/* + * 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.media.tv.ad; + +import android.media.tv.ad.ITvAdServiceCallback; +import android.media.tv.ad.ITvAdSessionCallback; +import android.os.Bundle; +import android.view.InputChannel; + +/** + * Top-level interface to a TV AD component (implemented in a Service). It's used for + * TvAdManagerService to communicate with TvAdService. + * @hide + */ +oneway interface ITvAdService { + void registerCallback(in ITvAdServiceCallback callback); + void unregisterCallback(in ITvAdServiceCallback callback); + void createSession(in InputChannel channel, in ITvAdSessionCallback callback, + in String serviceId, in String type); + void sendAppLinkCommand(in Bundle command); +}
\ No newline at end of file diff --git a/media/java/android/media/tv/ad/ITvAdServiceCallback.aidl b/media/java/android/media/tv/ad/ITvAdServiceCallback.aidl new file mode 100644 index 000000000000..a087181e097d --- /dev/null +++ b/media/java/android/media/tv/ad/ITvAdServiceCallback.aidl @@ -0,0 +1,24 @@ +/* + * 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.media.tv.ad; + +/** + * Helper interface for ITvAdService to allow the TvAdService to notify the TvAdManagerService. + * @hide + */ +oneway interface ITvAdServiceCallback { +}
\ No newline at end of file diff --git a/media/java/android/media/tv/ad/ITvAdSession.aidl b/media/java/android/media/tv/ad/ITvAdSession.aidl index b834f1b9fb92..751257ce4d4e 100644 --- a/media/java/android/media/tv/ad/ITvAdSession.aidl +++ b/media/java/android/media/tv/ad/ITvAdSession.aidl @@ -16,10 +16,15 @@ package android.media.tv.ad; +import android.view.Surface; + /** - * Sub-interface of ITvAdService which is created per session and has its own context. + * Sub-interface of ITvAdService.aidl which is created per session and has its own context. * @hide */ oneway interface ITvAdSession { + void release(); void startAdService(); + void setSurface(in Surface surface); + void dispatchSurfaceChanged(int format, int width, int height); } diff --git a/media/java/android/media/tv/ad/ITvAdSessionCallback.aidl b/media/java/android/media/tv/ad/ITvAdSessionCallback.aidl new file mode 100644 index 000000000000..f21ef198ab01 --- /dev/null +++ b/media/java/android/media/tv/ad/ITvAdSessionCallback.aidl @@ -0,0 +1,29 @@ +/* + * 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.media.tv.ad; + +import android.media.tv.ad.ITvAdSession; + +/** + * Helper interface for ITvAdSession to allow TvAdService to notify the system service when there is + * a related event. + * @hide + */ +oneway interface ITvAdSessionCallback { + void onSessionCreated(in ITvAdSession session); + void onLayoutSurface(int left, int top, int right, int bottom); +}
\ No newline at end of file diff --git a/media/java/android/media/tv/ad/ITvAdSessionWrapper.java b/media/java/android/media/tv/ad/ITvAdSessionWrapper.java new file mode 100644 index 000000000000..4df2783f6511 --- /dev/null +++ b/media/java/android/media/tv/ad/ITvAdSessionWrapper.java @@ -0,0 +1,152 @@ +/* + * 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.media.tv.ad; + +import android.content.Context; +import android.os.Looper; +import android.os.Message; +import android.os.RemoteException; +import android.util.Log; +import android.view.InputChannel; +import android.view.InputEvent; +import android.view.InputEventReceiver; +import android.view.Surface; + +import com.android.internal.os.HandlerCaller; +import com.android.internal.os.SomeArgs; + +/** + * Implements the internal ITvAdSession interface. + * @hide + */ +public class ITvAdSessionWrapper + extends ITvAdSession.Stub implements HandlerCaller.Callback { + + private static final String TAG = "ITvAdSessionWrapper"; + + private static final int EXECUTE_MESSAGE_TIMEOUT_SHORT_MILLIS = 1000; + private static final int EXECUTE_MESSAGE_TIMEOUT_LONG_MILLIS = 5 * 1000; + 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 final HandlerCaller mCaller; + private TvAdService.Session mSessionImpl; + private InputChannel mChannel; + private TvAdEventReceiver mReceiver; + + public ITvAdSessionWrapper( + Context context, TvAdService.Session mSessionImpl, InputChannel channel) { + this.mSessionImpl = mSessionImpl; + mCaller = new HandlerCaller(context, null, this, true /* asyncHandler */); + mChannel = channel; + if (channel != null) { + mReceiver = new TvAdEventReceiver(channel, context.getMainLooper()); + } + } + + @Override + public void release() { + mCaller.executeOrSendMessage(mCaller.obtainMessage(DO_RELEASE)); + } + + + @Override + public void executeMessage(Message msg) { + if (mSessionImpl == null) { + return; + } + + long startTime = System.nanoTime(); + switch (msg.what) { + case DO_RELEASE: { + mSessionImpl.release(); + mSessionImpl = null; + if (mReceiver != null) { + mReceiver.dispose(); + mReceiver = null; + } + if (mChannel != null) { + mChannel.dispose(); + mChannel = null; + } + break; + } + case DO_SET_SURFACE: { + mSessionImpl.setSurface((Surface) msg.obj); + break; + } + case DO_DISPATCH_SURFACE_CHANGED: { + SomeArgs args = (SomeArgs) msg.obj; + mSessionImpl.dispatchSurfaceChanged( + (Integer) args.argi1, (Integer) args.argi2, (Integer) args.argi3); + args.recycle(); + break; + } + default: { + Log.w(TAG, "Unhandled message code: " + msg.what); + break; + } + } + long durationMs = (System.nanoTime() - startTime) / (1000 * 1000); + if (durationMs > EXECUTE_MESSAGE_TIMEOUT_SHORT_MILLIS) { + Log.w(TAG, "Handling message (" + msg.what + ") took too long time (duration=" + + durationMs + "ms)"); + if (durationMs > EXECUTE_MESSAGE_TIMEOUT_LONG_MILLIS) { + // TODO: handle timeout + } + } + + } + + @Override + public void startAdService() throws RemoteException { + + } + + @Override + public void setSurface(Surface surface) { + mCaller.executeOrSendMessage(mCaller.obtainMessageO(DO_SET_SURFACE, surface)); + } + + @Override + public void dispatchSurfaceChanged(int format, int width, int height) { + mCaller.executeOrSendMessage( + mCaller.obtainMessageIIII(DO_DISPATCH_SURFACE_CHANGED, format, width, height, 0)); + } + + private final class TvAdEventReceiver extends InputEventReceiver { + TvAdEventReceiver(InputChannel inputChannel, Looper looper) { + super(inputChannel, looper); + } + + @Override + public void onInputEvent(InputEvent event) { + if (mSessionImpl == null) { + // The session has been finished. + finishInputEvent(event, false); + return; + } + + int handled = mSessionImpl.dispatchInputEvent(event, this); + if (handled != TvAdManager.Session.DISPATCH_IN_PROGRESS) { + finishInputEvent( + event, handled == TvAdManager.Session.DISPATCH_HANDLED); + } + } + } +} diff --git a/media/java/android/media/tv/ad/TvAdManager.java b/media/java/android/media/tv/ad/TvAdManager.java index 2b52c4b107b6..9c7505197dec 100644 --- a/media/java/android/media/tv/ad/TvAdManager.java +++ b/media/java/android/media/tv/ad/TvAdManager.java @@ -17,12 +17,30 @@ package android.media.tv.ad; import android.annotation.FlaggedApi; +import android.annotation.NonNull; +import android.annotation.Nullable; import android.annotation.SystemService; import android.content.Context; +import android.media.tv.TvInputManager; import android.media.tv.flags.Flags; +import android.os.Handler; import android.os.IBinder; +import android.os.Looper; +import android.os.Message; import android.os.RemoteException; import android.util.Log; +import android.util.Pools; +import android.util.SparseArray; +import android.view.InputChannel; +import android.view.InputEvent; +import android.view.InputEventSender; +import android.view.Surface; + +import com.android.internal.util.Preconditions; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Executor; /** * Central system API to the overall client-side TV AD architecture, which arbitrates interaction @@ -37,10 +55,163 @@ public class TvAdManager { private final ITvAdManager mService; private final int mUserId; + // A mapping from the sequence number of a session to its SessionCallbackRecord. + private final SparseArray<SessionCallbackRecord> mSessionCallbackRecordMap = + new SparseArray<>(); + + // @GuardedBy("mLock") + private final List<TvAdServiceCallbackRecord> mCallbackRecords = new ArrayList<>(); + + // A sequence number for the next session to be created. Should be protected by a lock + // {@code mSessionCallbackRecordMap}. + private int mNextSeq; + + private final Object mLock = new Object(); + private final ITvAdClient mClient; + /** @hide */ public TvAdManager(ITvAdManager service, int userId) { mService = service; mUserId = userId; + mClient = new ITvAdClient.Stub() { + @Override + public void onSessionCreated(String serviceId, IBinder token, InputChannel channel, + int seq) { + synchronized (mSessionCallbackRecordMap) { + SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq); + if (record == null) { + Log.e(TAG, "Callback not found for " + token); + return; + } + Session session = null; + if (token != null) { + session = new Session(token, channel, mService, mUserId, seq, + mSessionCallbackRecordMap); + } else { + mSessionCallbackRecordMap.delete(seq); + } + record.postSessionCreated(session); + } + } + + @Override + public void onSessionReleased(int seq) { + synchronized (mSessionCallbackRecordMap) { + SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq); + mSessionCallbackRecordMap.delete(seq); + if (record == null) { + Log.e(TAG, "Callback not found for seq:" + seq); + return; + } + record.mSession.releaseInternal(); + record.postSessionReleased(); + } + } + + @Override + public void onLayoutSurface(int left, int top, int right, int bottom, int seq) { + synchronized (mSessionCallbackRecordMap) { + SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq); + if (record == null) { + Log.e(TAG, "Callback not found for seq " + seq); + return; + } + record.postLayoutSurface(left, top, right, bottom); + } + } + + }; + + ITvAdManagerCallback managerCallback = + new ITvAdManagerCallback.Stub() { + @Override + public void onAdServiceAdded(String serviceId) { + synchronized (mLock) { + for (TvAdServiceCallbackRecord record : mCallbackRecords) { + record.postAdServiceAdded(serviceId); + } + } + } + + @Override + public void onAdServiceRemoved(String serviceId) { + synchronized (mLock) { + for (TvAdServiceCallbackRecord record : mCallbackRecords) { + record.postAdServiceRemoved(serviceId); + } + } + } + + @Override + public void onAdServiceUpdated(String serviceId) { + synchronized (mLock) { + for (TvAdServiceCallbackRecord record : mCallbackRecords) { + record.postAdServiceUpdated(serviceId); + } + } + } + }; + try { + if (mService != null) { + mService.registerCallback(managerCallback, mUserId); + } + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Returns the complete list of TV AD service on the system. + * + * @return List of {@link TvAdServiceInfo} for each TV AD service that describes its meta + * information. + * @hide + */ + @NonNull + public List<TvAdServiceInfo> getTvAdServiceList() { + try { + return mService.getTvAdServiceList(mUserId); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Creates a {@link Session} for a given TV AD service. + * + * <p>The number of sessions that can be created at the same time is limited by the capability + * of the given AD service. + * + * @param serviceId The ID of the AD service. + * @param callback A callback used to receive the created session. + * @param handler A {@link Handler} that the session creation will be delivered to. + * @hide + */ + public void createSession( + @NonNull String serviceId, + @NonNull String type, + @NonNull final TvAdManager.SessionCallback callback, + @NonNull Handler handler) { + createSessionInternal(serviceId, type, callback, handler); + } + + private void createSessionInternal(String serviceId, String type, + TvAdManager.SessionCallback callback, Handler handler) { + Preconditions.checkNotNull(serviceId); + Preconditions.checkNotNull(type); + Preconditions.checkNotNull(callback); + Preconditions.checkNotNull(handler); + TvAdManager.SessionCallbackRecord + record = new TvAdManager.SessionCallbackRecord(callback, handler); + synchronized (mSessionCallbackRecordMap) { + int seq = mNextSeq++; + mSessionCallbackRecordMap.put(seq, record); + try { + mService.createSession(mClient, serviceId, type, seq, mUserId); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } } /** @@ -48,14 +219,121 @@ public class TvAdManager { * @hide */ public static final class Session { - private final IBinder mToken; + static final int DISPATCH_IN_PROGRESS = -1; + static final int DISPATCH_NOT_HANDLED = 0; + static final int DISPATCH_HANDLED = 1; + + private static final long INPUT_SESSION_NOT_RESPONDING_TIMEOUT = 2500; private final ITvAdManager mService; private final int mUserId; + private final int mSeq; + private final SparseArray<SessionCallbackRecord> mSessionCallbackRecordMap; - private Session(IBinder token, ITvAdManager service, int userId) { + // For scheduling input event handling on the main thread. This also serves as a lock to + // protect pending input events and the input channel. + private final InputEventHandler mHandler = new InputEventHandler(Looper.getMainLooper()); + + private TvInputManager.Session mInputSession; + private final Pools.Pool<PendingEvent> mPendingEventPool = new Pools.SimplePool<>(20); + private final SparseArray<PendingEvent> mPendingEvents = new SparseArray<>(20); + private TvInputEventSender mSender; + private InputChannel mInputChannel; + private IBinder mToken; + + private Session(IBinder token, InputChannel channel, ITvAdManager service, int userId, + int seq, SparseArray<SessionCallbackRecord> sessionCallbackRecordMap) { mToken = token; + mInputChannel = channel; mService = service; mUserId = userId; + mSeq = seq; + mSessionCallbackRecordMap = sessionCallbackRecordMap; + } + + /** + * Releases this session. + */ + public void release() { + if (mToken == null) { + Log.w(TAG, "The session has been already released"); + return; + } + try { + mService.releaseSession(mToken, mUserId); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + + releaseInternal(); + } + + /** + * Sets the {@link android.view.Surface} for this session. + * + * @param surface A {@link android.view.Surface} used to render AD. + */ + public void setSurface(Surface surface) { + if (mToken == null) { + Log.w(TAG, "The session has been already released"); + return; + } + // surface can be null. + try { + mService.setSurface(mToken, surface, mUserId); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Notifies of any structural changes (format or size) of the surface passed in + * {@link #setSurface}. + * + * @param format The new PixelFormat of the surface. + * @param width The new width of the surface. + * @param height The new height of the surface. + */ + public void dispatchSurfaceChanged(int format, int width, int height) { + if (mToken == null) { + Log.w(TAG, "The session has been already released"); + return; + } + try { + mService.dispatchSurfaceChanged(mToken, format, width, height, mUserId); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + private void flushPendingEventsLocked() { + mHandler.removeMessages(InputEventHandler.MSG_FLUSH_INPUT_EVENT); + + final int count = mPendingEvents.size(); + for (int i = 0; i < count; i++) { + int seq = mPendingEvents.keyAt(i); + Message msg = mHandler.obtainMessage( + InputEventHandler.MSG_FLUSH_INPUT_EVENT, seq, 0); + msg.setAsynchronous(true); + msg.sendToTarget(); + } + } + + private void releaseInternal() { + mToken = null; + synchronized (mHandler) { + if (mInputChannel != null) { + if (mSender != null) { + flushPendingEventsLocked(); + mSender.dispose(); + mSender = null; + } + mInputChannel.dispose(); + mInputChannel = null; + } + } + synchronized (mSessionCallbackRecordMap) { + mSessionCallbackRecordMap.delete(mSeq); + } } void startAdService() { @@ -69,5 +347,324 @@ public class TvAdManager { 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; + public static final int MSG_FLUSH_INPUT_EVENT = 3; + + InputEventHandler(Looper looper) { + super(looper, null, true); + } + + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case MSG_SEND_INPUT_EVENT: { + sendInputEventAndReportResultOnMainLooper((PendingEvent) msg.obj); + return; + } + case MSG_TIMEOUT_INPUT_EVENT: { + finishedInputEvent(msg.arg1, false, true); + return; + } + case MSG_FLUSH_INPUT_EVENT: { + finishedInputEvent(msg.arg1, false, false); + return; + } + } + } + } + + // Assumes the event has already been removed from the queue. + void invokeFinishedInputEventCallback(PendingEvent p, boolean handled) { + p.mHandled = handled; + if (p.mEventHandler.getLooper().isCurrentThread()) { + // Already running on the callback handler thread so we can send the callback + // immediately. + p.run(); + } else { + // Post the event to the callback handler thread. + // In this case, the callback will be responsible for recycling the event. + Message msg = Message.obtain(p.mEventHandler, p); + msg.setAsynchronous(true); + msg.sendToTarget(); + } + } + + // Must be called on the main looper + private void sendInputEventAndReportResultOnMainLooper(PendingEvent p) { + synchronized (mHandler) { + int result = sendInputEventOnMainLooperLocked(p); + if (result == DISPATCH_IN_PROGRESS) { + return; + } + } + + invokeFinishedInputEventCallback(p, false); + } + + private int sendInputEventOnMainLooperLocked(PendingEvent p) { + if (mInputChannel != null) { + if (mSender == null) { + mSender = new TvInputEventSender(mInputChannel, mHandler.getLooper()); + } + + final InputEvent event = p.mEvent; + final int seq = event.getSequenceNumber(); + if (mSender.sendInputEvent(seq, event)) { + mPendingEvents.put(seq, p); + Message msg = mHandler.obtainMessage( + InputEventHandler.MSG_TIMEOUT_INPUT_EVENT, p); + msg.setAsynchronous(true); + mHandler.sendMessageDelayed(msg, INPUT_SESSION_NOT_RESPONDING_TIMEOUT); + return DISPATCH_IN_PROGRESS; + } + + Log.w(TAG, "Unable to send input event to session: " + mToken + " dropping:" + + event); + } + return DISPATCH_NOT_HANDLED; + } + + void finishedInputEvent(int seq, boolean handled, boolean timeout) { + final PendingEvent p; + synchronized (mHandler) { + int index = mPendingEvents.indexOfKey(seq); + if (index < 0) { + return; // spurious, event already finished or timed out + } + + p = mPendingEvents.valueAt(index); + mPendingEvents.removeAt(index); + + if (timeout) { + Log.w(TAG, "Timeout waiting for session to handle input event after " + + INPUT_SESSION_NOT_RESPONDING_TIMEOUT + " ms: " + mToken); + } else { + mHandler.removeMessages(InputEventHandler.MSG_TIMEOUT_INPUT_EVENT, p); + } + } + + invokeFinishedInputEventCallback(p, handled); + } + + private void recyclePendingEventLocked(PendingEvent p) { + p.recycle(); + mPendingEventPool.release(p); + } + + /** + * Callback that is invoked when an input event that was dispatched to this session has been + * finished. + * + * @hide + */ + public interface FinishedInputEventCallback { + /** + * Called when the dispatched input event is finished. + * + * @param token A token passed to {@link #dispatchInputEvent}. + * @param handled {@code true} if the dispatched input event was handled properly. + * {@code false} otherwise. + */ + void onFinishedInputEvent(Object token, boolean handled); + } + + private final class TvInputEventSender extends InputEventSender { + TvInputEventSender(InputChannel inputChannel, Looper looper) { + super(inputChannel, looper); + } + + @Override + public void onInputEventFinished(int seq, boolean handled) { + finishedInputEvent(seq, handled, false); + } + } + + private final class PendingEvent implements Runnable { + public InputEvent mEvent; + public Object mEventToken; + public Session.FinishedInputEventCallback mCallback; + public Handler mEventHandler; + public boolean mHandled; + + public void recycle() { + mEvent = null; + mEventToken = null; + mCallback = null; + mEventHandler = null; + mHandled = false; + } + + @Override + public void run() { + mCallback.onFinishedInputEvent(mEventToken, mHandled); + + synchronized (mEventHandler) { + recyclePendingEventLocked(this); + } + } + } + } + + + /** + * Interface used to receive the created session. + * @hide + */ + public abstract static class SessionCallback { + /** + * This is called after {@link TvAdManager#createSession} has been processed. + * + * @param session A {@link TvAdManager.Session} instance created. This can be + * {@code null} if the creation request failed. + */ + public void onSessionCreated(@Nullable Session session) { + } + + /** + * This is called when {@link TvAdManager.Session} is released. + * This typically happens when the process hosting the session has crashed or been killed. + * + * @param session the {@link TvAdManager.Session} instance released. + */ + public void onSessionReleased(@NonNull Session session) { + } + + /** + * This is called when {@link TvAdService.Session#layoutSurface} is called to + * change the layout of surface. + * + * @param session A {@link TvAdManager.Session} associated with this callback. + * @param left Left position. + * @param top Top position. + * @param right Right position. + * @param bottom Bottom position. + */ + public void onLayoutSurface(Session session, int left, int top, int right, int bottom) { + } + + } + + /** + * Callback used to monitor status of the TV AD service. + * @hide + */ + public abstract static class TvAdServiceCallback { + /** + * This is called when a TV AD service is added to the system. + * + * <p>Normally it happens when the user installs a new TV AD service package that implements + * {@link TvAdService} interface. + * + * @param serviceId The ID of the TV AD service. + */ + public void onAdServiceAdded(@NonNull String serviceId) { + } + + /** + * This is called when a TV AD service is removed from the system. + * + * <p>Normally it happens when the user uninstalls the previously installed TV AD service + * package. + * + * @param serviceId The ID of the TV AD service. + */ + public void onAdServiceRemoved(@NonNull String serviceId) { + } + + /** + * This is called when a TV AD service is updated on the system. + * + * <p>Normally it happens when a previously installed TV AD service package is re-installed + * or a newer version of the package exists becomes available/unavailable. + * + * @param serviceId The ID of the TV AD service. + */ + public void onAdServiceUpdated(@NonNull String serviceId) { + } + + } + + private static final class SessionCallbackRecord { + private final SessionCallback mSessionCallback; + private final Handler mHandler; + private Session mSession; + + SessionCallbackRecord(SessionCallback sessionCallback, Handler handler) { + mSessionCallback = sessionCallback; + mHandler = handler; + } + + void postSessionCreated(final Session session) { + mSession = session; + mHandler.post(new Runnable() { + @Override + public void run() { + mSessionCallback.onSessionCreated(session); + } + }); + } + + void postSessionReleased() { + mHandler.post(new Runnable() { + @Override + public void run() { + mSessionCallback.onSessionReleased(mSession); + } + }); + } + + void postLayoutSurface(final int left, final int top, final int right, + final int bottom) { + mHandler.post(new Runnable() { + @Override + public void run() { + mSessionCallback.onLayoutSurface(mSession, left, top, right, bottom); + } + }); + } + } + + private static final class TvAdServiceCallbackRecord { + private final TvAdServiceCallback mCallback; + private final Executor mExecutor; + + TvAdServiceCallbackRecord(TvAdServiceCallback callback, Executor executor) { + mCallback = callback; + mExecutor = executor; + } + + public TvAdServiceCallback getCallback() { + return mCallback; + } + + public void postAdServiceAdded(final String serviceId) { + mExecutor.execute(new Runnable() { + @Override + public void run() { + mCallback.onAdServiceAdded(serviceId); + } + }); + } + + public void postAdServiceRemoved(final String serviceId) { + mExecutor.execute(new Runnable() { + @Override + public void run() { + mCallback.onAdServiceRemoved(serviceId); + } + }); + } + + public void postAdServiceUpdated(final String serviceId) { + mExecutor.execute(new Runnable() { + @Override + public void run() { + mCallback.onAdServiceUpdated(serviceId); + } + }); + } } } diff --git a/media/java/android/media/tv/ad/TvAdService.java b/media/java/android/media/tv/ad/TvAdService.java index 6897a78647c2..699570397e34 100644 --- a/media/java/android/media/tv/ad/TvAdService.java +++ b/media/java/android/media/tv/ad/TvAdService.java @@ -16,8 +16,37 @@ package android.media.tv.ad; +import android.annotation.CallSuper; +import android.annotation.MainThread; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SdkConstant; +import android.annotation.SuppressLint; import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.graphics.PixelFormat; +import android.os.Bundle; +import android.os.Handler; +import android.os.IBinder; +import android.os.Message; +import android.os.RemoteCallbackList; +import android.os.RemoteException; +import android.util.Log; +import android.view.InputChannel; +import android.view.InputDevice; +import android.view.InputEvent; +import android.view.InputEventReceiver; import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.Surface; +import android.view.View; +import android.view.WindowManager; + +import com.android.internal.os.SomeArgs; + +import java.util.ArrayList; +import java.util.List; /** * The TvAdService class represents a TV client-side advertisement service. @@ -36,9 +65,123 @@ public abstract class TvAdService extends Service { public static final String SERVICE_META_DATA = "android.media.tv.ad.service"; /** + * This is the interface name that a service implementing a TV AD service should + * say that it supports -- that is, this is the action it uses for its intent filter. To be + * supported, the service must also require the + * android.Manifest.permission#BIND_TV_AD_SERVICE permission so that other + * applications cannot abuse it. + */ + @SdkConstant(SdkConstant.SdkConstantType.SERVICE_ACTION) + public static final String SERVICE_INTERFACE = "android.media.tv.ad.TvAdService"; + + private final Handler mServiceHandler = new ServiceHandler(); + private final RemoteCallbackList<ITvAdServiceCallback> mCallbacks = new RemoteCallbackList<>(); + + @Override + @Nullable + public final IBinder onBind(@NonNull Intent intent) { + ITvAdService.Stub tvAdServiceBinder = new ITvAdService.Stub() { + @Override + public void registerCallback(ITvAdServiceCallback cb) { + if (cb != null) { + mCallbacks.register(cb); + } + } + + @Override + public void unregisterCallback(ITvAdServiceCallback cb) { + if (cb != null) { + mCallbacks.unregister(cb); + } + } + + @Override + public void createSession(InputChannel channel, ITvAdSessionCallback cb, + String serviceId, String type) { + if (cb == null) { + return; + } + SomeArgs args = SomeArgs.obtain(); + args.arg1 = channel; + args.arg2 = cb; + args.arg3 = serviceId; + args.arg4 = type; + mServiceHandler.obtainMessage(ServiceHandler.DO_CREATE_SESSION, args) + .sendToTarget(); + } + + @Override + public void sendAppLinkCommand(Bundle command) { + onAppLinkCommand(command); + } + }; + return tvAdServiceBinder; + } + + /** + * Called when app link command is received. + */ + public void onAppLinkCommand(@NonNull Bundle command) { + } + + + /** + * Returns a concrete implementation of {@link Session}. + * + * <p>May return {@code null} if this TV AD service fails to create a session for some + * reason. + * + * @param serviceId The ID of the TV AD associated with the session. + * @param type The type of the TV AD associated with the session. + */ + @Nullable + public abstract Session onCreateSession(@NonNull String serviceId, @NonNull String type); + + /** * Base class for derived classes to implement to provide a TV AD session. */ public abstract static class Session implements KeyEvent.Callback { + private final KeyEvent.DispatcherState mDispatcherState = new KeyEvent.DispatcherState(); + + private final Object mLock = new Object(); + // @GuardedBy("mLock") + private ITvAdSessionCallback mSessionCallback; + // @GuardedBy("mLock") + private final List<Runnable> mPendingActions = new ArrayList<>(); + private final Context mContext; + final Handler mHandler; + private final WindowManager mWindowManager; + private Surface mSurface; + + + /** + * Creates a new Session. + * + * @param context The context of the application + */ + public Session(@NonNull Context context) { + mContext = context; + mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); + mHandler = new Handler(context.getMainLooper()); + } + + /** + * Releases TvAdService session. + */ + public abstract void onRelease(); + + void release() { + onRelease(); + if (mSurface != null) { + mSurface.release(); + mSurface = null; + } + synchronized (mLock) { + mSessionCallback = null; + mPendingActions.clear(); + } + } + /** * Starts TvAdService session. */ @@ -48,21 +191,264 @@ public abstract class TvAdService extends Service { void startAdService() { onStartAdService(); } - } - /** - * Implements the internal ITvAdService interface. - */ - public static class ITvAdSessionWrapper extends ITvAdSession.Stub { - private final Session mSessionImpl; + @Override + public boolean onKeyDown(int keyCode, @NonNull KeyEvent event) { + return false; + } + + @Override + public boolean onKeyLongPress(int keyCode, @NonNull KeyEvent event) { + return false; + } - public ITvAdSessionWrapper(Session mSessionImpl) { - this.mSessionImpl = mSessionImpl; + @Override + public boolean onKeyMultiple(int keyCode, int count, @NonNull KeyEvent event) { + return false; } @Override - public void startAdService() { - mSessionImpl.startAdService(); + public boolean onKeyUp(int keyCode, @NonNull KeyEvent event) { + return false; } + + /** + * Implement this method to handle touch screen motion events on the current session. + * + * @param event The motion event being received. + * @return If you handled the event, return {@code true}. If you want to allow the event to + * be handled by the next receiver, return {@code false}. + * @see View#onTouchEvent + */ + public boolean onTouchEvent(@NonNull MotionEvent event) { + return false; + } + + /** + * Implement this method to handle trackball events on the current session. + * + * @param event The motion event being received. + * @return If you handled the event, return {@code true}. If you want to allow the event to + * be handled by the next receiver, return {@code false}. + * @see View#onTrackballEvent + */ + public boolean onTrackballEvent(@NonNull MotionEvent event) { + return false; + } + + /** + * Implement this method to handle generic motion events on the current session. + * + * @param event The motion event being received. + * @return If you handled the event, return {@code true}. If you want to allow the event to + * be handled by the next receiver, return {@code false}. + * @see View#onGenericMotionEvent + */ + public boolean onGenericMotionEvent(@NonNull MotionEvent event) { + return false; + } + + /** + * Assigns a size and position to the surface passed in {@link #onSetSurface}. The position + * is relative to the overlay view that sits on top of this surface. + * + * @param left Left position in pixels, relative to the overlay view. + * @param top Top position in pixels, relative to the overlay view. + * @param right Right position in pixels, relative to the overlay view. + * @param bottom Bottom position in pixels, relative to the overlay view. + */ + @CallSuper + public void layoutSurface(final int left, final int top, final int right, + final int bottom) { + if (left > right || top > bottom) { + throw new IllegalArgumentException("Invalid parameter"); + } + executeOrPostRunnableOnMainThread(new Runnable() { + @MainThread + @Override + public void run() { + try { + if (DEBUG) { + Log.d(TAG, "layoutSurface (l=" + left + ", t=" + top + + ", r=" + right + ", b=" + bottom + ",)"); + } + if (mSessionCallback != null) { + mSessionCallback.onLayoutSurface(left, top, right, bottom); + } + } catch (RemoteException e) { + Log.w(TAG, "error in layoutSurface", e); + } + } + }); + } + + /** + * Called when the application sets the surface. + * + * <p>The TV AD service should render AD UI onto the given surface. When called with + * {@code null}, the AD service should immediately free any references to the currently set + * surface and stop using it. + * + * @param surface The surface to be used for AD UI rendering. Can be {@code null}. + * @return {@code true} if the surface was set successfully, {@code false} otherwise. + */ + public abstract boolean onSetSurface(@Nullable Surface surface); + + /** + * Called after any structural changes (format or size) have been made to the surface passed + * in {@link #onSetSurface}. This method is always called at least once, after + * {@link #onSetSurface} is called with non-null surface. + * + * @param format The new {@link PixelFormat} of the surface. + * @param width The new width of the surface. + * @param height The new height of the surface. + */ + public void onSurfaceChanged(@PixelFormat.Format int format, int width, int height) { + } + + /** + * Takes care of dispatching incoming input events and tells whether the event was handled. + */ + int dispatchInputEvent(InputEvent event, InputEventReceiver receiver) { + if (DEBUG) Log.d(TAG, "dispatchInputEvent(" + event + ")"); + if (event instanceof KeyEvent) { + KeyEvent keyEvent = (KeyEvent) event; + if (keyEvent.dispatch(this, mDispatcherState, this)) { + return TvAdManager.Session.DISPATCH_HANDLED; + } + + // TODO: special handlings of navigation keys and media keys + } else if (event instanceof MotionEvent) { + MotionEvent motionEvent = (MotionEvent) event; + final int source = motionEvent.getSource(); + if (motionEvent.isTouchEvent()) { + if (onTouchEvent(motionEvent)) { + return TvAdManager.Session.DISPATCH_HANDLED; + } + } else if ((source & InputDevice.SOURCE_CLASS_TRACKBALL) != 0) { + if (onTrackballEvent(motionEvent)) { + return TvAdManager.Session.DISPATCH_HANDLED; + } + } else { + if (onGenericMotionEvent(motionEvent)) { + return TvAdManager.Session.DISPATCH_HANDLED; + } + } + } + // TODO: handle overlay view + return TvAdManager.Session.DISPATCH_NOT_HANDLED; + } + + + private void initialize(ITvAdSessionCallback callback) { + synchronized (mLock) { + mSessionCallback = callback; + for (Runnable runnable : mPendingActions) { + runnable.run(); + } + mPendingActions.clear(); + } + } + + /** + * Calls {@link #onSetSurface}. + */ + void setSurface(Surface surface) { + onSetSurface(surface); + if (mSurface != null) { + mSurface.release(); + } + mSurface = surface; + // TODO: Handle failure. + } + + /** + * Calls {@link #onSurfaceChanged}. + */ + void dispatchSurfaceChanged(int format, int width, int height) { + if (DEBUG) { + Log.d(TAG, "dispatchSurfaceChanged(format=" + format + ", width=" + width + + ", height=" + height + ")"); + } + onSurfaceChanged(format, width, height); + } + + private void executeOrPostRunnableOnMainThread(Runnable action) { + synchronized (mLock) { + if (mSessionCallback == null) { + // The session is not initialized yet. + mPendingActions.add(action); + } else { + if (mHandler.getLooper().isCurrentThread()) { + action.run(); + } else { + // Posts the runnable if this is not called from the main thread + mHandler.post(action); + } + } + } + } + } + + + @SuppressLint("HandlerLeak") + private final class ServiceHandler extends Handler { + private static final int DO_CREATE_SESSION = 1; + private static final int DO_NOTIFY_SESSION_CREATED = 2; + + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case DO_CREATE_SESSION: { + SomeArgs args = (SomeArgs) msg.obj; + InputChannel channel = (InputChannel) args.arg1; + ITvAdSessionCallback cb = (ITvAdSessionCallback) args.arg2; + String serviceId = (String) args.arg3; + String type = (String) args.arg4; + args.recycle(); + TvAdService.Session sessionImpl = onCreateSession(serviceId, type); + if (sessionImpl == null) { + try { + // Failed to create a session. + cb.onSessionCreated(null); + } catch (RemoteException e) { + Log.e(TAG, "error in onSessionCreated", e); + } + return; + } + ITvAdSession stub = + new ITvAdSessionWrapper(TvAdService.this, sessionImpl, channel); + + SomeArgs someArgs = SomeArgs.obtain(); + someArgs.arg1 = sessionImpl; + someArgs.arg2 = stub; + someArgs.arg3 = cb; + mServiceHandler.obtainMessage( + DO_NOTIFY_SESSION_CREATED, someArgs).sendToTarget(); + return; + } + case DO_NOTIFY_SESSION_CREATED: { + SomeArgs args = (SomeArgs) msg.obj; + Session sessionImpl = (Session) args.arg1; + ITvAdSession stub = (ITvAdSession) args.arg2; + ITvAdSessionCallback cb = (ITvAdSessionCallback) args.arg3; + try { + cb.onSessionCreated(stub); + } catch (RemoteException e) { + Log.e(TAG, "error in onSessionCreated", e); + } + if (sessionImpl != null) { + sessionImpl.initialize(cb); + } + args.recycle(); + return; + } + default: { + Log.w(TAG, "Unhandled message code: " + msg.what); + return; + } + } + } + } } diff --git a/media/java/android/media/tv/ad/TvAdServiceInfo.java b/media/java/android/media/tv/ad/TvAdServiceInfo.java index ed04f1f9058c..45dc89d838d8 100644 --- a/media/java/android/media/tv/ad/TvAdServiceInfo.java +++ b/media/java/android/media/tv/ad/TvAdServiceInfo.java @@ -24,6 +24,7 @@ import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.content.pm.ServiceInfo; import android.content.res.Resources; +import android.content.res.TypedArray; import android.content.res.XmlResourceParser; import android.os.Parcel; import android.os.Parcelable; @@ -63,8 +64,7 @@ public final class TvAdServiceInfo implements Parcelable { if (context == null) { throw new IllegalArgumentException("context cannot be null."); } - // TODO: use a constant - Intent intent = new Intent("android.media.tv.ad.TvAdService").setComponent(component); + Intent intent = new Intent(TvAdService.SERVICE_INTERFACE).setComponent(component); ResolveInfo resolveInfo = context.getPackageManager().resolveService( intent, PackageManager.GET_SERVICES | PackageManager.GET_META_DATA); if (resolveInfo == null) { @@ -80,6 +80,7 @@ public final class TvAdServiceInfo implements Parcelable { mService = resolveInfo; mId = id; + mTypes.addAll(types); } private TvAdServiceInfo(ResolveInfo service, String id, List<String> types) { @@ -147,9 +148,8 @@ public final class TvAdServiceInfo implements Parcelable { ResolveInfo resolveInfo, Context context, List<String> types) { ServiceInfo serviceInfo = resolveInfo.serviceInfo; PackageManager pm = context.getPackageManager(); - // TODO: use constant for the metadata try (XmlResourceParser parser = - serviceInfo.loadXmlMetaData(pm, "android.media.tv.ad.service")) { + serviceInfo.loadXmlMetaData(pm, TvAdService.SERVICE_META_DATA)) { if (parser == null) { throw new IllegalStateException( "No " + "android.media.tv.ad.service" @@ -171,7 +171,15 @@ public final class TvAdServiceInfo implements Parcelable { + XML_START_TAG_NAME + " tag for " + serviceInfo.name); } - // TODO: parse attributes + TypedArray sa = resources.obtainAttributes(attrs, + com.android.internal.R.styleable.TvAdService); + CharSequence[] textArr = sa.getTextArray( + com.android.internal.R.styleable.TvAdService_adServiceTypes); + for (CharSequence cs : textArr) { + types.add(cs.toString().toLowerCase()); + } + + sa.recycle(); } catch (IOException | XmlPullParserException e) { throw new IllegalStateException( "Failed reading meta-data for " + serviceInfo.packageName, e); diff --git a/media/java/android/media/tv/ad/TvAdView.java b/media/java/android/media/tv/ad/TvAdView.java index 1a3771a9f24c..5e67fe9f697b 100644 --- a/media/java/android/media/tv/ad/TvAdView.java +++ b/media/java/android/media/tv/ad/TvAdView.java @@ -16,8 +16,20 @@ package android.media.tv.ad; +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.os.Handler; +import android.util.AttributeSet; import android.util.Log; +import android.util.Xml; +import android.view.Surface; +import android.view.SurfaceHolder; +import android.view.SurfaceView; +import android.view.View; import android.view.ViewGroup; /** @@ -28,18 +40,166 @@ public class TvAdView extends ViewGroup { private static final String TAG = "TvAdView"; private static final boolean DEBUG = false; - // TODO: create session + private final TvAdManager mTvAdManager; + + private final Handler mHandler = new Handler(); private TvAdManager.Session mSession; + private MySessionCallback mSessionCallback; + + private final AttributeSet mAttrs; + private final int mDefStyleAttr; + private final XmlResourceParser mParser; + + private SurfaceView mSurfaceView; + private Surface mSurface; + + private boolean mSurfaceChanged; + private int mSurfaceFormat; + private int mSurfaceWidth; + private int mSurfaceHeight; + + private boolean mUseRequestedSurfaceLayout; + private int mSurfaceViewLeft; + private int mSurfaceViewRight; + private int mSurfaceViewTop; + private int mSurfaceViewBottom; + + + + private final SurfaceHolder.Callback mSurfaceHolderCallback = new SurfaceHolder.Callback() { + @Override + public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { + if (DEBUG) { + Log.d(TAG, "surfaceChanged(holder=" + holder + ", format=" + format + + ", width=" + width + ", height=" + height + ")"); + } + mSurfaceFormat = format; + mSurfaceWidth = width; + mSurfaceHeight = height; + mSurfaceChanged = true; + dispatchSurfaceChanged(mSurfaceFormat, mSurfaceWidth, mSurfaceHeight); + } + + @Override + public void surfaceCreated(SurfaceHolder holder) { + mSurface = holder.getSurface(); + setSessionSurface(mSurface); + } + + @Override + public void surfaceDestroyed(SurfaceHolder holder) { + mSurface = null; + mSurfaceChanged = false; + setSessionSurface(null); + } + }; + + + public TvAdView(@NonNull Context context) { + this(context, null, 0); + } + + public TvAdView(@NonNull Context context, @Nullable AttributeSet attrs) { + this(context, attrs, 0); + } + + public TvAdView(@NonNull Context context, @Nullable AttributeSet attrs, + int defStyleAttr) { + super(context, attrs, defStyleAttr); + int sourceResId = Resources.getAttributeSetSourceResId(attrs); + if (sourceResId != Resources.ID_NULL) { + Log.d(TAG, "Build local AttributeSet"); + mParser = context.getResources().getXml(sourceResId); + mAttrs = Xml.asAttributeSet(mParser); + } else { + Log.d(TAG, "Use passed in AttributeSet"); + mParser = null; + mAttrs = attrs; + } + mDefStyleAttr = defStyleAttr; + resetSurfaceView(); + mTvAdManager = (TvAdManager) getContext().getSystemService(Context.TV_AD_SERVICE); + } + + @Override + public void onLayout(boolean changed, int left, int top, int right, int bottom) { + if (DEBUG) { + Log.d(TAG, "onLayout (left=" + left + ", top=" + top + ", right=" + right + + ", bottom=" + bottom + ",)"); + } + if (mUseRequestedSurfaceLayout) { + mSurfaceView.layout(mSurfaceViewLeft, mSurfaceViewTop, mSurfaceViewRight, + mSurfaceViewBottom); + } else { + mSurfaceView.layout(0, 0, right - left, bottom - top); + } + } - public TvAdView(Context context) { - super(context, /* attrs = */null, /* defStyleAttr = */0); + @Override + public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + mSurfaceView.measure(widthMeasureSpec, heightMeasureSpec); + int width = mSurfaceView.getMeasuredWidth(); + int height = mSurfaceView.getMeasuredHeight(); + int childState = mSurfaceView.getMeasuredState(); + setMeasuredDimension(resolveSizeAndState(width, widthMeasureSpec, childState), + resolveSizeAndState(height, heightMeasureSpec, + childState << MEASURED_HEIGHT_STATE_SHIFT)); } @Override - protected void onLayout(boolean changed, int l, int t, int r, int b) { + public void onVisibilityChanged(@NonNull View changedView, int visibility) { + super.onVisibilityChanged(changedView, visibility); + mSurfaceView.setVisibility(visibility); + } + + private void resetSurfaceView() { + if (mSurfaceView != null) { + mSurfaceView.getHolder().removeCallback(mSurfaceHolderCallback); + removeView(mSurfaceView); + } + mSurface = null; + mSurfaceView = new SurfaceView(getContext(), mAttrs, mDefStyleAttr) { + @Override + protected void updateSurface() { + super.updateSurface(); + }}; + // The surface view's content should be treated as secure all the time. + mSurfaceView.setSecure(true); + mSurfaceView.getHolder().addCallback(mSurfaceHolderCallback); + mSurfaceView.getHolder().setFormat(PixelFormat.TRANSLUCENT); + + mSurfaceView.setZOrderOnTop(false); + mSurfaceView.setZOrderMediaOverlay(true); + + addView(mSurfaceView); + } + + private void setSessionSurface(Surface surface) { + if (mSession == null) { + return; + } + mSession.setSurface(surface); + } + + private void dispatchSurfaceChanged(int format, int width, int height) { + if (mSession == null) { + return; + } + //mSession.dispatchSurfaceChanged(format, width, height); + } + + /** + * Prepares the AD service of corresponding {@link TvAdService}. + * + * @param serviceId the AD service ID, which can be found in TvAdServiceInfo#getId(). + */ + public void prepareAdService(@NonNull String serviceId, @NonNull String type) { if (DEBUG) { - Log.d(TAG, - "onLayout (left=" + l + ", top=" + t + ", right=" + r + ", bottom=" + b + ",)"); + Log.d(TAG, "prepareAdService"); + } + mSessionCallback = new TvAdView.MySessionCallback(serviceId); + if (mTvAdManager != null) { + mTvAdManager.createSession(serviceId, type, mSessionCallback, mHandler); } } @@ -54,4 +214,75 @@ public class TvAdView extends ViewGroup { mSession.startAdService(); } } + + private class MySessionCallback extends TvAdManager.SessionCallback { + final String mServiceId; + + MySessionCallback(String serviceId) { + mServiceId = serviceId; + } + + @Override + public void onSessionCreated(TvAdManager.Session session) { + if (DEBUG) { + Log.d(TAG, "onSessionCreated()"); + } + if (this != mSessionCallback) { + Log.w(TAG, "onSessionCreated - session already created"); + // This callback is obsolete. + if (session != null) { + session.release(); + } + return; + } + mSession = session; + if (session != null) { + // mSurface may not be ready yet as soon as starting an application. + // In the case, we don't send Session.setSurface(null) unnecessarily. + // setSessionSurface will be called in surfaceCreated. + if (mSurface != null) { + setSessionSurface(mSurface); + if (mSurfaceChanged) { + dispatchSurfaceChanged(mSurfaceFormat, mSurfaceWidth, mSurfaceHeight); + } + } + } else { + // Failed to create + // Todo: forward error to Tv App + mSessionCallback = null; + } + } + + @Override + public void onSessionReleased(TvAdManager.Session session) { + if (DEBUG) { + Log.d(TAG, "onSessionReleased()"); + } + if (this != mSessionCallback) { + Log.w(TAG, "onSessionReleased - session not created"); + return; + } + mSessionCallback = null; + mSession = null; + } + + @Override + public void onLayoutSurface( + TvAdManager.Session session, int left, int top, int right, int bottom) { + if (DEBUG) { + Log.d(TAG, "onLayoutSurface (left=" + left + ", top=" + top + ", right=" + + right + ", bottom=" + bottom + ",)"); + } + if (this != mSessionCallback) { + Log.w(TAG, "onLayoutSurface - session not created"); + return; + } + mSurfaceViewLeft = left; + mSurfaceViewTop = top; + mSurfaceViewRight = right; + mSurfaceViewBottom = bottom; + mUseRequestedSurfaceLayout = true; + requestLayout(); + } + } } diff --git a/media/java/android/media/tv/interactive/ITvInteractiveAppClient.aidl b/media/java/android/media/tv/interactive/ITvInteractiveAppClient.aidl index 77391841c6fe..e3dba03d6093 100644 --- a/media/java/android/media/tv/interactive/ITvInteractiveAppClient.aidl +++ b/media/java/android/media/tv/interactive/ITvInteractiveAppClient.aidl @@ -48,6 +48,7 @@ oneway interface ITvInteractiveAppClient { void onRequestCurrentChannelLcn(int seq); void onRequestStreamVolume(int seq); void onRequestTrackInfoList(int seq); + void onRequestSelectedTrackInfo(int seq); void onRequestCurrentTvInputId(int seq); void onRequestTimeShiftMode(int seq); void onRequestAvailableSpeeds(int seq); diff --git a/media/java/android/media/tv/interactive/ITvInteractiveAppManager.aidl b/media/java/android/media/tv/interactive/ITvInteractiveAppManager.aidl index 41cbe4ae02d0..4316d053a275 100644 --- a/media/java/android/media/tv/interactive/ITvInteractiveAppManager.aidl +++ b/media/java/android/media/tv/interactive/ITvInteractiveAppManager.aidl @@ -102,6 +102,8 @@ interface ITvInteractiveAppManager { int UserId); void notifyAdResponse(in IBinder sessionToken, in AdResponse response, int UserId); void notifyAdBufferConsumed(in IBinder sessionToken, in AdBuffer buffer, int userId); + void sendSelectedTrackInfo(in IBinder sessionToken, in List<TvTrackInfo> tracks, + int userId); void createMediaView(in IBinder sessionToken, in IBinder windowToken, in Rect frame, int userId); diff --git a/media/java/android/media/tv/interactive/ITvInteractiveAppSession.aidl b/media/java/android/media/tv/interactive/ITvInteractiveAppSession.aidl index 052bc3d5adce..ba7cf13a7a1d 100644 --- a/media/java/android/media/tv/interactive/ITvInteractiveAppSession.aidl +++ b/media/java/android/media/tv/interactive/ITvInteractiveAppSession.aidl @@ -78,6 +78,7 @@ oneway interface ITvInteractiveAppSession { void notifyBroadcastInfoResponse(in BroadcastInfoResponse response); void notifyAdResponse(in AdResponse response); void notifyAdBufferConsumed(in AdBuffer buffer); + void sendSelectedTrackInfo(in List<TvTrackInfo> tracks); void createMediaView(in IBinder windowToken, in Rect frame); void relayoutMediaView(in Rect frame); diff --git a/media/java/android/media/tv/interactive/ITvInteractiveAppSessionCallback.aidl b/media/java/android/media/tv/interactive/ITvInteractiveAppSessionCallback.aidl index 9e43e79144fd..416b8f12d5ea 100644 --- a/media/java/android/media/tv/interactive/ITvInteractiveAppSessionCallback.aidl +++ b/media/java/android/media/tv/interactive/ITvInteractiveAppSessionCallback.aidl @@ -50,6 +50,7 @@ oneway interface ITvInteractiveAppSessionCallback { void onRequestCurrentTvInputId(); void onRequestTimeShiftMode(); void onRequestAvailableSpeeds(); + void onRequestSelectedTrackInfo(); void onRequestStartRecording(in String requestId, in Uri programUri); void onRequestStopRecording(in String recordingId); void onRequestScheduleRecording(in String requestId, in String inputId, in Uri channelUri, diff --git a/media/java/android/media/tv/interactive/ITvInteractiveAppSessionWrapper.java b/media/java/android/media/tv/interactive/ITvInteractiveAppSessionWrapper.java index 253ade809ece..518b08a93f95 100644 --- a/media/java/android/media/tv/interactive/ITvInteractiveAppSessionWrapper.java +++ b/media/java/android/media/tv/interactive/ITvInteractiveAppSessionWrapper.java @@ -102,6 +102,7 @@ public class ITvInteractiveAppSessionWrapper private static final int DO_NOTIFY_RECORDING_SCHEDULED = 45; 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 final HandlerCaller mCaller; private Session mSessionImpl; @@ -247,6 +248,10 @@ public class ITvInteractiveAppSessionWrapper args.recycle(); break; } + case DO_SEND_SELECTED_TRACK_INFO: { + mSessionImpl.sendSelectedTrackInfo((List<TvTrackInfo>) msg.obj); + break; + } case DO_NOTIFY_VIDEO_AVAILABLE: { mSessionImpl.notifyVideoAvailable(); break; @@ -526,6 +531,12 @@ public class ITvInteractiveAppSessionWrapper } @Override + public void sendSelectedTrackInfo(List<TvTrackInfo> tracks) { + mCaller.executeOrSendMessage( + mCaller.obtainMessageO(DO_SEND_SELECTED_TRACK_INFO, tracks)); + } + + @Override public void notifyTracksChanged(List<TvTrackInfo> tracks) { mCaller.executeOrSendMessage(mCaller.obtainMessageO(DO_NOTIFY_TRACKS_CHANGED, tracks)); } diff --git a/media/java/android/media/tv/interactive/TvInteractiveAppManager.java b/media/java/android/media/tv/interactive/TvInteractiveAppManager.java index 7cce84a1ee16..bf4379f470d8 100755 --- a/media/java/android/media/tv/interactive/TvInteractiveAppManager.java +++ b/media/java/android/media/tv/interactive/TvInteractiveAppManager.java @@ -33,7 +33,6 @@ import android.media.tv.TvContentRating; import android.media.tv.TvInputManager; import android.media.tv.TvRecordingInfo; import android.media.tv.TvTrackInfo; -import android.media.tv.interactive.TvInteractiveAppService.Session; import android.net.Uri; import android.os.Bundle; import android.os.Handler; @@ -506,6 +505,18 @@ public final class TvInteractiveAppManager { } @Override + public void onRequestSelectedTrackInfo(int seq) { + synchronized (mSessionCallbackRecordMap) { + SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq); + if (record == null) { + Log.e(TAG, "Callback not found for seq " + seq); + return; + } + record.postRequestSelectedTrackInfo(); + } + } + + @Override public void onRequestCurrentTvInputId(int seq) { synchronized (mSessionCallbackRecordMap) { SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq); @@ -1209,6 +1220,18 @@ public final class TvInteractiveAppManager { } } + void sendSelectedTrackInfo(@NonNull List<TvTrackInfo> tracks) { + if (mToken == null) { + Log.w(TAG, "The session has been already released"); + return; + } + try { + mService.sendSelectedTrackInfo(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"); @@ -2108,6 +2131,15 @@ public final class TvInteractiveAppManager { }); } + void postRequestSelectedTrackInfo() { + mHandler.post(new Runnable() { + @Override + public void run() { + mSessionCallback.onRequestSelectedTrackInfo(mSession); + } + }); + } + void postRequestCurrentTvInputId() { mHandler.post(new Runnable() { @Override @@ -2378,6 +2410,15 @@ public final class TvInteractiveAppManager { } /** + * This is called when {@link TvInteractiveAppService.Session#requestSelectedTrackInfo()} is + * called. + * + * @param session A {@link TvInteractiveAppManager.Session} associated with this callback. + */ + public void onRequestSelectedTrackInfo(Session session) { + } + + /** * This is called when {@link TvInteractiveAppService.Session#requestCurrentTvInputId} is * called. * diff --git a/media/java/android/media/tv/interactive/TvInteractiveAppService.java b/media/java/android/media/tv/interactive/TvInteractiveAppService.java index 241940486a14..5cc86bacf54f 100755 --- a/media/java/android/media/tv/interactive/TvInteractiveAppService.java +++ b/media/java/android/media/tv/interactive/TvInteractiveAppService.java @@ -932,6 +932,16 @@ public abstract class TvInteractiveAppService extends Service { @NonNull Bundle data) { } + /** + * Called when the TV App sends the selected track info as a response to + * requestSelectedTrackInfo. + * + * @param tracks + * @hide + */ + public void onSelectedTrackInfo(List<TvTrackInfo> tracks) { + } + @Override public boolean onKeyDown(int keyCode, @NonNull KeyEvent event) { return false; @@ -1338,6 +1348,30 @@ public abstract class TvInteractiveAppService extends Service { } /** + * Requests the currently selected {@link TvTrackInfo} from the TV App. + * + * <p> Normally, track info cannot be synchronized until the channel has + * been changed. This is used when the session of the TIAS is newly + * created and the normal synchronization has not happened yet. + * @hide + */ + @CallSuper + public void requestSelectedTrackInfo() { + executeOrPostRunnableOnMainThread(() -> { + try { + if (DEBUG) { + Log.d(TAG, "requestSelectedTrackInfo"); + } + if (mSessionCallback != null) { + mSessionCallback.onRequestSelectedTrackInfo(); + } + } catch (RemoteException e) { + Log.w(TAG, "error in requestSelectedTrackInfo", e); + } + }); + } + + /** * Requests starting of recording * * <p> This is used to request the active {@link android.media.tv.TvRecordingClient} to @@ -1781,6 +1815,13 @@ public abstract class TvInteractiveAppService extends Service { onTvMessage(type, data); } + void sendSelectedTrackInfo(List<TvTrackInfo> tracks) { + if (DEBUG) { + Log.d(TAG, "notifySelectedTrackInfo (tracks= " + tracks + ")"); + } + onSelectedTrackInfo(tracks); + } + /** * Calls {@link #onAdBufferConsumed}. */ diff --git a/media/java/android/media/tv/interactive/TvInteractiveAppView.java b/media/java/android/media/tv/interactive/TvInteractiveAppView.java index cbaf5e482faa..40a12e4db4cc 100755 --- a/media/java/android/media/tv/interactive/TvInteractiveAppView.java +++ b/media/java/android/media/tv/interactive/TvInteractiveAppView.java @@ -582,6 +582,20 @@ public class TvInteractiveAppView extends ViewGroup { } /** + * Sends the currently selected track info to the TV Interactive App. + * + * @hide + */ + public void sendSelectedTrackInfo(@Nullable List<TvTrackInfo> tracks) { + if (DEBUG) { + Log.d(TAG, "sendSelectedTrackInfo"); + } + if (mSession != null) { + mSession.sendSelectedTrackInfo(tracks); + } + } + + /** * Sends current TV input ID to related TV interactive app. * * @param inputId The current TV input ID whose channel is tuned. {@code null} if no channel is @@ -1197,6 +1211,16 @@ public class TvInteractiveAppView extends ViewGroup { } /** + * This is called when {@link TvInteractiveAppService.Session#requestSelectedTrackInfo()} is + * called. + * + * @param iAppServiceId The ID of the TV interactive app service bound to this view. + * @hide + */ + public void onRequestSelectedTrackInfo(@NonNull String iAppServiceId) { + } + + /** * This is called when {@link TvInteractiveAppService.Session#requestCurrentTvInputId()} is * called. * @@ -1714,6 +1738,28 @@ public class TvInteractiveAppView extends ViewGroup { } @Override + public void onRequestSelectedTrackInfo(Session session) { + if (DEBUG) { + Log.d(TAG, "onRequestSelectedTrackInfo"); + } + if (this != mSessionCallback) { + Log.w(TAG, "onRequestSelectedTrackInfo - session not created"); + return; + } + synchronized (mCallbackLock) { + if (mCallbackExecutor != null) { + mCallbackExecutor.execute(() -> { + synchronized (mCallbackLock) { + if (mCallback != null) { + mCallback.onRequestSelectedTrackInfo(mIAppServiceId); + } + } + }); + } + } + } + + @Override public void onRequestCurrentTvInputId(Session session) { if (DEBUG) { Log.d(TAG, "onRequestCurrentTvInputId"); diff --git a/media/java/android/media/tv/tuner/Tuner.java b/media/java/android/media/tv/tuner/Tuner.java index 09f09b94ac0d..f74edbfdba6a 100644 --- a/media/java/android/media/tv/tuner/Tuner.java +++ b/media/java/android/media/tv/tuner/Tuner.java @@ -30,6 +30,7 @@ import android.content.pm.PackageManager; import android.hardware.tv.tuner.Constant; import android.hardware.tv.tuner.Constant64Bit; import android.hardware.tv.tuner.FrontendScanType; +import android.media.MediaCodec; import android.media.tv.TvInputService; import android.media.tv.tuner.dvr.DvrPlayback; import android.media.tv.tuner.dvr.DvrRecorder; @@ -272,8 +273,12 @@ public class Tuner implements AutoCloseable { try { System.loadLibrary("media_tv_tuner"); nativeInit(); + // Load and initialize MediaCodec to avoid flaky cts test result. + Class.forName(MediaCodec.class.getName()); } catch (UnsatisfiedLinkError e) { Log.d(TAG, "tuner JNI library not found!"); + } catch (ClassNotFoundException e) { + Log.e(TAG, "MediaCodec class not found!", e); } } diff --git a/media/jni/android_media_tv_Tuner.cpp b/media/jni/android_media_tv_Tuner.cpp index 3fcb8713672f..00b0e57c09ea 100644 --- a/media/jni/android_media_tv_Tuner.cpp +++ b/media/jni/android_media_tv_Tuner.cpp @@ -967,7 +967,7 @@ void FilterClientCallbackImpl::onFilterStatus(const DemuxFilterStatus status) { ScopedLocalRef<jobject> filter(env); { android::Mutex::Autolock autoLock(mLock); - if (env->IsSameObject(filter.get(), nullptr)) { + if (env->IsSameObject(mFilterObj, nullptr)) { ALOGE("FilterClientCallbackImpl::onFilterStatus:" "Filter object has been freed. Ignoring callback."); return; diff --git a/nfc/Android.bp b/nfc/Android.bp index 5d1404a56a9e..74bec3ee8b3c 100644 --- a/nfc/Android.bp +++ b/nfc/Android.bp @@ -69,7 +69,12 @@ java_sdk_library { jarjar_rules: ":nfc-jarjar-rules", lint: { strict_updatability_linting: true, + baseline_filename: "lint-baseline.xml", }, + apex_available: [ + "//apex_available:platform", + "com.android.nfcservices", + ], } filegroup { diff --git a/nfc/lint-baseline.xml b/nfc/lint-baseline.xml new file mode 100644 index 000000000000..1dfdd29e480a --- /dev/null +++ b/nfc/lint-baseline.xml @@ -0,0 +1,213 @@ +<?xml version="1.0" encoding="UTF-8"?> +<issues format="6" by="lint 8.4.0-alpha01" type="baseline" client="" dependencies="true" name="" variant="all" version="8.4.0-alpha01"> + + <issue + id="NewApi" + message="Call requires API level 35 (current min is 34): `new android.nfc.cardemulation.AidGroup`" + errorLine1=" AidGroup aidGroup = new AidGroup(aids, category);" + errorLine2=" ~~~~~~~~~~~~"> + <location + file="frameworks/base/nfc/java/android/nfc/cardemulation/CardEmulation.java" + line="377" + column="29"/> + </issue> + + <issue + id="NewApi" + message="Call requires API level 35 (current min is 34): `android.nfc.cardemulation.AidGroup#getAids`" + errorLine1=" return (group != null ? group.getAids() : null);" + errorLine2=" ~~~~~~~"> + <location + file="frameworks/base/nfc/java/android/nfc/cardemulation/CardEmulation.java" + line="537" + column="43"/> + </issue> + + <issue + id="NewApi" + message="Call requires API level 35 (current min is 34): `android.nfc.cardemulation.AidGroup#getAids`" + errorLine1=" return (group != null ? group.getAids() : null);" + errorLine2=" ~~~~~~~"> + <location + file="frameworks/base/nfc/java/android/nfc/cardemulation/CardEmulation.java" + line="547" + column="47"/> + </issue> + + <issue + id="NewApi" + message="Call requires API level 35 (current min is 34): `android.nfc.cardemulation.ApduServiceInfo#getAids`" + errorLine1=" return (serviceInfo != null ? serviceInfo.getAids() : null);" + errorLine2=" ~~~~~~~"> + <location + file="frameworks/base/nfc/java/android/nfc/cardemulation/CardEmulation.java" + line="714" + column="55"/> + </issue> + + <issue + id="NewApi" + message="Call requires API level 35 (current min is 34): `android.nfc.cardemulation.ApduServiceInfo#getAids`" + errorLine1=" return (serviceInfo != null ? serviceInfo.getAids() : null);" + errorLine2=" ~~~~~~~"> + <location + file="frameworks/base/nfc/java/android/nfc/cardemulation/CardEmulation.java" + line="724" + column="59"/> + </issue> + + <issue + id="NewApi" + message="Call requires API level 35 (current min is 34): `android.nfc.cardemulation.ApduServiceInfo#isOnHost`" + errorLine1=" if (!serviceInfo.isOnHost()) {" + errorLine2=" ~~~~~~~~"> + <location + file="frameworks/base/nfc/java/android/nfc/cardemulation/CardEmulation.java" + line="755" + column="34"/> + </issue> + + <issue + id="NewApi" + message="Call requires API level 35 (current min is 34): `android.nfc.cardemulation.ApduServiceInfo#getOffHostSecureElement`" + errorLine1=" return serviceInfo.getOffHostSecureElement() == null ?" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~"> + <location + file="frameworks/base/nfc/java/android/nfc/cardemulation/CardEmulation.java" + line="756" + column="40"/> + </issue> + + <issue + id="NewApi" + message="Call requires API level 35 (current min is 34): `android.nfc.cardemulation.ApduServiceInfo#getOffHostSecureElement`" + errorLine1=' "OffHost" : serviceInfo.getOffHostSecureElement();' + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~"> + <location + file="frameworks/base/nfc/java/android/nfc/cardemulation/CardEmulation.java" + line="757" + column="53"/> + </issue> + + <issue + id="NewApi" + message="Call requires API level 35 (current min is 34): `android.nfc.cardemulation.ApduServiceInfo#isOnHost`" + errorLine1=" if (!serviceInfo.isOnHost()) {" + errorLine2=" ~~~~~~~~"> + <location + file="frameworks/base/nfc/java/android/nfc/cardemulation/CardEmulation.java" + line="772" + column="38"/> + </issue> + + <issue + id="NewApi" + message="Call requires API level 35 (current min is 34): `android.nfc.cardemulation.ApduServiceInfo#getOffHostSecureElement`" + errorLine1=" return serviceInfo.getOffHostSecureElement() == null ?" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~"> + <location + file="frameworks/base/nfc/java/android/nfc/cardemulation/CardEmulation.java" + line="773" + column="44"/> + </issue> + + <issue + id="NewApi" + message="Call requires API level 35 (current min is 34): `android.nfc.cardemulation.ApduServiceInfo#getOffHostSecureElement`" + errorLine1=' "Offhost" : serviceInfo.getOffHostSecureElement();' + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~"> + <location + file="frameworks/base/nfc/java/android/nfc/cardemulation/CardEmulation.java" + line="774" + column="57"/> + </issue> + + <issue + id="NewApi" + message="Call requires API level 35 (current min is 34): `android.nfc.cardemulation.ApduServiceInfo#getDescription`" + errorLine1=" return (serviceInfo != null ? serviceInfo.getDescription() : null);" + errorLine2=" ~~~~~~~~~~~~~~"> + <location + file="frameworks/base/nfc/java/android/nfc/cardemulation/CardEmulation.java" + line="798" + column="55"/> + </issue> + + <issue + id="NewApi" + message="Call requires API level 35 (current min is 34): `android.nfc.cardemulation.ApduServiceInfo#getDescription`" + errorLine1=" return (serviceInfo != null ? serviceInfo.getDescription() : null);" + errorLine2=" ~~~~~~~~~~~~~~"> + <location + file="frameworks/base/nfc/java/android/nfc/cardemulation/CardEmulation.java" + line="808" + column="59"/> + </issue> + + <issue + id="NewApi" + message="Call requires API level 35 (current min is 34): `android.app.Activity#isResumed`" + errorLine1=" if (!activity.isResumed()) {" + errorLine2=" ~~~~~~~~~"> + <location + file="frameworks/base/nfc/java/android/nfc/cardemulation/CardEmulation.java" + line="1032" + column="23"/> + </issue> + + <issue + id="NewApi" + message="Call requires API level 35 (current min is 34): `android.app.Activity#isResumed`" + errorLine1=" if (!activity.isResumed()) {" + errorLine2=" ~~~~~~~~~"> + <location + file="frameworks/base/nfc/java/android/nfc/cardemulation/CardEmulation.java" + line="1066" + column="23"/> + </issue> + + <issue + id="NewApi" + message="Call requires API level 35 (current min is 34): `android.app.Activity#isResumed`" + errorLine1=" resumed = activity.isResumed();" + errorLine2=" ~~~~~~~~~"> + <location + file="frameworks/base/nfc/java/android/nfc/NfcActivityManager.java" + line="124" + column="32"/> + </issue> + + <issue + id="NewApi" + message="Call requires API level 35 (current min is 34): `android.app.Activity#isResumed`" + errorLine1=" if (!activity.isResumed()) {" + errorLine2=" ~~~~~~~~~"> + <location + file="frameworks/base/nfc/java/android/nfc/NfcAdapter.java" + line="2457" + column="23"/> + </issue> + + <issue + id="NewApi" + message="Call requires API level 35 (current min is 34): `android.app.Activity#isResumed`" + errorLine1=" if (!activity.isResumed()) {" + errorLine2=" ~~~~~~~~~"> + <location + file="frameworks/base/nfc/java/android/nfc/cardemulation/NfcFCardEmulation.java" + line="315" + column="23"/> + </issue> + + <issue + id="NewApi" + message="Call requires API level 35 (current min is 34): `android.app.Activity#isResumed`" + errorLine1=" if (!activity.isResumed()) {" + errorLine2=" ~~~~~~~~~"> + <location + file="frameworks/base/nfc/java/android/nfc/cardemulation/NfcFCardEmulation.java" + line="351" + column="23"/> + </issue> + +</issues>
\ No newline at end of file diff --git a/packages/CredentialManager/Android.bp b/packages/CredentialManager/Android.bp index 991fe41bb7f3..c292b5027f36 100644 --- a/packages/CredentialManager/Android.bp +++ b/packages/CredentialManager/Android.bp @@ -7,19 +7,14 @@ package { default_applicable_licenses: ["frameworks_base_license"], } -android_app { - name: "CredentialManager", - defaults: ["platform_app_defaults"], - certificate: "platform", +android_library { + name: "CredentialManager-handheld", + + platform_apis: true, + srcs: ["src/**/*.kt"], resource_dirs: ["res"], - dex_preopt: { - profile_guided: true, - //TODO: b/312357299 - Update baseline profile - profile: "profile.txt.prof", - }, - static_libs: [ "CredentialManagerShared", "PlatformComposeCore", @@ -42,6 +37,23 @@ android_app { "androidx.recyclerview_recyclerview", "kotlinx-coroutines-core", ], +} + +android_app { + name: "CredentialManager", + defaults: ["platform_app_defaults"], + certificate: "platform", + + dex_preopt: { + profile_guided: true, + //TODO: b/312357299 - Update baseline profile + profile: "profile.txt.prof", + }, + + // Do not add new dependencies here. Add to CredentialManager-handheld instead. + static_libs: [ + "CredentialManager-handheld", + ], platform_apis: true, privileged: true, diff --git a/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorActivity.kt b/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorActivity.kt index c409ba6aa78e..f8ffc9e26799 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorActivity.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/CredentialSelectorActivity.kt @@ -34,6 +34,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.res.stringResource import androidx.lifecycle.viewmodel.compose.viewModel +import com.android.compose.theme.PlatformTheme import com.android.credentialmanager.common.Constants import com.android.credentialmanager.common.DialogState import com.android.credentialmanager.common.ProviderActivityResult @@ -43,7 +44,6 @@ import com.android.credentialmanager.createflow.CreateCredentialScreen import com.android.credentialmanager.createflow.hasContentToDisplay import com.android.credentialmanager.getflow.GetCredentialScreen import com.android.credentialmanager.getflow.hasContentToDisplay -import com.android.credentialmanager.ui.theme.PlatformTheme @ExperimentalMaterialApi class CredentialSelectorActivity : ComponentActivity() { diff --git a/packages/CredentialManager/src/com/android/credentialmanager/autofill/CredentialAutofillService.kt b/packages/CredentialManager/src/com/android/credentialmanager/autofill/CredentialAutofillService.kt index 58467afe43a4..03ac605222ba 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/autofill/CredentialAutofillService.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/autofill/CredentialAutofillService.kt @@ -173,7 +173,7 @@ class CredentialAutofillService : AutofillService() { CancellationSignal(), Executors.newSingleThreadExecutor(), outcome, - autofillCallback + autofillCallback.asBinder() ) } @@ -358,8 +358,8 @@ class CredentialAutofillService : AutofillService() { } else { spec = inlinePresentationSpecs[inlinePresentationSpecsCount - 1] } - val displayName: String = if (primaryEntry.credentialType == CredentialType.PASSKEY - && primaryEntry.displayName != null) { + val displayName: String = if (primaryEntry.credentialType == + CredentialType.PASSKEY && primaryEntry.displayName != null) { primaryEntry.displayName!! } else { primaryEntry.userName diff --git a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/BottomSheet.kt b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/BottomSheet.kt index db69b8bdf42b..d319e4cc9ef9 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/BottomSheet.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/BottomSheet.kt @@ -24,11 +24,11 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import com.android.compose.rememberSystemUiController +import com.android.compose.theme.LocalAndroidColorScheme import com.android.credentialmanager.common.material.ModalBottomSheetLayout import com.android.credentialmanager.common.material.ModalBottomSheetValue import com.android.credentialmanager.common.material.rememberModalBottomSheetState import com.android.credentialmanager.ui.theme.EntryShape -import com.android.credentialmanager.ui.theme.LocalAndroidColorScheme import kotlinx.coroutines.launch @@ -54,7 +54,7 @@ fun ModalBottomSheet( setBottomSheetSystemBarsColor(sysUiController) } ModalBottomSheetLayout( - sheetBackgroundColor = LocalAndroidColorScheme.current.colorSurfaceBright, + sheetBackgroundColor = LocalAndroidColorScheme.current.surfaceBright, modifier = Modifier.background(Color.Transparent), sheetState = state, sheetContent = sheetContent, diff --git a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/Cards.kt b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/Cards.kt index 3976f9a305ab..bdfe39920d44 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/Cards.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/Cards.kt @@ -30,8 +30,8 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp +import com.android.compose.theme.LocalAndroidColorScheme import com.android.credentialmanager.ui.theme.Shapes -import com.android.credentialmanager.ui.theme.LocalAndroidColorScheme /** * Container card for the whole sheet. @@ -50,7 +50,7 @@ fun SheetContainerCard( modifier = modifier.fillMaxWidth().wrapContentHeight(), border = null, colors = CardDefaults.cardColors( - containerColor = LocalAndroidColorScheme.current.colorSurfaceBright, + containerColor = LocalAndroidColorScheme.current.surfaceBright, ), ) { if (topAppBar != null) { diff --git a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/Entry.kt b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/Entry.kt index 1c394ec4ba55..a6253b8d4e07 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/Entry.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/Entry.kt @@ -56,9 +56,9 @@ import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp +import com.android.compose.theme.LocalAndroidColorScheme import com.android.credentialmanager.R import com.android.credentialmanager.ui.theme.EntryShape -import com.android.credentialmanager.ui.theme.LocalAndroidColorScheme import com.android.credentialmanager.ui.theme.Shapes @Composable @@ -168,7 +168,7 @@ fun Entry( // Decorative purpose only. contentDescription = null, modifier = Modifier.size(24.dp), - tint = LocalAndroidColorScheme.current.colorOnSurfaceVariant, + tint = LocalAndroidColorScheme.current.onSurfaceVariant, ) } } @@ -182,7 +182,7 @@ fun Entry( Icon( modifier = iconSize, bitmap = iconImageBitmap, - tint = LocalAndroidColorScheme.current.colorOnSurfaceVariant, + tint = LocalAndroidColorScheme.current.onSurfaceVariant, // Decorative purpose only. contentDescription = null, ) @@ -206,7 +206,7 @@ fun Entry( Icon( modifier = iconSize, imageVector = iconImageVector, - tint = LocalAndroidColorScheme.current.colorOnSurfaceVariant, + tint = LocalAndroidColorScheme.current.onSurfaceVariant, // Decorative purpose only. contentDescription = null, ) @@ -218,7 +218,7 @@ fun Entry( Icon( modifier = iconSize, painter = iconPainter, - tint = LocalAndroidColorScheme.current.colorOnSurfaceVariant, + tint = LocalAndroidColorScheme.current.onSurfaceVariant, // Decorative purpose only. contentDescription = null, ) @@ -229,9 +229,9 @@ fun Entry( }, border = null, colors = SuggestionChipDefaults.suggestionChipColors( - containerColor = LocalAndroidColorScheme.current.colorSurfaceContainerHigh, - labelColor = LocalAndroidColorScheme.current.colorOnSurfaceVariant, - iconContentColor = LocalAndroidColorScheme.current.colorOnSurfaceVariant, + containerColor = LocalAndroidColorScheme.current.surfaceContainerHigh, + labelColor = LocalAndroidColorScheme.current.onSurfaceVariant, + iconContentColor = LocalAndroidColorScheme.current.onSurfaceVariant, ), ) } @@ -294,7 +294,7 @@ fun PasskeyBenefitRow( Icon( modifier = Modifier.size(24.dp), painter = leadingIconPainter, - tint = LocalAndroidColorScheme.current.colorOnSurfaceVariant, + tint = LocalAndroidColorScheme.current.onSurfaceVariant, // Decorative purpose only. contentDescription = null, ) @@ -353,7 +353,7 @@ fun MoreOptionTopAppBar( R.string.accessibility_back_arrow_button ), modifier = Modifier.size(24.dp).autoMirrored(), - tint = LocalAndroidColorScheme.current.colorOnSurfaceVariant, + tint = LocalAndroidColorScheme.current.onSurfaceVariant, ) } } diff --git a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/SectionHeader.kt b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/SectionHeader.kt index 2df0c7a9b1e8..342af3b134b0 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/SectionHeader.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/SectionHeader.kt @@ -24,20 +24,20 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp -import com.android.credentialmanager.ui.theme.LocalAndroidColorScheme +import com.android.compose.theme.LocalAndroidColorScheme @Composable fun CredentialListSectionHeader(text: String, isFirstSection: Boolean) { InternalSectionHeader( text = text, - color = LocalAndroidColorScheme.current.colorOnSurfaceVariant, + color = LocalAndroidColorScheme.current.onSurfaceVariant, applyTopPadding = !isFirstSection ) } @Composable fun MoreAboutPasskeySectionHeader(text: String) { - InternalSectionHeader(text, LocalAndroidColorScheme.current.colorOnSurface) + InternalSectionHeader(text, LocalAndroidColorScheme.current.onSurface) } @Composable diff --git a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/SystemUiControllerUtils.kt b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/SystemUiControllerUtils.kt index a6195237d139..b4075f1c4d80 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/SystemUiControllerUtils.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/SystemUiControllerUtils.kt @@ -19,8 +19,8 @@ package com.android.credentialmanager.common.ui import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.Color import com.android.compose.SystemUiController +import com.android.compose.theme.LocalAndroidColorScheme import com.android.credentialmanager.common.material.ModalBottomSheetDefaults -import com.android.credentialmanager.ui.theme.LocalAndroidColorScheme @Composable fun setTransparentSystemBarsColor(sysUiController: SystemUiController) { @@ -34,7 +34,7 @@ fun setBottomSheetSystemBarsColor(sysUiController: SystemUiController) { darkIcons = false ) sysUiController.setNavigationBarColor( - color = LocalAndroidColorScheme.current.colorSurfaceBright, + color = LocalAndroidColorScheme.current.surfaceBright, darkIcons = false ) }
\ No newline at end of file diff --git a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/Texts.kt b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/Texts.kt index 6b46636964e4..9111e6162cc1 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/Texts.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/Texts.kt @@ -25,7 +25,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.TextLayoutResult import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow -import com.android.credentialmanager.ui.theme.LocalAndroidColorScheme +import com.android.compose.theme.LocalAndroidColorScheme /** * The headline for a screen. E.g. "Create a passkey for X", "Choose a saved sign-in for X". @@ -37,7 +37,7 @@ fun HeadlineText(text: String, modifier: Modifier = Modifier) { Text( modifier = modifier.wrapContentSize(), text = text, - color = LocalAndroidColorScheme.current.colorOnSurface, + color = LocalAndroidColorScheme.current.onSurface, textAlign = TextAlign.Center, style = MaterialTheme.typography.headlineSmall, ) @@ -51,7 +51,7 @@ fun BodyMediumText(text: String, modifier: Modifier = Modifier) { Text( modifier = modifier.wrapContentSize(), text = text, - color = LocalAndroidColorScheme.current.colorOnSurfaceVariant, + color = LocalAndroidColorScheme.current.onSurfaceVariant, style = MaterialTheme.typography.bodyMedium, ) } @@ -69,7 +69,7 @@ fun BodySmallText( Text( modifier = modifier.wrapContentSize(), text = text, - color = LocalAndroidColorScheme.current.colorOnSurfaceVariant, + color = LocalAndroidColorScheme.current.onSurfaceVariant, style = MaterialTheme.typography.bodySmall, overflow = TextOverflow.Ellipsis, maxLines = if (enforceOneLine) 1 else Int.MAX_VALUE, @@ -85,7 +85,7 @@ fun LargeTitleText(text: String, modifier: Modifier = Modifier) { Text( modifier = modifier.wrapContentSize(), text = text, - color = LocalAndroidColorScheme.current.colorOnSurface, + color = LocalAndroidColorScheme.current.onSurface, style = MaterialTheme.typography.titleLarge, ) } @@ -103,7 +103,7 @@ fun SmallTitleText( Text( modifier = modifier.wrapContentSize(), text = text, - color = LocalAndroidColorScheme.current.colorOnSurface, + color = LocalAndroidColorScheme.current.onSurface, style = MaterialTheme.typography.titleSmall, overflow = TextOverflow.Ellipsis, maxLines = if (enforceOneLine) 1 else Int.MAX_VALUE, @@ -159,7 +159,7 @@ fun LargeLabelTextOnSurfaceVariant(text: String, modifier: Modifier = Modifier) modifier = modifier.wrapContentSize(), text = text, textAlign = TextAlign.Center, - color = LocalAndroidColorScheme.current.colorOnSurfaceVariant, + color = LocalAndroidColorScheme.current.onSurfaceVariant, style = MaterialTheme.typography.labelLarge, ) } diff --git a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialComponents.kt b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialComponents.kt index 14a91651753b..f261d1fa062d 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialComponents.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialComponents.kt @@ -46,6 +46,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.core.graphics.drawable.toBitmap +import com.android.compose.theme.LocalAndroidColorScheme import com.android.credentialmanager.CredentialSelectorViewModel import com.android.credentialmanager.R import com.android.credentialmanager.model.EntryInfo @@ -70,7 +71,6 @@ import com.android.credentialmanager.common.ui.HeadlineText import com.android.credentialmanager.logging.CreateCredentialEvent import com.android.credentialmanager.model.creation.CreateOptionInfo import com.android.credentialmanager.model.creation.RemoteInfo -import com.android.credentialmanager.ui.theme.LocalAndroidColorScheme import com.android.internal.logging.UiEventLogger.UiEventEnum @Composable @@ -460,7 +460,7 @@ fun CreationSelectionCard( item { Divider( thickness = 1.dp, - color = LocalAndroidColorScheme.current.colorOutlineVariant, + color = LocalAndroidColorScheme.current.outlineVariant, modifier = Modifier.padding(vertical = 16.dp) ) } diff --git a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt index a291f59021f0..458a99a8cd2f 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt @@ -26,7 +26,7 @@ import com.android.credentialmanager.model.get.RemoteEntryInfo import com.android.internal.util.Preconditions data class GetCredentialUiState( - val isRequestForAllOptions: Boolean, + val isRequestForAllOptions: Boolean, val providerInfoList: List<ProviderInfo>, val requestDisplayInfo: RequestDisplayInfo, val providerDisplayInfo: ProviderDisplayInfo = toProviderDisplayInfo(providerInfoList), @@ -165,7 +165,7 @@ fun toProviderDisplayInfo( ) } -private fun toActiveEntry( +fun toActiveEntry( providerDisplayInfo: ProviderDisplayInfo, ): EntryInfo? { val sortedUserNameToCredentialEntryList = diff --git a/packages/CredentialManager/src/com/android/credentialmanager/ui/theme/AndroidColorScheme.kt b/packages/CredentialManager/src/com/android/credentialmanager/ui/theme/AndroidColorScheme.kt deleted file mode 100644 index a33904d30393..000000000000 --- a/packages/CredentialManager/src/com/android/credentialmanager/ui/theme/AndroidColorScheme.kt +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright (C) 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.credentialmanager.ui.theme - -import android.annotation.ColorInt -import android.content.Context -import androidx.compose.runtime.staticCompositionLocalOf -import androidx.compose.ui.graphics.Color -import com.android.internal.R - -/** File copied from PlatformComposeCore. */ - -/** CompositionLocal used to pass [AndroidColorScheme] down the tree. */ -val LocalAndroidColorScheme = - staticCompositionLocalOf<AndroidColorScheme> { - throw IllegalStateException( - "No AndroidColorScheme configured. Make sure to use LocalAndroidColorScheme in a " + - "Composable surrounded by a PlatformTheme {}." - ) - } - -/** - * The Android color scheme. - * - * Important: Use M3 colors from MaterialTheme.colorScheme whenever possible instead. In the future, - * most of the colors in this class will be removed in favor of their M3 counterpart. - */ -class AndroidColorScheme internal constructor(context: Context) { - val colorSurfaceBright = getColor(context, R.attr.materialColorSurfaceBright) - val colorSurfaceContainerHigh = getColor(context, R.attr.materialColorSurfaceContainerHigh) - val colorOutlineVariant = getColor(context, R.attr.materialColorOutlineVariant) - val colorOnSurface = getColor(context, R.attr.materialColorOnSurface) - val colorOnSurfaceVariant = getColor(context, R.attr.materialColorOnSurfaceVariant) - - companion object { - fun getColor(context: Context, attr: Int): Color { - val ta = context.obtainStyledAttributes(intArrayOf(attr)) - @ColorInt val color = ta.getColor(0, 0) - ta.recycle() - return Color(color) - } - } -} diff --git a/packages/CredentialManager/src/com/android/credentialmanager/ui/theme/PlatformTheme.kt b/packages/CredentialManager/src/com/android/credentialmanager/ui/theme/PlatformTheme.kt deleted file mode 100644 index 2f1ce68db9dc..000000000000 --- a/packages/CredentialManager/src/com/android/credentialmanager/ui/theme/PlatformTheme.kt +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright (C) 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.credentialmanager.ui.theme - -import androidx.compose.foundation.isSystemInDarkTheme -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.dynamicDarkColorScheme -import androidx.compose.material3.dynamicLightColorScheme -import androidx.compose.runtime.Composable -import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.runtime.remember -import androidx.compose.ui.platform.LocalContext -import com.android.credentialmanager.ui.theme.typography.TypeScaleTokens -import com.android.credentialmanager.ui.theme.typography.TypefaceNames -import com.android.credentialmanager.ui.theme.typography.TypefaceTokens -import com.android.credentialmanager.ui.theme.typography.TypographyTokens -import com.android.credentialmanager.ui.theme.typography.platformTypography - -/** File copied from PlatformComposeCore. */ - -/** - * The Material 3 theme that should wrap all Platform Composables. - * - * TODO(b/280685309): Merge with the official SysUI platform theme. - */ -@Composable -fun PlatformTheme( - isDarkTheme: Boolean = isSystemInDarkTheme(), - content: @Composable () -> Unit, -) { - val context = LocalContext.current - - val colorScheme = - if (isDarkTheme) { - dynamicDarkColorScheme(context) - } else { - dynamicLightColorScheme(context) - } - val androidColorScheme = AndroidColorScheme(context) - val typefaceNames = remember(context) { TypefaceNames.get(context) } - val typography = - remember(typefaceNames) { - platformTypography(TypographyTokens(TypeScaleTokens(TypefaceTokens(typefaceNames)))) - } - - MaterialTheme(colorScheme, typography = typography) { - CompositionLocalProvider( - LocalAndroidColorScheme provides androidColorScheme, - ) { - content() - } - } -} diff --git a/packages/CredentialManager/src/com/android/credentialmanager/ui/theme/typography/PlatformTypography.kt b/packages/CredentialManager/src/com/android/credentialmanager/ui/theme/typography/PlatformTypography.kt deleted file mode 100644 index 984e4f19e4d4..000000000000 --- a/packages/CredentialManager/src/com/android/credentialmanager/ui/theme/typography/PlatformTypography.kt +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright (C) 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.credentialmanager.ui.theme.typography - -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Typography - -/** File copied from PlatformComposeCore. */ - -/** - * The typography for Platform Compose code. - * - * Do not use directly and call [MaterialTheme.typography] instead to access the different text - * styles. - */ -internal fun platformTypography(typographyTokens: TypographyTokens): Typography { - return Typography( - displayLarge = typographyTokens.displayLarge, - displayMedium = typographyTokens.displayMedium, - displaySmall = typographyTokens.displaySmall, - headlineLarge = typographyTokens.headlineLarge, - headlineMedium = typographyTokens.headlineMedium, - headlineSmall = typographyTokens.headlineSmall, - titleLarge = typographyTokens.titleLarge, - titleMedium = typographyTokens.titleMedium, - titleSmall = typographyTokens.titleSmall, - bodyLarge = typographyTokens.bodyLarge, - bodyMedium = typographyTokens.bodyMedium, - bodySmall = typographyTokens.bodySmall, - labelLarge = typographyTokens.labelLarge, - labelMedium = typographyTokens.labelMedium, - labelSmall = typographyTokens.labelSmall, - ) -} diff --git a/packages/CredentialManager/src/com/android/credentialmanager/ui/theme/typography/TypeScaleTokens.kt b/packages/CredentialManager/src/com/android/credentialmanager/ui/theme/typography/TypeScaleTokens.kt deleted file mode 100644 index b2dd20720f6a..000000000000 --- a/packages/CredentialManager/src/com/android/credentialmanager/ui/theme/typography/TypeScaleTokens.kt +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Copyright (C) 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.credentialmanager.ui.theme.typography - -import androidx.compose.ui.unit.sp - -/** File copied from PlatformComposeCore. */ -internal class TypeScaleTokens(typefaceTokens: TypefaceTokens) { - val bodyLargeFont = typefaceTokens.plain - val bodyLargeLineHeight = 24.0.sp - val bodyLargeSize = 16.sp - val bodyLargeTracking = 0.0.sp - val bodyLargeWeight = TypefaceTokens.WeightRegular - val bodyMediumFont = typefaceTokens.plain - val bodyMediumLineHeight = 20.0.sp - val bodyMediumSize = 14.sp - val bodyMediumTracking = 0.0.sp - val bodyMediumWeight = TypefaceTokens.WeightRegular - val bodySmallFont = typefaceTokens.plain - val bodySmallLineHeight = 16.0.sp - val bodySmallSize = 12.sp - val bodySmallTracking = 0.1.sp - val bodySmallWeight = TypefaceTokens.WeightRegular - val displayLargeFont = typefaceTokens.brand - val displayLargeLineHeight = 64.0.sp - val displayLargeSize = 57.sp - val displayLargeTracking = 0.0.sp - val displayLargeWeight = TypefaceTokens.WeightRegular - val displayMediumFont = typefaceTokens.brand - val displayMediumLineHeight = 52.0.sp - val displayMediumSize = 45.sp - val displayMediumTracking = 0.0.sp - val displayMediumWeight = TypefaceTokens.WeightRegular - val displaySmallFont = typefaceTokens.brand - val displaySmallLineHeight = 44.0.sp - val displaySmallSize = 36.sp - val displaySmallTracking = 0.0.sp - val displaySmallWeight = TypefaceTokens.WeightRegular - val headlineLargeFont = typefaceTokens.brand - val headlineLargeLineHeight = 40.0.sp - val headlineLargeSize = 32.sp - val headlineLargeTracking = 0.0.sp - val headlineLargeWeight = TypefaceTokens.WeightRegular - val headlineMediumFont = typefaceTokens.brand - val headlineMediumLineHeight = 36.0.sp - val headlineMediumSize = 28.sp - val headlineMediumTracking = 0.0.sp - val headlineMediumWeight = TypefaceTokens.WeightRegular - val headlineSmallFont = typefaceTokens.brand - val headlineSmallLineHeight = 32.0.sp - val headlineSmallSize = 24.sp - val headlineSmallTracking = 0.0.sp - val headlineSmallWeight = TypefaceTokens.WeightRegular - val labelLargeFont = typefaceTokens.plain - val labelLargeLineHeight = 20.0.sp - val labelLargeSize = 14.sp - val labelLargeTracking = 0.0.sp - val labelLargeWeight = TypefaceTokens.WeightMedium - val labelMediumFont = typefaceTokens.plain - val labelMediumLineHeight = 16.0.sp - val labelMediumSize = 12.sp - val labelMediumTracking = 0.1.sp - val labelMediumWeight = TypefaceTokens.WeightMedium - val labelSmallFont = typefaceTokens.plain - val labelSmallLineHeight = 16.0.sp - val labelSmallSize = 11.sp - val labelSmallTracking = 0.1.sp - val labelSmallWeight = TypefaceTokens.WeightMedium - val titleLargeFont = typefaceTokens.brand - val titleLargeLineHeight = 28.0.sp - val titleLargeSize = 22.sp - val titleLargeTracking = 0.0.sp - val titleLargeWeight = TypefaceTokens.WeightRegular - val titleMediumFont = typefaceTokens.plain - val titleMediumLineHeight = 24.0.sp - val titleMediumSize = 16.sp - val titleMediumTracking = 0.0.sp - val titleMediumWeight = TypefaceTokens.WeightMedium - val titleSmallFont = typefaceTokens.plain - val titleSmallLineHeight = 20.0.sp - val titleSmallSize = 14.sp - val titleSmallTracking = 0.0.sp - val titleSmallWeight = TypefaceTokens.WeightMedium -} diff --git a/packages/CredentialManager/src/com/android/credentialmanager/ui/theme/typography/TypefaceTokens.kt b/packages/CredentialManager/src/com/android/credentialmanager/ui/theme/typography/TypefaceTokens.kt deleted file mode 100644 index 3cc761f1cc60..000000000000 --- a/packages/CredentialManager/src/com/android/credentialmanager/ui/theme/typography/TypefaceTokens.kt +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright (C) 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -@file:OptIn(ExperimentalTextApi::class) - -package com.android.credentialmanager.ui.theme.typography - -import android.content.Context -import androidx.compose.ui.text.ExperimentalTextApi -import androidx.compose.ui.text.font.DeviceFontFamilyName -import androidx.compose.ui.text.font.Font -import androidx.compose.ui.text.font.FontFamily -import androidx.compose.ui.text.font.FontWeight - -/** File copied from PlatformComposeCore. */ -internal class TypefaceTokens(typefaceNames: TypefaceNames) { - companion object { - val WeightMedium = FontWeight.Medium - val WeightRegular = FontWeight.Normal - } - - private val brandFont = DeviceFontFamilyName(typefaceNames.brand) - private val plainFont = DeviceFontFamilyName(typefaceNames.plain) - - val brand = - FontFamily( - Font(brandFont, weight = WeightMedium), - Font(brandFont, weight = WeightRegular), - ) - val plain = - FontFamily( - Font(plainFont, weight = WeightMedium), - Font(plainFont, weight = WeightRegular), - ) -} - -internal data class TypefaceNames -private constructor( - val brand: String, - val plain: String, -) { - private enum class Config(val configName: String, val default: String) { - Brand("config_headlineFontFamily", "sans-serif"), - Plain("config_bodyFontFamily", "sans-serif"), - } - - companion object { - fun get(context: Context): TypefaceNames { - return TypefaceNames( - brand = getTypefaceName(context, Config.Brand), - plain = getTypefaceName(context, Config.Plain), - ) - } - - private fun getTypefaceName(context: Context, config: Config): String { - return context - .getString(context.resources.getIdentifier(config.configName, "string", "android")) - .takeIf { it.isNotEmpty() } - ?: config.default - } - } -} diff --git a/packages/CredentialManager/src/com/android/credentialmanager/ui/theme/typography/TypographyTokens.kt b/packages/CredentialManager/src/com/android/credentialmanager/ui/theme/typography/TypographyTokens.kt deleted file mode 100644 index aadab92f40cc..000000000000 --- a/packages/CredentialManager/src/com/android/credentialmanager/ui/theme/typography/TypographyTokens.kt +++ /dev/null @@ -1,143 +0,0 @@ -/* - * Copyright (C) 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.credentialmanager.ui.theme.typography - -import androidx.compose.ui.text.TextStyle - -/** File copied from PlatformComposeCore. */ -internal class TypographyTokens(typeScaleTokens: TypeScaleTokens) { - val bodyLarge = - TextStyle( - fontFamily = typeScaleTokens.bodyLargeFont, - fontWeight = typeScaleTokens.bodyLargeWeight, - fontSize = typeScaleTokens.bodyLargeSize, - lineHeight = typeScaleTokens.bodyLargeLineHeight, - letterSpacing = typeScaleTokens.bodyLargeTracking, - ) - val bodyMedium = - TextStyle( - fontFamily = typeScaleTokens.bodyMediumFont, - fontWeight = typeScaleTokens.bodyMediumWeight, - fontSize = typeScaleTokens.bodyMediumSize, - lineHeight = typeScaleTokens.bodyMediumLineHeight, - letterSpacing = typeScaleTokens.bodyMediumTracking, - ) - val bodySmall = - TextStyle( - fontFamily = typeScaleTokens.bodySmallFont, - fontWeight = typeScaleTokens.bodySmallWeight, - fontSize = typeScaleTokens.bodySmallSize, - lineHeight = typeScaleTokens.bodySmallLineHeight, - letterSpacing = typeScaleTokens.bodySmallTracking, - ) - val displayLarge = - TextStyle( - fontFamily = typeScaleTokens.displayLargeFont, - fontWeight = typeScaleTokens.displayLargeWeight, - fontSize = typeScaleTokens.displayLargeSize, - lineHeight = typeScaleTokens.displayLargeLineHeight, - letterSpacing = typeScaleTokens.displayLargeTracking, - ) - val displayMedium = - TextStyle( - fontFamily = typeScaleTokens.displayMediumFont, - fontWeight = typeScaleTokens.displayMediumWeight, - fontSize = typeScaleTokens.displayMediumSize, - lineHeight = typeScaleTokens.displayMediumLineHeight, - letterSpacing = typeScaleTokens.displayMediumTracking, - ) - val displaySmall = - TextStyle( - fontFamily = typeScaleTokens.displaySmallFont, - fontWeight = typeScaleTokens.displaySmallWeight, - fontSize = typeScaleTokens.displaySmallSize, - lineHeight = typeScaleTokens.displaySmallLineHeight, - letterSpacing = typeScaleTokens.displaySmallTracking, - ) - val headlineLarge = - TextStyle( - fontFamily = typeScaleTokens.headlineLargeFont, - fontWeight = typeScaleTokens.headlineLargeWeight, - fontSize = typeScaleTokens.headlineLargeSize, - lineHeight = typeScaleTokens.headlineLargeLineHeight, - letterSpacing = typeScaleTokens.headlineLargeTracking, - ) - val headlineMedium = - TextStyle( - fontFamily = typeScaleTokens.headlineMediumFont, - fontWeight = typeScaleTokens.headlineMediumWeight, - fontSize = typeScaleTokens.headlineMediumSize, - lineHeight = typeScaleTokens.headlineMediumLineHeight, - letterSpacing = typeScaleTokens.headlineMediumTracking, - ) - val headlineSmall = - TextStyle( - fontFamily = typeScaleTokens.headlineSmallFont, - fontWeight = typeScaleTokens.headlineSmallWeight, - fontSize = typeScaleTokens.headlineSmallSize, - lineHeight = typeScaleTokens.headlineSmallLineHeight, - letterSpacing = typeScaleTokens.headlineSmallTracking, - ) - val labelLarge = - TextStyle( - fontFamily = typeScaleTokens.labelLargeFont, - fontWeight = typeScaleTokens.labelLargeWeight, - fontSize = typeScaleTokens.labelLargeSize, - lineHeight = typeScaleTokens.labelLargeLineHeight, - letterSpacing = typeScaleTokens.labelLargeTracking, - ) - val labelMedium = - TextStyle( - fontFamily = typeScaleTokens.labelMediumFont, - fontWeight = typeScaleTokens.labelMediumWeight, - fontSize = typeScaleTokens.labelMediumSize, - lineHeight = typeScaleTokens.labelMediumLineHeight, - letterSpacing = typeScaleTokens.labelMediumTracking, - ) - val labelSmall = - TextStyle( - fontFamily = typeScaleTokens.labelSmallFont, - fontWeight = typeScaleTokens.labelSmallWeight, - fontSize = typeScaleTokens.labelSmallSize, - lineHeight = typeScaleTokens.labelSmallLineHeight, - letterSpacing = typeScaleTokens.labelSmallTracking, - ) - val titleLarge = - TextStyle( - fontFamily = typeScaleTokens.titleLargeFont, - fontWeight = typeScaleTokens.titleLargeWeight, - fontSize = typeScaleTokens.titleLargeSize, - lineHeight = typeScaleTokens.titleLargeLineHeight, - letterSpacing = typeScaleTokens.titleLargeTracking, - ) - val titleMedium = - TextStyle( - fontFamily = typeScaleTokens.titleMediumFont, - fontWeight = typeScaleTokens.titleMediumWeight, - fontSize = typeScaleTokens.titleMediumSize, - lineHeight = typeScaleTokens.titleMediumLineHeight, - letterSpacing = typeScaleTokens.titleMediumTracking, - ) - val titleSmall = - TextStyle( - fontFamily = typeScaleTokens.titleSmallFont, - fontWeight = typeScaleTokens.titleSmallWeight, - fontSize = typeScaleTokens.titleSmallSize, - lineHeight = typeScaleTokens.titleSmallLineHeight, - letterSpacing = typeScaleTokens.titleSmallTracking, - ) -} diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/UnarchiveActivity.java b/packages/PackageInstaller/src/com/android/packageinstaller/UnarchiveActivity.java index b5af845ea0ac..9af799c37e8f 100644 --- a/packages/PackageInstaller/src/com/android/packageinstaller/UnarchiveActivity.java +++ b/packages/PackageInstaller/src/com/android/packageinstaller/UnarchiveActivity.java @@ -31,7 +31,7 @@ import android.os.Bundle; import android.os.Process; import android.util.Log; -import androidx.annotation.Nullable; +import androidx.annotation.NonNull; import java.io.IOException; import java.util.Arrays; @@ -105,7 +105,7 @@ public class UnarchiveActivity extends Activity { } } - @Nullable + @NonNull private String[] getRequestedPermissions(String callingPackage) { String[] requestedPermissions = null; try { @@ -115,7 +115,7 @@ public class UnarchiveActivity extends Activity { // Should be unreachable because we've just fetched the packageName above. Log.e(TAG, "Package not found for " + callingPackage); } - return requestedPermissions; + return requestedPermissions == null ? new String[]{} : requestedPermissions; } void startUnarchive() { diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/handheld/UninstallAlertDialogFragment.java b/packages/PackageInstaller/src/com/android/packageinstaller/handheld/UninstallAlertDialogFragment.java index 2da8c8c69ff8..221ca4fd1c66 100644 --- a/packages/PackageInstaller/src/com/android/packageinstaller/handheld/UninstallAlertDialogFragment.java +++ b/packages/PackageInstaller/src/com/android/packageinstaller/handheld/UninstallAlertDialogFragment.java @@ -32,6 +32,7 @@ import android.content.pm.PackageManager; import android.os.Bundle; import android.os.Flags; import android.os.Process; +import android.os.SystemProperties; import android.os.UserHandle; import android.os.UserManager; import android.util.Log; @@ -131,7 +132,7 @@ public class UninstallAlertDialogFragment extends DialogFragment implements final boolean isUpdate = ((dialogInfo.appInfo.flags & ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) != 0); final boolean isArchive = - android.content.pm.Flags.archiving() && ( + isArchivingEnabled() && ( (dialogInfo.deleteFlags & PackageManager.DELETE_ARCHIVE) != 0); final UserHandle myUserHandle = Process.myUserHandle(); UserManager userManager = getContext().getSystemService(UserManager.class); @@ -242,6 +243,11 @@ public class UninstallAlertDialogFragment extends DialogFragment implements return dialogBuilder.create(); } + private static boolean isArchivingEnabled() { + return android.content.pm.Flags.archiving() + || SystemProperties.getBoolean("pm.archiving.enabled", false); + } + private boolean isCloneProfile(UserHandle userHandle) { UserManager customUserManager = getContext() .createContextAsUser(UserHandle.of(userHandle.getIdentifier()), 0) diff --git a/packages/SettingsLib/ActionBarShadow/Android.bp b/packages/SettingsLib/ActionBarShadow/Android.bp index 6f9445874fce..77cbb00427dc 100644 --- a/packages/SettingsLib/ActionBarShadow/Android.bp +++ b/packages/SettingsLib/ActionBarShadow/Android.bp @@ -10,6 +10,9 @@ package { android_library { name: "SettingsLibActionBarShadow", use_resource_processor: true, + defaults: [ + "SettingsLintDefaults", + ], srcs: ["src/**/*.java"], diff --git a/packages/SettingsLib/ActionButtonsPreference/Android.bp b/packages/SettingsLib/ActionButtonsPreference/Android.bp index 122855561751..c36b82d175e2 100644 --- a/packages/SettingsLib/ActionButtonsPreference/Android.bp +++ b/packages/SettingsLib/ActionButtonsPreference/Android.bp @@ -10,6 +10,9 @@ package { android_library { name: "SettingsLibActionButtonsPreference", use_resource_processor: true, + defaults: [ + "SettingsLintDefaults", + ], srcs: ["src/**/*.java"], resource_dirs: ["res"], diff --git a/packages/SettingsLib/ActivityEmbedding/Android.bp b/packages/SettingsLib/ActivityEmbedding/Android.bp index 41de29a93e51..838a9e505ece 100644 --- a/packages/SettingsLib/ActivityEmbedding/Android.bp +++ b/packages/SettingsLib/ActivityEmbedding/Android.bp @@ -10,6 +10,9 @@ package { android_library { name: "SettingsLibActivityEmbedding", use_resource_processor: true, + defaults: [ + "SettingsLintDefaults", + ], srcs: ["src/**/*.java"], diff --git a/packages/SettingsLib/AdaptiveIcon/Android.bp b/packages/SettingsLib/AdaptiveIcon/Android.bp index 044ba872f3e5..67b6fb5f2ed9 100644 --- a/packages/SettingsLib/AdaptiveIcon/Android.bp +++ b/packages/SettingsLib/AdaptiveIcon/Android.bp @@ -10,6 +10,9 @@ package { android_library { name: "SettingsLibAdaptiveIcon", use_resource_processor: true, + defaults: [ + "SettingsLintDefaults", + ], srcs: ["src/**/*.java"], resource_dirs: ["res"], diff --git a/packages/SettingsLib/Android.bp b/packages/SettingsLib/Android.bp index 5da4b9518a31..c2cb75709b45 100644 --- a/packages/SettingsLib/Android.bp +++ b/packages/SettingsLib/Android.bp @@ -9,6 +9,9 @@ package { android_library { name: "SettingsLib", + defaults: [ + "SettingsLintDefaults", + ], static_libs: [ "androidx.localbroadcastmanager_localbroadcastmanager", @@ -60,8 +63,15 @@ android_library { "src/**/*.java", "src/**/*.kt", ], +} + +// defaults for lint option +java_defaults { + name: "SettingsLintDefaults", lint: { - extra_check_modules: ["SettingsLibLintChecker"], + extra_check_modules: [ + "SettingsLibLintChecker", + ], }, } diff --git a/packages/SettingsLib/AppPreference/Android.bp b/packages/SettingsLib/AppPreference/Android.bp index 69b9d44fe16f..c5b2ef686688 100644 --- a/packages/SettingsLib/AppPreference/Android.bp +++ b/packages/SettingsLib/AppPreference/Android.bp @@ -10,6 +10,9 @@ package { android_library { name: "SettingsLibAppPreference", use_resource_processor: true, + defaults: [ + "SettingsLintDefaults", + ], srcs: ["src/**/*.java"], resource_dirs: ["res"], diff --git a/packages/SettingsLib/BannerMessagePreference/Android.bp b/packages/SettingsLib/BannerMessagePreference/Android.bp index da91344242a1..07290de8661e 100644 --- a/packages/SettingsLib/BannerMessagePreference/Android.bp +++ b/packages/SettingsLib/BannerMessagePreference/Android.bp @@ -10,6 +10,9 @@ package { android_library { name: "SettingsLibBannerMessagePreference", use_resource_processor: true, + defaults: [ + "SettingsLintDefaults", + ], srcs: ["src/**/*.java"], resource_dirs: ["res"], diff --git a/packages/SettingsLib/BarChartPreference/Android.bp b/packages/SettingsLib/BarChartPreference/Android.bp index be1e0cf8ab4f..448ed56a7f28 100644 --- a/packages/SettingsLib/BarChartPreference/Android.bp +++ b/packages/SettingsLib/BarChartPreference/Android.bp @@ -10,6 +10,9 @@ package { android_library { name: "SettingsLibBarChartPreference", use_resource_processor: true, + defaults: [ + "SettingsLintDefaults", + ], srcs: ["src/**/*.java"], resource_dirs: ["res"], diff --git a/packages/SettingsLib/ButtonPreference/Android.bp b/packages/SettingsLib/ButtonPreference/Android.bp index 35572fad55a2..0382829b2652 100644 --- a/packages/SettingsLib/ButtonPreference/Android.bp +++ b/packages/SettingsLib/ButtonPreference/Android.bp @@ -10,6 +10,9 @@ package { android_library { name: "SettingsLibButtonPreference", use_resource_processor: true, + defaults: [ + "SettingsLintDefaults", + ], srcs: ["src/**/*.java"], resource_dirs: ["res"], diff --git a/packages/SettingsLib/CollapsingToolbarBaseActivity/Android.bp b/packages/SettingsLib/CollapsingToolbarBaseActivity/Android.bp index 70f7554d5e53..87ec0b8d46fb 100644 --- a/packages/SettingsLib/CollapsingToolbarBaseActivity/Android.bp +++ b/packages/SettingsLib/CollapsingToolbarBaseActivity/Android.bp @@ -10,6 +10,9 @@ package { android_library { name: "SettingsLibCollapsingToolbarBaseActivity", use_resource_processor: true, + defaults: [ + "SettingsLintDefaults", + ], srcs: ["src/**/*.java"], resource_dirs: ["res"], diff --git a/packages/SettingsLib/DisplayUtils/Android.bp b/packages/SettingsLib/DisplayUtils/Android.bp index eab35a11d7d6..279bb70d81bf 100644 --- a/packages/SettingsLib/DisplayUtils/Android.bp +++ b/packages/SettingsLib/DisplayUtils/Android.bp @@ -10,6 +10,9 @@ package { android_library { name: "SettingsLibDisplayUtils", use_resource_processor: true, + defaults: [ + "SettingsLintDefaults", + ], srcs: ["src/**/*.java"], diff --git a/packages/SettingsLib/EntityHeaderWidgets/Android.bp b/packages/SettingsLib/EntityHeaderWidgets/Android.bp index 17b662c60227..83f81c60c856 100644 --- a/packages/SettingsLib/EntityHeaderWidgets/Android.bp +++ b/packages/SettingsLib/EntityHeaderWidgets/Android.bp @@ -10,13 +10,16 @@ package { android_library { name: "SettingsLibEntityHeaderWidgets", use_resource_processor: true, + defaults: [ + "SettingsLintDefaults", + ], srcs: ["src/**/*.java"], resource_dirs: ["res"], static_libs: [ - "androidx.annotation_annotation", - "SettingsLibSettingsTheme" + "androidx.annotation_annotation", + "SettingsLibSettingsTheme", ], sdk_version: "system_current", diff --git a/packages/SettingsLib/FooterPreference/Android.bp b/packages/SettingsLib/FooterPreference/Android.bp index b45cd65467d2..d1ad80d5a4d7 100644 --- a/packages/SettingsLib/FooterPreference/Android.bp +++ b/packages/SettingsLib/FooterPreference/Android.bp @@ -10,6 +10,9 @@ package { android_library { name: "SettingsLibFooterPreference", use_resource_processor: true, + defaults: [ + "SettingsLintDefaults", + ], srcs: ["src/**/*.java"], resource_dirs: ["res"], diff --git a/packages/SettingsLib/HelpUtils/Android.bp b/packages/SettingsLib/HelpUtils/Android.bp index 041fce254b72..284106e96fda 100644 --- a/packages/SettingsLib/HelpUtils/Android.bp +++ b/packages/SettingsLib/HelpUtils/Android.bp @@ -10,6 +10,9 @@ package { android_library { name: "SettingsLibHelpUtils", use_resource_processor: true, + defaults: [ + "SettingsLintDefaults", + ], srcs: ["src/**/*.java"], resource_dirs: ["res"], diff --git a/packages/SettingsLib/IllustrationPreference/Android.bp b/packages/SettingsLib/IllustrationPreference/Android.bp index 4d4759b99f15..6407810367cf 100644 --- a/packages/SettingsLib/IllustrationPreference/Android.bp +++ b/packages/SettingsLib/IllustrationPreference/Android.bp @@ -10,6 +10,9 @@ package { android_library { name: "SettingsLibIllustrationPreference", use_resource_processor: true, + defaults: [ + "SettingsLintDefaults", + ], srcs: ["src/**/*.java"], resource_dirs: ["res"], diff --git a/packages/SettingsLib/LayoutPreference/Android.bp b/packages/SettingsLib/LayoutPreference/Android.bp index 53ded2385634..8cf636ac8de3 100644 --- a/packages/SettingsLib/LayoutPreference/Android.bp +++ b/packages/SettingsLib/LayoutPreference/Android.bp @@ -10,6 +10,9 @@ package { android_library { name: "SettingsLibLayoutPreference", use_resource_processor: true, + defaults: [ + "SettingsLintDefaults", + ], srcs: ["src/**/*.java"], resource_dirs: ["res"], diff --git a/packages/SettingsLib/MainSwitchPreference/Android.bp b/packages/SettingsLib/MainSwitchPreference/Android.bp index 010a6ce9d4d9..b984aaf050d5 100644 --- a/packages/SettingsLib/MainSwitchPreference/Android.bp +++ b/packages/SettingsLib/MainSwitchPreference/Android.bp @@ -10,6 +10,9 @@ package { android_library { name: "SettingsLibMainSwitchPreference", use_resource_processor: true, + defaults: [ + "SettingsLintDefaults", + ], srcs: ["src/**/*.java"], resource_dirs: ["res"], diff --git a/packages/SettingsLib/ProfileSelector/Android.bp b/packages/SettingsLib/ProfileSelector/Android.bp index 155ed2e091f8..6dc07b29a510 100644 --- a/packages/SettingsLib/ProfileSelector/Android.bp +++ b/packages/SettingsLib/ProfileSelector/Android.bp @@ -10,6 +10,9 @@ package { android_library { name: "SettingsLibProfileSelector", use_resource_processor: true, + defaults: [ + "SettingsLintDefaults", + ], srcs: ["src/**/*.java"], resource_dirs: ["res"], diff --git a/packages/SettingsLib/RestrictedLockUtils/Android.bp b/packages/SettingsLib/RestrictedLockUtils/Android.bp index 3b04bd99e0f4..8d722eba49bf 100644 --- a/packages/SettingsLib/RestrictedLockUtils/Android.bp +++ b/packages/SettingsLib/RestrictedLockUtils/Android.bp @@ -16,6 +16,9 @@ filegroup { android_library { name: "SettingsLibRestrictedLockUtils", use_resource_processor: true, + defaults: [ + "SettingsLintDefaults", + ], srcs: ["src/**/*.java"], resource_dirs: ["res"], diff --git a/packages/SettingsLib/SchedulesProvider/Android.bp b/packages/SettingsLib/SchedulesProvider/Android.bp index 22e4e94b80b1..c0fc741e7447 100644 --- a/packages/SettingsLib/SchedulesProvider/Android.bp +++ b/packages/SettingsLib/SchedulesProvider/Android.bp @@ -10,6 +10,9 @@ package { android_library { name: "SettingsLibSchedulesProvider", use_resource_processor: true, + defaults: [ + "SettingsLintDefaults", + ], srcs: ["src/**/*.java"], diff --git a/packages/SettingsLib/SearchProvider/Android.bp b/packages/SettingsLib/SearchProvider/Android.bp index c385d385dcc9..61ed65cbe46f 100644 --- a/packages/SettingsLib/SearchProvider/Android.bp +++ b/packages/SettingsLib/SearchProvider/Android.bp @@ -10,6 +10,9 @@ package { android_library { name: "SettingsLibSearchProvider", use_resource_processor: true, + defaults: [ + "SettingsLintDefaults", + ], srcs: ["src/**/*.java"], diff --git a/packages/SettingsLib/SelectorWithWidgetPreference/Android.bp b/packages/SettingsLib/SelectorWithWidgetPreference/Android.bp index 702387ecadab..2fe446d24b34 100644 --- a/packages/SettingsLib/SelectorWithWidgetPreference/Android.bp +++ b/packages/SettingsLib/SelectorWithWidgetPreference/Android.bp @@ -10,6 +10,9 @@ package { android_library { name: "SettingsLibSelectorWithWidgetPreference", use_resource_processor: true, + defaults: [ + "SettingsLintDefaults", + ], srcs: ["src/**/*.java"], resource_dirs: ["res"], diff --git a/packages/SettingsLib/SelectorWithWidgetPreference/res/layout-v33/preference_selector_with_widget.xml b/packages/SettingsLib/SelectorWithWidgetPreference/res/layout-v33/preference_selector_with_widget.xml index 0b274646214a..0764609d66d3 100644 --- a/packages/SettingsLib/SelectorWithWidgetPreference/res/layout-v33/preference_selector_with_widget.xml +++ b/packages/SettingsLib/SelectorWithWidgetPreference/res/layout-v33/preference_selector_with_widget.xml @@ -66,6 +66,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:maxLines="2" + android:ellipsize="end" android:hyphenationFrequency="normalFast" android:lineBreakWordStyle="phrase" android:textAppearance="?android:attr/textAppearanceListItem"/> diff --git a/packages/SettingsLib/SelectorWithWidgetPreference/res/layout/preference_selector_with_widget.xml b/packages/SettingsLib/SelectorWithWidgetPreference/res/layout/preference_selector_with_widget.xml index 8bb56ff0a07d..4f1a9102c35c 100644 --- a/packages/SettingsLib/SelectorWithWidgetPreference/res/layout/preference_selector_with_widget.xml +++ b/packages/SettingsLib/SelectorWithWidgetPreference/res/layout/preference_selector_with_widget.xml @@ -66,6 +66,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:maxLines="2" + android:ellipsize="end" android:textAppearance="?android:attr/textAppearanceListItem"/> <LinearLayout diff --git a/packages/SettingsLib/SettingsSpinner/Android.bp b/packages/SettingsLib/SettingsSpinner/Android.bp index 0eec50563a75..8fed61fd3f13 100644 --- a/packages/SettingsLib/SettingsSpinner/Android.bp +++ b/packages/SettingsLib/SettingsSpinner/Android.bp @@ -10,6 +10,9 @@ package { android_library { name: "SettingsLibSettingsSpinner", use_resource_processor: true, + defaults: [ + "SettingsLintDefaults", + ], srcs: ["src/**/*.java"], resource_dirs: ["res"], diff --git a/packages/SettingsLib/SettingsTransition/Android.bp b/packages/SettingsLib/SettingsTransition/Android.bp index 06493c056203..e04af6c1ab11 100644 --- a/packages/SettingsLib/SettingsTransition/Android.bp +++ b/packages/SettingsLib/SettingsTransition/Android.bp @@ -10,6 +10,9 @@ package { android_library { name: "SettingsLibSettingsTransition", use_resource_processor: true, + defaults: [ + "SettingsLintDefaults", + ], srcs: ["src/**/*.java"], diff --git a/packages/SettingsLib/Spa/gallery/AndroidManifest.xml b/packages/SettingsLib/Spa/gallery/AndroidManifest.xml index 965fdcfd6f98..df5644b8aad0 100644 --- a/packages/SettingsLib/Spa/gallery/AndroidManifest.xml +++ b/packages/SettingsLib/Spa/gallery/AndroidManifest.xml @@ -73,6 +73,10 @@ android:authorities="com.android.spa.gallery.debug.provider" android:exported="false"> </provider> - + <activity + android:name="com.android.settingslib.spa.gallery.GalleryDialogActivity" + android:exported="true" + android:theme="@style/Theme.SpaLib.Dialog"> + </activity> </application> </manifest> diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GalleryDialogActivity.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GalleryDialogActivity.kt new file mode 100644 index 000000000000..e22ed355bff1 --- /dev/null +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GalleryDialogActivity.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.settingslib.spa.gallery + +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign +import com.android.settingslib.spa.SpaBaseDialogActivity +import com.android.settingslib.spa.widget.dialog.AlertDialogButton +import com.android.settingslib.spa.widget.dialog.SettingsAlertDialogWithIcon + +class GalleryDialogActivity : SpaBaseDialogActivity() { + @Composable + override fun Content() { + SettingsAlertDialogWithIcon( + onDismissRequest = { finish() }, + confirmButton = AlertDialogButton("confirm") { finish() }, + dismissButton = AlertDialogButton("dismiss") { finish() }, + title = "title", + text = { + Text( + "text", + modifier = Modifier.fillMaxWidth(), + textAlign = TextAlign.Center + ) + } + ) + } +} diff --git a/packages/SettingsLib/Spa/spa/res/values/themes.xml b/packages/SettingsLib/Spa/spa/res/values/themes.xml index 25846ec2d20b..4b5a9bc88ca0 100644 --- a/packages/SettingsLib/Spa/spa/res/values/themes.xml +++ b/packages/SettingsLib/Spa/spa/res/values/themes.xml @@ -22,4 +22,6 @@ <item name="android:windowActionBar">false</item> <item name="android:windowNoTitle">true</item> </style> + + <style name="Theme.SpaLib.Dialog" parent="Theme.Material3.DayNight.Dialog"/> </resources> diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/SpaBaseDialogActivity.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/SpaBaseDialogActivity.kt new file mode 100644 index 000000000000..dfb780af214b --- /dev/null +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/SpaBaseDialogActivity.kt @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib.spa + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.runtime.Composable +import com.android.settingslib.spa.framework.common.LogCategory +import com.android.settingslib.spa.framework.common.SpaEnvironmentFactory +import com.android.settingslib.spa.framework.theme.SettingsTheme + +abstract class SpaBaseDialogActivity : ComponentActivity() { + private val spaEnvironment get() = SpaEnvironmentFactory.instance + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + spaEnvironment.logger.message(TAG, "onCreate", category = LogCategory.FRAMEWORK) + setContent { + SettingsTheme { + Content() + } + } + } + + @Composable + abstract fun Content() + + companion object { + private const val TAG = "SpaBaseDialogActivity" + } +} diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/dialog/SettingsAlertDialog.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/dialog/SettingsAlertDialog.kt index 207c174fcf4a..de080e3d8ef4 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/dialog/SettingsAlertDialog.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/dialog/SettingsAlertDialog.kt @@ -99,11 +99,11 @@ private fun AlertDialogPresenter.SettingsAlertDialog( } @Composable -private fun getDialogWidth(): Dp { +fun getDialogWidth(): Dp { val configuration = LocalConfiguration.current return configuration.screenWidthDp.dp * when (configuration.orientation) { - Configuration.ORIENTATION_LANDSCAPE -> 0.6f - else -> 0.8f + Configuration.ORIENTATION_LANDSCAPE -> 0.65f + else -> 0.85f } } diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/dialog/SettingsAlertDialogWithIcon.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/dialog/SettingsAlertDialogWithIcon.kt new file mode 100644 index 000000000000..1695e4f33915 --- /dev/null +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/dialog/SettingsAlertDialogWithIcon.kt @@ -0,0 +1,88 @@ +/* + * 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.settingslib.spa.widget.dialog + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.WarningAmber +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Button +import androidx.compose.material3.Icon +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.window.DialogProperties + +@Composable +fun SettingsAlertDialogWithIcon( + onDismissRequest: () -> Unit, + confirmButton: AlertDialogButton?, + dismissButton: AlertDialogButton?, + title: String?, + text: @Composable (() -> Unit)?, +) { + AlertDialog( + onDismissRequest = onDismissRequest, + icon = { Icon(Icons.Default.WarningAmber, contentDescription = null) }, + modifier = Modifier.width(getDialogWidth()), + confirmButton = { + confirmButton?.let { + Button( + onClick = { + it.onClick() + }, + ) { + Text(it.text) + } + } + }, + dismissButton = dismissButton?.let { + { + OutlinedButton( + onClick = { + it.onClick() + }, + ) { + Text(it.text) + } + } + }, + title = title?.let { + { + Text( + it, + modifier = Modifier.fillMaxWidth(), + textAlign = TextAlign.Center + ) + } + }, + text = text?.let { + { + Column(Modifier.verticalScroll(rememberScrollState())) { + text() + } + } + }, + properties = DialogProperties(usePlatformDefaultWidth = false), + ) +}
\ No newline at end of file diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppListRepository.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppListRepository.kt index abeffece2cd0..0a98791e8a6a 100644 --- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppListRepository.kt +++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppListRepository.kt @@ -24,6 +24,7 @@ import android.content.pm.ApplicationInfo import android.content.pm.PackageManager import android.content.pm.PackageManager.ApplicationInfoFlags import android.content.pm.ResolveInfo +import android.os.SystemProperties import com.android.internal.R import com.android.settingslib.spaprivileged.framework.common.userManager import kotlinx.coroutines.async @@ -110,7 +111,7 @@ class AppListRepositoryImpl( ): List<ApplicationInfo> { val disabledComponentsFlag = (PackageManager.MATCH_DISABLED_COMPONENTS or PackageManager.MATCH_DISABLED_UNTIL_USED_COMPONENTS).toLong() - val archivedPackagesFlag: Long = if (featureFlags.archiving()) + val archivedPackagesFlag: Long = if (isArchivingEnabled(featureFlags)) PackageManager.MATCH_ARCHIVED_PACKAGES else 0L val regularFlags = ApplicationInfoFlags.of( disabledComponentsFlag or @@ -148,6 +149,9 @@ class AppListRepositoryImpl( } } + private fun isArchivingEnabled(featureFlags: FeatureFlags) = + featureFlags.archiving() || SystemProperties.getBoolean("pm.archiving.enabled", false) + override fun showSystemPredicate( userIdFlow: Flow<Int>, showSystemFlow: Flow<Boolean>, diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppInfoPage.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppInfoPage.kt index 1c830c1c5b06..74b556ea106e 100644 --- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppInfoPage.kt +++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppInfoPage.kt @@ -162,6 +162,7 @@ internal fun <T : AppRecord> TogglePermissionAppListModel<T>.TogglePermissionApp uid = checkNotNull(applicationInfo).uid, packageName = packageName) }) RestrictedSwitchPreference(switchModel, restrictions, restrictionsProviderFactory) + InfoPageAdditionalContent(record, isAllowed) } } diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppList.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppList.kt index 916d83af3f8f..3f7a8526839f 100644 --- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppList.kt +++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/TogglePermissionAppList.kt @@ -77,6 +77,9 @@ interface TogglePermissionAppListModel<T : AppRecord> { * Sets whether the permission is allowed for the given app. */ fun setAllowed(record: T, newAllowed: Boolean) + + @Composable + fun InfoPageAdditionalContent(record: T, isAllowed: () -> Boolean?){} } interface TogglePermissionAppListProvider { diff --git a/packages/SettingsLib/Tile/Android.bp b/packages/SettingsLib/Tile/Android.bp index 19c59dd221c2..54b97487c3cd 100644 --- a/packages/SettingsLib/Tile/Android.bp +++ b/packages/SettingsLib/Tile/Android.bp @@ -10,6 +10,9 @@ package { android_library { name: "SettingsLibTile", use_resource_processor: true, + defaults: [ + "SettingsLintDefaults", + ], srcs: ["src/**/*.java"], diff --git a/packages/SettingsLib/TopIntroPreference/Android.bp b/packages/SettingsLib/TopIntroPreference/Android.bp index 77b7ac1246bd..e70201b0feb7 100644 --- a/packages/SettingsLib/TopIntroPreference/Android.bp +++ b/packages/SettingsLib/TopIntroPreference/Android.bp @@ -10,6 +10,9 @@ package { android_library { name: "SettingsLibTopIntroPreference", use_resource_processor: true, + defaults: [ + "SettingsLintDefaults", + ], srcs: ["src/**/*.java"], resource_dirs: ["res"], diff --git a/packages/SettingsLib/TwoTargetPreference/Android.bp b/packages/SettingsLib/TwoTargetPreference/Android.bp index 5aa906ec0ab3..70d9630d1865 100644 --- a/packages/SettingsLib/TwoTargetPreference/Android.bp +++ b/packages/SettingsLib/TwoTargetPreference/Android.bp @@ -10,6 +10,9 @@ package { android_library { name: "SettingsLibTwoTargetPreference", use_resource_processor: true, + defaults: [ + "SettingsLintDefaults", + ], srcs: ["src/**/*.java"], resource_dirs: ["res"], diff --git a/packages/SettingsLib/UsageProgressBarPreference/Android.bp b/packages/SettingsLib/UsageProgressBarPreference/Android.bp index 4cc90ccbfe80..0a83aabfce7b 100644 --- a/packages/SettingsLib/UsageProgressBarPreference/Android.bp +++ b/packages/SettingsLib/UsageProgressBarPreference/Android.bp @@ -10,6 +10,9 @@ package { android_library { name: "SettingsLibUsageProgressBarPreference", use_resource_processor: true, + defaults: [ + "SettingsLintDefaults", + ], srcs: ["src/**/*.java"], resource_dirs: ["res"], diff --git a/packages/SettingsLib/Utils/Android.bp b/packages/SettingsLib/Utils/Android.bp index d5a56c86431f..5d2aef780385 100644 --- a/packages/SettingsLib/Utils/Android.bp +++ b/packages/SettingsLib/Utils/Android.bp @@ -10,6 +10,9 @@ package { android_library { name: "SettingsLibUtils", use_resource_processor: true, + defaults: [ + "SettingsLintDefaults", + ], srcs: ["src/**/*.java"], resource_dirs: ["res"], diff --git a/packages/SettingsLib/res/values/strings.xml b/packages/SettingsLib/res/values/strings.xml index 5aa2bfc6441a..a4b3af990c17 100644 --- a/packages/SettingsLib/res/values/strings.xml +++ b/packages/SettingsLib/res/values/strings.xml @@ -338,6 +338,9 @@ <!-- Message for telling the user the kind of BT device being displayed in list. [CHAR LIMIT=30 BACKUP_MESSAGE_ID=5165842622743212268] --> <string name="bluetooth_talkback_input_peripheral">Input Peripheral</string> + <!-- Message for telling the user the kind of BT device being displayed in list. [CHAR LIMIT=30 BACKUP_MESSAGE_ID=26580326066627664] --> + <string name="bluetooth_talkback_hearing_aids">Hearing Aids</string> + <!-- Message for telling the user the kind of BT device being displayed in list. [CHAR LIMIT=30 BACKUP_MESSAGE_ID=5615463912185280812] --> <string name="bluetooth_talkback_bluetooth">Bluetooth</string> @@ -1079,6 +1082,10 @@ <!-- [CHAR_LIMIT=NONE] Label for battery on main page of settings --> <string name="power_remaining_settings_home_page"><xliff:g id="percentage" example="10%">%1$s</xliff:g> - <xliff:g id="time_string" example="1 hour left based on your usage">%2$s</xliff:g></string> + <!-- [CHAR_LIMIT=NONE] Label for charging on hold on main page of settings --> + <string name="power_charging_on_hold_settings_home_page"><xliff:g id="level">%1$s</xliff:g> - Charging on hold to protect battery</string> + <!-- [CHAR_LIMIT=NONE] Label for incompatible charging accessory on main page of settings --> + <string name="power_incompatible_charging_settings_home_page"><xliff:g id="level">%1$s</xliff:g> - Checking charging accessory</string> <!-- [CHAR_LIMIT=40] Label for estimated remaining duration of battery discharging --> <string name="power_remaining_duration_only">About <xliff:g id="time_remaining">%1$s</xliff:g> left</string> <!-- [CHAR_LIMIT=40] Label for battery level chart when discharging with duration --> @@ -1139,11 +1146,13 @@ <!-- Battery Info screen. Value for a status item. Used for diagnostic info screens, precise translation isn't needed --> <string name="battery_info_status_discharging">Not charging</string> <!-- Battery Info screen. Value for a status item. A state which device is connected with any charger(e.g. USB, Adapter or Wireless) but not charging yet. Used for diagnostic info screens, precise translation isn't needed --> - <string name="battery_info_status_not_charging">Connected, not charging</string> + <string name="battery_info_status_not_charging">Connected, but not charging</string> <!-- Battery Info screen. Value for a status item. Used for diagnostic info screens, precise translation isn't needed --> <string name="battery_info_status_full">Charged</string> <!-- [CHAR_LIMIT=40] Battery Info screen. Value for a status item. A state which device is fully charged --> <string name="battery_info_status_full_charged">Fully Charged</string> + <!-- [CHAR_LIMIT=None] Battery Info screen. Value for a status item. A state which device charging on hold --> + <string name="battery_info_status_charging_on_hold">Charging on hold</string> <!-- Summary for settings preference disabled by administrator [CHAR LIMIT=50] --> <string name="disabled_by_admin_summary_text">Controlled by admin</string> diff --git a/packages/SettingsLib/search/Android.bp b/packages/SettingsLib/search/Android.bp index 390c9d2e98de..1f8d1dde15df 100644 --- a/packages/SettingsLib/search/Android.bp +++ b/packages/SettingsLib/search/Android.bp @@ -17,6 +17,9 @@ java_library { android_library { name: "SettingsLib-search", use_resource_processor: true, + defaults: [ + "SettingsLintDefaults", + ], static_libs: [ "SettingsLib-search-interface", ], diff --git a/packages/SettingsLib/src/com/android/settingslib/Utils.java b/packages/SettingsLib/src/com/android/settingslib/Utils.java index c2be571b444a..fb14a172d76c 100644 --- a/packages/SettingsLib/src/com/android/settingslib/Utils.java +++ b/packages/SettingsLib/src/com/android/settingslib/Utils.java @@ -1,6 +1,7 @@ package com.android.settingslib; import static android.app.admin.DevicePolicyResources.Strings.Settings.WORK_PROFILE_USER_LABEL; +import static android.webkit.Flags.updateServiceV2; import android.annotation.ColorInt; import android.app.admin.DevicePolicyManager; @@ -34,6 +35,7 @@ import android.net.vcn.VcnTransportInfo; import android.net.wifi.WifiInfo; import android.os.BatteryManager; import android.os.Build; +import android.os.RemoteException; import android.os.SystemProperties; import android.os.UserHandle; import android.os.UserManager; @@ -44,6 +46,9 @@ import android.telephony.NetworkRegistrationInfo; import android.telephony.ServiceState; import android.telephony.TelephonyManager; import android.util.Log; +import android.webkit.IWebViewUpdateService; +import android.webkit.WebViewFactory; +import android.webkit.WebViewProviderInfo; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -65,6 +70,8 @@ import java.util.List; public class Utils { + private static final String TAG = "Utils"; + @VisibleForTesting static final String STORAGE_MANAGER_ENABLED_PROPERTY = "ro.storage_manager.enabled"; @@ -76,6 +83,7 @@ public class Utils { private static String sPermissionControllerPackageName; private static String sServicesSystemSharedLibPackageName; private static String sSharedSystemSharedLibPackageName; + private static String sDefaultWebViewPackageName; static final int[] WIFI_PIE = { com.android.internal.R.drawable.ic_wifi_signal_0, @@ -445,6 +453,7 @@ public class Utils { || packageName.equals(sServicesSystemSharedLibPackageName) || packageName.equals(sSharedSystemSharedLibPackageName) || packageName.equals(PrintManager.PRINT_SPOOLER_PACKAGE_NAME) + || (updateServiceV2() && packageName.equals(getDefaultWebViewPackageName())) || isDeviceProvisioningPackage(resources, packageName); } @@ -459,6 +468,29 @@ public class Utils { } /** + * Fetch the package name of the default WebView provider. + */ + @Nullable + private static String getDefaultWebViewPackageName() { + if (sDefaultWebViewPackageName != null) { + return sDefaultWebViewPackageName; + } + + try { + IWebViewUpdateService service = WebViewFactory.getUpdateService(); + if (service != null) { + WebViewProviderInfo provider = service.getDefaultWebViewPackage(); + if (provider != null) { + sDefaultWebViewPackageName = provider.packageName; + } + } + } catch (RemoteException e) { + Log.e(TAG, "RemoteException when trying to fetch default WebView package Name", e); + } + return sDefaultWebViewPackageName; + } + + /** * Returns the Wifi icon resource for a given RSSI level. * * @param level The number of bars to show (0-4) diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java index 0ffcc45b6466..f7f06739d4b2 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java @@ -117,6 +117,12 @@ public class BluetoothUtils { } } + if (cachedDevice.isHearingAidDevice()) { + return new Pair<>(getBluetoothDrawable(context, + com.android.internal.R.drawable.ic_bt_hearing_aid), + context.getString(R.string.bluetooth_talkback_hearing_aids)); + } + List<LocalBluetoothProfile> profiles = cachedDevice.getProfiles(); int resId = 0; for (LocalBluetoothProfile profile : profiles) { @@ -125,7 +131,8 @@ public class BluetoothUtils { // The device should show hearing aid icon if it contains any hearing aid related // profiles if (profile instanceof HearingAidProfile || profile instanceof HapClientProfile) { - return new Pair<>(getBluetoothDrawable(context, profileResId), null); + return new Pair<>(getBluetoothDrawable(context, profileResId), + context.getString(R.string.bluetooth_talkback_hearing_aids)); } if (resId == 0) { resId = profileResId; diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java index 245fe6edd601..560bc467ca8e 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java @@ -410,8 +410,13 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> connectDevice(); } - void setHearingAidInfo(HearingAidInfo hearingAidInfo) { + public void setHearingAidInfo(HearingAidInfo hearingAidInfo) { mHearingAidInfo = hearingAidInfo; + dispatchAttributesChanged(); + } + + public HearingAidInfo getHearingAidInfo() { + return mHearingAidInfo; } /** @@ -1788,4 +1793,40 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> boolean getUnpairing() { return mUnpairing; } + + ListenableFuture<Void> syncProfileForMemberDevice() { + return ThreadUtils.getBackgroundExecutor() + .submit( + () -> { + List<Pair<LocalBluetoothProfile, Boolean>> toSync = + Stream.of( + mProfileManager.getA2dpProfile(), + mProfileManager.getHeadsetProfile(), + mProfileManager.getHearingAidProfile(), + mProfileManager.getLeAudioProfile(), + mProfileManager.getLeAudioBroadcastAssistantProfile()) + .filter(Objects::nonNull) + .map(profile -> new Pair<>(profile, profile.isEnabled(mDevice))) + .toList(); + + for (var t : toSync) { + LocalBluetoothProfile profile = t.first; + boolean enabledForMain = t.second; + + for (var member : mMemberDevices) { + BluetoothDevice btDevice = member.getDevice(); + + if (enabledForMain != profile.isEnabled(btDevice)) { + Log.d(TAG, "Syncing profile " + profile + " to " + + enabledForMain + " for member device " + + btDevice.getAnonymizedAddress() + " of main device " + + mDevice.getAnonymizedAddress()); + profile.setEnabled(btDevice, enabledForMain); + } + } + } + return null; + } + ); + } } diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java index a6536a8c47d9..89fe268b3258 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java @@ -349,6 +349,7 @@ public class CachedBluetoothDeviceManager { if (profileId == BluetoothProfile.HEADSET || profileId == BluetoothProfile.A2DP || profileId == BluetoothProfile.LE_AUDIO + || profileId == BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT || profileId == BluetoothProfile.CSIP_SET_COORDINATOR) { return mCsipDeviceManager.onProfileConnectionStateChangedIfProcessed(cachedDevice, state); diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CsipDeviceManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CsipDeviceManager.java index a49314aae1b3..e67ec48d3401 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CsipDeviceManager.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CsipDeviceManager.java @@ -379,6 +379,7 @@ public class CsipDeviceManager { if (hasChanged) { log("addMemberDevicesIntoMainDevice: After changed, CachedBluetoothDevice list: " + mCachedDevices); + preferredMainDevice.syncProfileForMemberDevice(); } return hasChanged; } diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothUtilsTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothUtilsTest.java index 7409eea2cd51..f7ec80b041e9 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothUtilsTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothUtilsTest.java @@ -16,6 +16,7 @@ package com.android.settingslib.bluetooth; import static com.google.common.truth.Truth.assertThat; + import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -87,6 +88,14 @@ public class BluetoothUtilsTest { } @Test + public void getBtClassDrawableWithDescription_typeHearingAid_returnHearingAidDrawable() { + when(mCachedBluetoothDevice.isHearingAidDevice()).thenReturn(true); + BluetoothUtils.getBtClassDrawableWithDescription(mContext, mCachedBluetoothDevice); + + verify(mContext).getDrawable(com.android.internal.R.drawable.ic_bt_hearing_aid); + } + + @Test public void getBtRainbowDrawableWithDescription_normalHeadset_returnAdaptiveIcon() { when(mBluetoothDevice.getMetadata( BluetoothDevice.METADATA_IS_UNTETHERED_HEADSET)).thenReturn("false".getBytes()); diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceTest.java index ed545ab5c81d..9db8b47e9644 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceTest.java @@ -18,7 +18,9 @@ package com.android.settingslib.bluetooth; 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.anyString; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; @@ -56,6 +58,8 @@ import org.robolectric.RuntimeEnvironment; import org.robolectric.annotation.Config; import org.robolectric.shadow.api.Shadow; +import java.util.concurrent.ExecutionException; + @RunWith(RobolectricTestRunner.class) @Config(shadows = {ShadowBluetoothAdapter.class}) public class CachedBluetoothDeviceTest { @@ -1815,6 +1819,52 @@ public class CachedBluetoothDeviceTest { assertThat(mCachedDevice.isConnectedHearingAidDevice()).isFalse(); } + @Test + public void syncProfileForMemberDevice_hasDiff_shouldSync() + throws ExecutionException, InterruptedException { + mCachedDevice.addMemberDevice(mSubCachedDevice); + when(mProfileManager.getA2dpProfile()).thenReturn(mA2dpProfile); + when(mProfileManager.getHearingAidProfile()).thenReturn(mHearingAidProfile); + when(mProfileManager.getLeAudioProfile()).thenReturn(mLeAudioProfile); + + when(mA2dpProfile.isEnabled(mDevice)).thenReturn(true); + when(mHearingAidProfile.isEnabled(mDevice)).thenReturn(true); + when(mLeAudioProfile.isEnabled(mDevice)).thenReturn(true); + + when(mA2dpProfile.isEnabled(mSubDevice)).thenReturn(true); + when(mHearingAidProfile.isEnabled(mSubDevice)).thenReturn(false); + when(mLeAudioProfile.isEnabled(mSubDevice)).thenReturn(false); + + mCachedDevice.syncProfileForMemberDevice().get(); + + verify(mA2dpProfile, never()).setEnabled(any(BluetoothDevice.class), anyBoolean()); + verify(mHearingAidProfile).setEnabled(any(BluetoothDevice.class), eq(true)); + verify(mLeAudioProfile).setEnabled(any(BluetoothDevice.class), eq(true)); + } + + @Test + public void syncProfileForMemberDevice_noDiff_shouldNotSync() + throws ExecutionException, InterruptedException { + mCachedDevice.addMemberDevice(mSubCachedDevice); + when(mProfileManager.getA2dpProfile()).thenReturn(mA2dpProfile); + when(mProfileManager.getHearingAidProfile()).thenReturn(mHearingAidProfile); + when(mProfileManager.getLeAudioProfile()).thenReturn(mLeAudioProfile); + + when(mA2dpProfile.isEnabled(mDevice)).thenReturn(false); + when(mHearingAidProfile.isEnabled(mDevice)).thenReturn(false); + when(mLeAudioProfile.isEnabled(mDevice)).thenReturn(true); + + when(mA2dpProfile.isEnabled(mSubDevice)).thenReturn(false); + when(mHearingAidProfile.isEnabled(mSubDevice)).thenReturn(false); + when(mLeAudioProfile.isEnabled(mSubDevice)).thenReturn(true); + + mCachedDevice.syncProfileForMemberDevice().get(); + + verify(mA2dpProfile, never()).setEnabled(any(BluetoothDevice.class), anyBoolean()); + verify(mHearingAidProfile, never()).setEnabled(any(BluetoothDevice.class), anyBoolean()); + verify(mLeAudioProfile, never()).setEnabled(any(BluetoothDevice.class), anyBoolean()); + } + private HearingAidInfo getLeftAshaHearingAidInfo() { return new HearingAidInfo.Builder() .setAshaDeviceSide(HearingAidProfile.DeviceSide.SIDE_LEFT) diff --git a/packages/SettingsProvider/TEST_MAPPING b/packages/SettingsProvider/TEST_MAPPING index 890510ffebe3..0eed2b7490d4 100644 --- a/packages/SettingsProvider/TEST_MAPPING +++ b/packages/SettingsProvider/TEST_MAPPING @@ -11,5 +11,10 @@ } ] } + ], + "postsubmit": [ + { + "name": "CtsDeviceConfigTestCases" + } ] } diff --git a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java index 5004f251cfd3..8ae50eb7ffad 100644 --- a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java +++ b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java @@ -241,6 +241,8 @@ public class SecureSettings { Settings.Secure.BLUETOOTH_LE_BROADCAST_PROGRAM_INFO, Settings.Secure.BLUETOOTH_LE_BROADCAST_CODE, Settings.Secure.BLUETOOTH_LE_BROADCAST_APP_SOURCE_NAME, + Settings.Secure.BLUETOOTH_LE_BROADCAST_IMPROVE_COMPATIBILITY, + Settings.Secure.BLUETOOTH_LE_BROADCAST_FALLBACK_ACTIVE_DEVICE_ADDRESS, Settings.Secure.CUSTOM_BUGREPORT_HANDLER_APP, Settings.Secure.CUSTOM_BUGREPORT_HANDLER_USER, Settings.Secure.LOCK_SCREEN_WEATHER_ENABLED, diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java index 0b0e182746e5..285c8c969343 100644 --- a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java +++ b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java @@ -387,6 +387,11 @@ public class SecureSettingsValidators { VALIDATORS.put(Secure.BLUETOOTH_LE_BROADCAST_PROGRAM_INFO, ANY_STRING_VALIDATOR); VALIDATORS.put(Secure.BLUETOOTH_LE_BROADCAST_CODE, ANY_STRING_VALIDATOR); VALIDATORS.put(Secure.BLUETOOTH_LE_BROADCAST_APP_SOURCE_NAME, ANY_STRING_VALIDATOR); + VALIDATORS.put( + Secure.BLUETOOTH_LE_BROADCAST_IMPROVE_COMPATIBILITY, + new DiscreteValueValidator(new String[] {"0", "1"})); + VALIDATORS.put( + Secure.BLUETOOTH_LE_BROADCAST_FALLBACK_ACTIVE_DEVICE_ADDRESS, ANY_STRING_VALIDATOR); VALIDATORS.put(Secure.CUSTOM_BUGREPORT_HANDLER_APP, ANY_STRING_VALIDATOR); VALIDATORS.put(Secure.CUSTOM_BUGREPORT_HANDLER_USER, ANY_INTEGER_VALIDATOR); VALIDATORS.put(Secure.LOCK_SCREEN_WEATHER_ENABLED, BOOLEAN_VALIDATOR); diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java index b0abf92ffe08..2d442f4c0e6e 100644 --- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java +++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java @@ -1349,6 +1349,26 @@ public class SettingsProvider extends ContentProvider { final int nameCount = names.size(); HashMap<String, String> flagsToValues = new HashMap<>(names.size()); + if (Flags.loadAconfigDefaults()) { + Map<String, Map<String, String>> allDefaults = + settingsState.getAconfigDefaultValues(); + + if (allDefaults != null) { + if (prefix != null) { + String namespace = prefix.substring(0, prefix.length() - 1); + + Map<String, String> namespaceDefaults = allDefaults.get(namespace); + if (namespaceDefaults != null) { + flagsToValues.putAll(namespaceDefaults); + } + } else { + for (Map<String, String> namespaceDefaults : allDefaults.values()) { + flagsToValues.putAll(namespaceDefaults); + } + } + } + } + for (int i = 0; i < nameCount; i++) { String name = names.get(i); Setting setting = settingsState.getSettingLocked(name); diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java index 73c2e22240d3..6f3c88fc8706 100644 --- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java +++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java @@ -69,6 +69,7 @@ import java.io.PrintWriter; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; +import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; @@ -236,6 +237,10 @@ final class SettingsState { @GuardedBy("mLock") private int mNextHistoricalOpIdx; + @GuardedBy("mLock") + @Nullable + private Map<String, Map<String, String>> mNamespaceDefaults; + public static final int SETTINGS_TYPE_GLOBAL = 0; public static final int SETTINGS_TYPE_SYSTEM = 1; public static final int SETTINGS_TYPE_SECURE = 2; @@ -331,25 +336,21 @@ final class SettingsState { readStateSyncLocked(); if (Flags.loadAconfigDefaults()) { - // Only load aconfig defaults if this is the first boot, the XML - // file doesn't exist yet, or this device is on its first boot after - // an OTA. - boolean shouldLoadAconfigValues = isConfigSettingsKey(mKey) - && (!file.exists() - || mContext.getPackageManager().isDeviceUpgrading()); - if (shouldLoadAconfigValues) { + if (isConfigSettingsKey(mKey)) { loadAconfigDefaultValuesLocked(); } } + } } @GuardedBy("mLock") private void loadAconfigDefaultValuesLocked() { + mNamespaceDefaults = new HashMap<>(); + for (String fileName : sAconfigTextProtoFilesOnDevice) { try (FileInputStream inputStream = new FileInputStream(fileName)) { - byte[] contents = inputStream.readAllBytes(); - loadAconfigDefaultValues(contents); + loadAconfigDefaultValues(inputStream.readAllBytes(), mNamespaceDefaults); } catch (IOException e) { Slog.e(LOG_TAG, "failed to read protobuf", e); } @@ -358,27 +359,21 @@ final class SettingsState { @VisibleForTesting @GuardedBy("mLock") - public void loadAconfigDefaultValues(byte[] fileContents) { + public static void loadAconfigDefaultValues(byte[] fileContents, + @NonNull Map<String, Map<String, String>> defaultMap) { try { - parsed_flags parsedFlags = parsed_flags.parseFrom(fileContents); - - if (parsedFlags == null) { - Slog.e(LOG_TAG, "failed to parse aconfig protobuf"); - return; - } - + parsed_flags parsedFlags = + parsed_flags.parseFrom(fileContents); for (parsed_flag flag : parsedFlags.getParsedFlagList()) { - String flagName = flag.getNamespace() + "/" - + flag.getPackage() + "." + flag.getName(); - String value = flag.getState() == flag_state.ENABLED ? "true" : "false"; - - Setting existingSetting = getSettingLocked(flagName); - boolean isDefaultLoaded = existingSetting.getTag() != null - && existingSetting.getTag().equals(BOOT_LOADED_DEFAULT_TAG); - if (existingSetting.getValue() == null || isDefaultLoaded) { - insertSettingLocked(flagName, value, BOOT_LOADED_DEFAULT_TAG, - false, flag.getPackage()); + if (!defaultMap.containsKey(flag.getNamespace())) { + Map<String, String> defaults = new HashMap<>(); + defaultMap.put(flag.getNamespace(), defaults); } + String flagName = flag.getNamespace() + + "/" + flag.getPackage() + "." + flag.getName(); + String flagValue = flag.getState() == flag_state.ENABLED + ? "true" : "false"; + defaultMap.get(flag.getNamespace()).put(flagName, flagValue); } } catch (IOException e) { Slog.e(LOG_TAG, "failed to parse protobuf", e); @@ -443,6 +438,13 @@ final class SettingsState { return names; } + @Nullable + public Map<String, Map<String, String>> getAconfigDefaultValues() { + synchronized (mLock) { + return mNamespaceDefaults; + } + } + // The settings provider must hold its lock when calling here. public Setting getSettingLocked(String name) { if (TextUtils.isEmpty(name)) { diff --git a/packages/SettingsProvider/src/com/android/providers/settings/device_config_service.aconfig b/packages/SettingsProvider/src/com/android/providers/settings/device_config_service.aconfig index ecac5ee18582..edbc0b391b27 100644 --- a/packages/SettingsProvider/src/com/android/providers/settings/device_config_service.aconfig +++ b/packages/SettingsProvider/src/com/android/providers/settings/device_config_service.aconfig @@ -14,3 +14,11 @@ flag { bug: "311155098" is_fixed_read_only: true } + +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 +}
\ No newline at end of file diff --git a/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsStateTest.java b/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsStateTest.java index 24625eaa5e13..e55bbecb67d7 100644 --- a/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsStateTest.java +++ b/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsStateTest.java @@ -30,6 +30,8 @@ import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileOutputStream; import java.io.PrintStream; +import java.util.HashMap; +import java.util.Map; public class SettingsStateTest extends AndroidTestCase { public static final String CRAZY_STRING = @@ -93,7 +95,6 @@ public class SettingsStateTest extends AndroidTestCase { SettingsState settingsState = new SettingsState( getContext(), lock, mSettingsFile, configKey, SettingsState.MAX_BYTES_PER_APP_PACKAGE_UNLIMITED, Looper.getMainLooper()); - parsed_flags flags = parsed_flags .newBuilder() .addParsedFlag(parsed_flag @@ -117,18 +118,13 @@ public class SettingsStateTest extends AndroidTestCase { .build(); synchronized (lock) { - settingsState.loadAconfigDefaultValues(flags.toByteArray()); - settingsState.persistSettingsLocked(); - } - settingsState.waitForHandler(); + Map<String, Map<String, String>> defaults = new HashMap<>(); + settingsState.loadAconfigDefaultValues(flags.toByteArray(), defaults); + Map<String, String> namespaceDefaults = defaults.get("test_namespace"); + assertEquals(2, namespaceDefaults.keySet().size()); - synchronized (lock) { - assertEquals("false", - settingsState.getSettingLocked( - "test_namespace/com.android.flags.flag1").getValue()); - assertEquals("true", - settingsState.getSettingLocked( - "test_namespace/com.android.flags.flag2").getValue()); + assertEquals("false", namespaceDefaults.get("test_namespace/com.android.flags.flag1")); + assertEquals("true", namespaceDefaults.get("test_namespace/com.android.flags.flag2")); } } @@ -150,21 +146,18 @@ public class SettingsStateTest extends AndroidTestCase { .build(); synchronized (lock) { - settingsState.loadAconfigDefaultValues(flags.toByteArray()); - settingsState.persistSettingsLocked(); - } - settingsState.waitForHandler(); + Map<String, Map<String, String>> defaults = new HashMap<>(); + settingsState.loadAconfigDefaultValues(flags.toByteArray(), defaults); - synchronized (lock) { - assertEquals(null, - settingsState.getSettingLocked( - "test_namespace/com.android.flags.flag1").getValue()); + Map<String, String> namespaceDefaults = defaults.get("test_namespace"); + assertEquals(null, namespaceDefaults); } } public void testInvalidAconfigProtoDoesNotCrash() { + Map<String, Map<String, String>> defaults = new HashMap<>(); SettingsState settingsState = getSettingStateObject(); - settingsState.loadAconfigDefaultValues("invalid protobuf".getBytes()); + settingsState.loadAconfigDefaultValues("invalid protobuf".getBytes(), defaults); } public void testIsBinary() { diff --git a/packages/Shell/AndroidManifest.xml b/packages/Shell/AndroidManifest.xml index 507d9c467d68..3dfc4540d6e7 100644 --- a/packages/Shell/AndroidManifest.xml +++ b/packages/Shell/AndroidManifest.xml @@ -559,6 +559,9 @@ <!-- Permission required for CTS test - android.server.biometrics --> <uses-permission android:name="android.permission.TEST_BIOMETRIC" /> + <!-- Permission required for CTS test - android.server.biometrics --> + <uses-permission android:name="android.permission.MANAGE_BIOMETRIC_DIALOG" /> + <!-- Permissions required for CTS test - NotificationManagerTest --> <uses-permission android:name="android.permission.MANAGE_NOTIFICATION_LISTENERS" /> @@ -891,6 +894,9 @@ <!-- Permission required for Cts test - CtsNotificationTestCases --> <uses-permission android:name="android.permission.RECEIVE_SENSITIVE_NOTIFICATIONS" /> + <!-- Permission required for BinaryTransparencyService shell API and host test --> + <uses-permission android:name="android.permission.GET_BACKGROUND_INSTALLED_PACKAGES" /> + <application android:label="@string/app_label" android:theme="@android:style/Theme.DeviceDefault.DayNight" diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml index 7443e4ccf79e..168e6e003dc6 100644 --- a/packages/SystemUI/AndroidManifest.xml +++ b/packages/SystemUI/AndroidManifest.xml @@ -355,6 +355,8 @@ <uses-permission android:name="android.permission.MONITOR_KEYBOARD_BACKLIGHT" /> + <uses-permission android:name="android.permission.MONITOR_STICKY_MODIFIER_STATE" /> + <!-- Listen to (dis-)connection of external displays and enable / disable them. --> <uses-permission android:name="android.permission.MANAGE_DISPLAYS" /> diff --git a/packages/SystemUI/accessibility/accessibilitymenu/Android.bp b/packages/SystemUI/accessibility/accessibilitymenu/Android.bp index 6c75b4346739..0df9bac295fb 100644 --- a/packages/SystemUI/accessibility/accessibilitymenu/Android.bp +++ b/packages/SystemUI/accessibility/accessibilitymenu/Android.bp @@ -37,6 +37,7 @@ android_app { "androidx.preference_preference", "androidx.viewpager_viewpager", "SettingsLibDisplayUtils", + "SettingsLibSettingsTheme", "com_android_a11y_menu_flags_lib", ], diff --git a/packages/SystemUI/accessibility/accessibilitymenu/AndroidManifest.xml b/packages/SystemUI/accessibility/accessibilitymenu/AndroidManifest.xml index ca8426560ccc..648cc3b035c5 100644 --- a/packages/SystemUI/accessibility/accessibilitymenu/AndroidManifest.xml +++ b/packages/SystemUI/accessibility/accessibilitymenu/AndroidManifest.xml @@ -40,7 +40,7 @@ android:exported="true" android:label="@string/accessibility_menu_settings_name" android:launchMode="singleTop" - android:theme="@style/AccessibilityMenuSettings"> + android:theme="@style/Theme.SettingsBase"> <intent-filter> <action android:name="android.intent.action.MAIN"/> diff --git a/packages/SystemUI/accessibility/accessibilitymenu/aconfig/accessibility.aconfig b/packages/SystemUI/accessibility/accessibilitymenu/aconfig/accessibility.aconfig index eadcd7c27a18..f5db6a4c4573 100644 --- a/packages/SystemUI/accessibility/accessibilitymenu/aconfig/accessibility.aconfig +++ b/packages/SystemUI/accessibility/accessibilitymenu/aconfig/accessibility.aconfig @@ -8,10 +8,3 @@ flag { description: "Hides the AccessibilityMenuService UI before taking action instead of after." bug: "292020123" } - -flag { - name: "a11y_menu_settings_back_button_fix_and_large_button_sizing" - namespace: "accessibility" - description: "Provides/restores back button functionality for the a11yMenu settings page. Also, fixes sizing problems with large shortcut buttons." - bug: "298467628" -} diff --git a/packages/SystemUI/accessibility/accessibilitymenu/res/values-night/styles.xml b/packages/SystemUI/accessibility/accessibilitymenu/res/values-night/styles.xml index 1f5765465075..81b3152375ff 100644 --- a/packages/SystemUI/accessibility/accessibilitymenu/res/values-night/styles.xml +++ b/packages/SystemUI/accessibility/accessibilitymenu/res/values-night/styles.xml @@ -16,10 +16,6 @@ --> <resources> - <style name="AccessibilityMenuSettings" parent="android:Theme.DeviceDefault.DayNight"> - <item name="android:windowLightStatusBar">false</item> - </style> - <!--Adds the theme to support SnackBar component and user configurable theme. --> <style name="ServiceTheme" parent="android:Theme.DeviceDefault.DayNight"> <item name="android:colorControlNormal">@color/colorControlNormal</item> diff --git a/packages/SystemUI/accessibility/accessibilitymenu/res/values/styles.xml b/packages/SystemUI/accessibility/accessibilitymenu/res/values/styles.xml index a2508cdf4f16..41691552f714 100644 --- a/packages/SystemUI/accessibility/accessibilitymenu/res/values/styles.xml +++ b/packages/SystemUI/accessibility/accessibilitymenu/res/values/styles.xml @@ -16,11 +16,6 @@ --> <resources> - <!--The theme is for preference CollapsingToolbarBaseActivity settings--> - <style name="AccessibilityMenuSettings" parent="android:Theme.DeviceDefault.DayNight"> - <item name="android:windowLightStatusBar">true</item> - </style> - <!--Adds the theme to support SnackBar component and user configurable theme. --> <style name="ServiceTheme" parent="android:Theme.DeviceDefault.Light"> <item name="android:colorControlNormal">@color/colorControlNormal</item> diff --git a/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/activity/A11yMenuSettingsActivity.java b/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/activity/A11yMenuSettingsActivity.java index bf51e23855d5..ab8f97ad9c3d 100644 --- a/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/activity/A11yMenuSettingsActivity.java +++ b/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/activity/A11yMenuSettingsActivity.java @@ -35,7 +35,6 @@ import androidx.preference.Preference; import androidx.preference.PreferenceFragmentCompat; import androidx.preference.PreferenceManager; -import com.android.systemui.accessibility.accessibilitymenu.Flags; import com.android.systemui.accessibility.accessibilitymenu.R; /** @@ -56,28 +55,17 @@ public class A11yMenuSettingsActivity extends FragmentActivity { ActionBar actionBar = getActionBar(); actionBar.setDisplayShowCustomEnabled(true); - - if (Flags.a11yMenuSettingsBackButtonFixAndLargeButtonSizing()) { - actionBar.setDisplayHomeAsUpEnabled(true); - } + actionBar.setDisplayHomeAsUpEnabled(true); actionBar.setCustomView(R.layout.preferences_action_bar); ((TextView) findViewById(R.id.action_bar_title)).setText( getResources().getString(R.string.accessibility_menu_settings_name) ); - actionBar.setDisplayOptions( - ActionBar.DISPLAY_TITLE_MULTIPLE_LINES - | ActionBar.DISPLAY_SHOW_TITLE - | ActionBar.DISPLAY_HOME_AS_UP); } @Override public boolean onNavigateUp() { - if (Flags.a11yMenuSettingsBackButtonFixAndLargeButtonSizing()) { - mCallback.onBackInvoked(); - return true; - } else { - return false; - } + mCallback.onBackInvoked(); + return true; } /** diff --git a/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/view/A11yMenuAdapter.java b/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/view/A11yMenuAdapter.java index c4f372cbce51..c2cf6e104a6a 100644 --- a/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/view/A11yMenuAdapter.java +++ b/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/view/A11yMenuAdapter.java @@ -28,7 +28,6 @@ import android.widget.ImageButton; import android.widget.TextView; import com.android.systemui.accessibility.accessibilitymenu.AccessibilityMenuService; -import com.android.systemui.accessibility.accessibilitymenu.Flags; import com.android.systemui.accessibility.accessibilitymenu.R; import com.android.systemui.accessibility.accessibilitymenu.activity.A11yMenuSettingsActivity.A11yMenuPreferenceFragment; import com.android.systemui.accessibility.accessibilitymenu.model.A11yMenuShortcut; @@ -81,10 +80,8 @@ public class A11yMenuAdapter extends BaseAdapter { if (convertView == null) { convertView = mInflater.inflate(R.layout.grid_item, parent, false); - if (Flags.a11yMenuSettingsBackButtonFixAndLargeButtonSizing()) { - configureShortcutSize(convertView, - A11yMenuPreferenceFragment.isLargeButtonsEnabled(mService)); - } + configureShortcutSize(convertView, + A11yMenuPreferenceFragment.isLargeButtonsEnabled(mService)); } A11yMenuShortcut shortcutItem = (A11yMenuShortcut) getItem(position); @@ -147,15 +144,6 @@ public class A11yMenuAdapter extends BaseAdapter { ImageButton shortcutIconButton = convertView.findViewById(R.id.shortcutIconBtn); TextView shortcutLabel = convertView.findViewById(R.id.shortcutLabel); - if (!Flags.a11yMenuSettingsBackButtonFixAndLargeButtonSizing()) { - if (A11yMenuPreferenceFragment.isLargeButtonsEnabled(mService)) { - ViewGroup.LayoutParams params = shortcutIconButton.getLayoutParams(); - params.width = (int) (params.width * LARGE_BUTTON_SCALE); - params.height = (int) (params.height * LARGE_BUTTON_SCALE); - shortcutLabel.setTextSize(android.util.TypedValue.COMPLEX_UNIT_PX, mLargeTextSize); - } - } - if (shortcutItem.getId() == A11yMenuShortcut.ShortcutId.UNSPECIFIED_ID_VALUE.ordinal()) { // Sets empty shortcut icon and label when the shortcut is ADD_ITEM. shortcutIconButton.setImageResource(android.R.color.transparent); diff --git a/packages/SystemUI/aconfig/accessibility.aconfig b/packages/SystemUI/aconfig/accessibility.aconfig index f7b1a26c9df9..7ba889bc8fee 100644 --- a/packages/SystemUI/aconfig/accessibility.aconfig +++ b/packages/SystemUI/aconfig/accessibility.aconfig @@ -36,3 +36,10 @@ flag { description: "Animates the floating menu's transition between curved and jagged edges." bug: "281140482" } + +flag { + name: "create_windowless_window_magnifier" + namespace: "accessibility" + description: "Uses SurfaceControlViewHost to create the magnifier for window magnification." + bug: "280992417" +} diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig index c23a49c68363..323613077a70 100644 --- a/packages/SystemUI/aconfig/systemui.aconfig +++ b/packages/SystemUI/aconfig/systemui.aconfig @@ -15,6 +15,13 @@ flag { } flag { + name: "notification_async_group_header_inflation" + namespace: "systemui" + description: "Inflates the notification group summary header views from the background thread." + bug: "217799515" +} + +flag { name: "notification_async_hybrid_view_inflation" namespace: "systemui" description: "Inflates hybrid (single-line) notification views from the background thread." @@ -343,3 +350,10 @@ flag { description: "Relocate Smartspace to bottom of the Lock Screen" bug: "316212788" } + +flag { + name: "pin_input_field_styled_focus_state" + namespace: "systemui" + description: "Enables styled focus states on pin input field if keyboard is connected" + bug: "316106516" +} 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 d76f0ff3ec18..a390305b144e 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 @@ -20,6 +20,7 @@ import android.appwidget.AppWidgetHostView import android.os.Bundle import android.util.SizeF import android.widget.FrameLayout +import androidx.compose.animation.core.animateFloatAsState import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background @@ -38,6 +39,7 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.GridItemSpan +import androidx.compose.foundation.lazy.grid.LazyGridState import androidx.compose.foundation.lazy.grid.LazyHorizontalGrid import androidx.compose.foundation.lazy.grid.rememberLazyGridState import androidx.compose.foundation.shape.RoundedCornerShape @@ -58,7 +60,9 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.State import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -67,8 +71,10 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.layout.LayoutCoordinates +import androidx.compose.ui.layout.boundsInWindow import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.layout.positionInWindow @@ -83,13 +89,16 @@ import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidView import androidx.compose.ui.window.Popup +import androidx.core.view.setPadding 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.firstItemAtOffset +import com.android.systemui.communal.ui.compose.extensions.observeTapsWithoutConsuming import com.android.systemui.communal.ui.viewmodel.BaseCommunalViewModel import com.android.systemui.communal.ui.viewmodel.CommunalEditModeViewModel -import com.android.systemui.media.controls.ui.MediaHierarchyManager -import com.android.systemui.media.controls.ui.MediaHostState import com.android.systemui.res.R @Composable @@ -106,22 +115,59 @@ fun CommunalHub( var toolbarSize: IntSize? by remember { mutableStateOf(null) } var gridCoordinates: LayoutCoordinates? by remember { mutableStateOf(null) } var isDraggingToRemove by remember { mutableStateOf(false) } + val gridState = rememberLazyGridState() + val contentListState = rememberContentListState(communalContent, viewModel) + val reorderingWidgets by viewModel.reorderingWidgets.collectAsState() + val selectedIndex = viewModel.selectedIndex.collectAsState() + val removeButtonEnabled by remember { + derivedStateOf { selectedIndex.value != null || reorderingWidgets } + } + + val contentPadding = gridContentPadding(viewModel.isEditMode, toolbarSize) + val contentOffset = beforeContentPadding(contentPadding).toOffset() Box( modifier = - modifier.fillMaxSize().background(LocalAndroidColorScheme.current.outlineVariant), + modifier + .fillMaxSize() + .background(LocalAndroidColorScheme.current.outlineVariant) + .pointerInput(gridState, contentOffset, contentListState) { + // If not in edit mode, don't allow selecting items. + if (!viewModel.isEditMode) return@pointerInput + observeTapsWithoutConsuming { offset -> + val adjustedOffset = offset - contentOffset + val index = + gridState.layoutInfo.visibleItemsInfo + .firstItemAtOffset(adjustedOffset) + ?.index + val newIndex = + if (index?.let(contentListState::isItemEditable) == true) { + index + } else { + null + } + viewModel.setSelectedIndex(newIndex) + } + }, ) { CommunalHubLazyGrid( communalContent = communalContent, viewModel = viewModel, - contentPadding = gridContentPadding(viewModel.isEditMode, toolbarSize), + contentPadding = contentPadding, + contentOffset = contentOffset, setGridCoordinates = { gridCoordinates = it }, - updateDragPositionForRemove = { + updateDragPositionForRemove = { offset -> isDraggingToRemove = - checkForDraggingToRemove(it, removeButtonCoordinates, gridCoordinates) + isPointerWithinCoordinates( + offset = gridCoordinates?.let { it.positionInWindow() + offset }, + containerToCheck = removeButtonCoordinates + ) isDraggingToRemove }, onOpenWidgetPicker = onOpenWidgetPicker, + gridState = gridState, + contentListState = contentListState, + selectedIndex = selectedIndex ) if (viewModel.isEditMode && onOpenWidgetPicker != null && onEditDone != null) { @@ -131,6 +177,14 @@ fun CommunalHub( setRemoveButtonCoordinates = { removeButtonCoordinates = it }, onEditDone = onEditDone, onOpenWidgetPicker = onOpenWidgetPicker, + onRemoveClicked = { + selectedIndex.value?.let { index -> + contentListState.onRemove(index) + contentListState.onSaveList() + viewModel.setSelectedIndex(null) + } + }, + removeEnabled = removeButtonEnabled ) } else { IconButton(onClick = viewModel::onOpenWidgetEditor) { @@ -160,16 +214,18 @@ private fun BoxScope.CommunalHubLazyGrid( communalContent: List<CommunalContentModel>, viewModel: BaseCommunalViewModel, contentPadding: PaddingValues, + selectedIndex: State<Int?>, + contentOffset: Offset, + gridState: LazyGridState, + contentListState: ContentListState, setGridCoordinates: (coordinates: LayoutCoordinates) -> Unit, updateDragPositionForRemove: (offset: Offset) -> Boolean, onOpenWidgetPicker: (() -> Unit)? = null, ) { var gridModifier = Modifier.align(Alignment.CenterStart) - val gridState = rememberLazyGridState() var list = communalContent var dragDropState: GridDragDropState? = null if (viewModel.isEditMode && viewModel is CommunalEditModeViewModel) { - val contentListState = rememberContentListState(list, viewModel) list = contentListState.list // for drag & drop operations within the communal hub grid dragDropState = @@ -181,7 +237,7 @@ private fun BoxScope.CommunalHubLazyGrid( gridModifier = gridModifier .fillMaxSize() - .dragContainer(dragDropState, beforeContentPadding(contentPadding), viewModel) + .dragContainer(dragDropState, contentOffset, viewModel) .onGloballyPositioned { setGridCoordinates(it) } // for widgets dropped from other activities val dragAndDropTargetState = @@ -220,8 +276,10 @@ private fun BoxScope.CommunalHubLazyGrid( list[index].size.dp().value, ) if (viewModel.isEditMode && dragDropState != null) { + val selected by remember(index) { derivedStateOf { index == selectedIndex.value } } DraggableItem( dragDropState = dragDropState, + selected = selected, enabled = list[index] is CommunalContentModel.Widget, index = index, size = size @@ -255,11 +313,19 @@ private fun BoxScope.CommunalHubLazyGrid( @Composable private fun Toolbar( isDraggingToRemove: Boolean, + removeEnabled: Boolean, + onRemoveClicked: () -> Unit, setToolbarSize: (toolbarSize: IntSize) -> Unit, setRemoveButtonCoordinates: (coordinates: LayoutCoordinates) -> Unit, onOpenWidgetPicker: () -> Unit, - onEditDone: () -> Unit, + onEditDone: () -> Unit ) { + val removeButtonAlpha: Float by + animateFloatAsState( + targetValue = if (removeEnabled) 1f else 0.5f, + label = "RemoveButtonAlphaAnimation" + ) + Row( modifier = Modifier.fillMaxWidth() @@ -303,13 +369,18 @@ private fun Toolbar( } } else { OutlinedButton( - // Button is disabled to make it non-clickable - enabled = false, - onClick = {}, - colors = ButtonDefaults.outlinedButtonColors(disabledContentColor = colors.primary), + enabled = removeEnabled, + onClick = onRemoveClicked, + colors = + ButtonDefaults.outlinedButtonColors( + contentColor = colors.primary, + disabledContentColor = colors.primary + ), border = BorderStroke(width = 1.0.dp, color = colors.primary), contentPadding = Dimensions.ButtonPadding, - modifier = Modifier.onGloballyPositioned { setRemoveButtonCoordinates(it) } + modifier = + Modifier.graphicsLayer { alpha = removeButtonAlpha } + .onGloballyPositioned { setRemoveButtonCoordinates(it) } ) { RemoveButtonContent(spacerModifier) } @@ -387,7 +458,7 @@ private fun CommunalContent( ) { when (model) { is CommunalContentModel.Widget -> WidgetContent(viewModel, model, size, modifier) - is CommunalContentModel.WidgetPlaceholder -> WidgetPlaceholderContent(size) + is CommunalContentModel.WidgetPlaceholder -> HighlightedItem(size) is CommunalContentModel.CtaTileInViewMode -> CtaTileInViewModeContent(viewModel, size, modifier) is CommunalContentModel.CtaTileInEditMode -> @@ -398,13 +469,13 @@ private fun CommunalContent( } } -/** Presents a placeholder card for the new widget being dragged and dropping into the grid. */ +/** Creates an empty card used to highlight a particular spot on the grid. */ @Composable -fun WidgetPlaceholderContent(size: SizeF) { +fun HighlightedItem(size: SizeF, modifier: Modifier = Modifier) { Card( - modifier = Modifier.size(Dp(size.width), Dp(size.height)), + modifier = modifier.size(Dp(size.width), Dp(size.height)), colors = CardDefaults.cardColors(containerColor = Color.Transparent), - border = BorderStroke(3.dp, LocalAndroidColorScheme.current.tertiaryFixed), + border = BorderStroke(CardOutlineWidth, LocalAndroidColorScheme.current.tertiaryFixed), shape = RoundedCornerShape(16.dp) ) {} } @@ -418,7 +489,7 @@ private fun CtaTileInViewModeContent( ) { val colors = LocalAndroidColorScheme.current Card( - modifier = modifier.height(size.height.dp), + modifier = modifier.height(size.height.dp).padding(CardOutlineWidth), colors = CardDefaults.cardColors( containerColor = colors.primary, @@ -490,7 +561,7 @@ private fun CtaTileInEditModeContent( } val colors = LocalAndroidColorScheme.current Card( - modifier = modifier.height(size.height.dp), + modifier = modifier.height(size.height.dp).padding(CardOutlineWidth), colors = CardDefaults.cardColors(containerColor = Color.Transparent), border = BorderStroke(1.dp, colors.primary), shape = RoundedCornerShape(200.dp), @@ -529,8 +600,9 @@ private fun WidgetContent( modifier = modifier.height(size.height.dp), contentAlignment = Alignment.Center, ) { + val paddingInPx = with(LocalDensity.current) { CardOutlineWidth.toPx().toInt() } AndroidView( - modifier = modifier, + modifier = modifier.allowGestures(allowed = !viewModel.isEditMode), factory = { context -> // The AppWidgetHostView will inherit the interaction handler from the // AppWidgetHost. So set the interaction handler here before creating the view, and @@ -540,9 +612,13 @@ private fun WidgetContent( model.appWidgetHost.setInteractionHandler(viewModel.getInteractionHandler()) val view = model.appWidgetHost - .createView(context, model.appWidgetId, model.providerInfo) + .createViewForCommunal(context, model.appWidgetId, model.providerInfo) .apply { updateAppWidgetSize(Bundle.EMPTY, listOf(size)) } model.appWidgetHost.setInteractionHandler(null) + // Remove the extra padding applied to AppWidgetHostView to allow widgets to + // occupy the entire box. The added padding is now adjusted to leave only sufficient + // space for displaying the outline around the box when the widget is selected. + view.setPadding(paddingInPx) view }, // For reusing composition in lazy lists. @@ -576,10 +652,6 @@ private fun Umo(viewModel: BaseCommunalViewModel, modifier: Modifier = Modifier) AndroidView( modifier = modifier, factory = { - viewModel.mediaHost.expansion = MediaHostState.EXPANDED - viewModel.mediaHost.showsOnlyActiveMedia = false - viewModel.mediaHost.falsingProtectionNeeded = false - viewModel.mediaHost.init(MediaHierarchyManager.LOCATION_COMMUNAL_HUB) viewModel.mediaHost.hostView.layoutParams = FrameLayout.LayoutParams( FrameLayout.LayoutParams.MATCH_PARENT, @@ -622,8 +694,8 @@ private fun gridContentPadding(isEditMode: Boolean, toolbarSize: IntSize?): Padd private fun beforeContentPadding(paddingValues: PaddingValues): ContentPaddingInPx { return with(LocalDensity.current) { ContentPaddingInPx( - startPadding = paddingValues.calculateLeftPadding(LayoutDirection.Ltr).toPx(), - topPadding = paddingValues.calculateTopPadding().toPx() + start = paddingValues.calculateLeftPadding(LayoutDirection.Ltr).toPx(), + top = paddingValues.calculateTopPadding().toPx() ) } } @@ -632,18 +704,15 @@ private fun beforeContentPadding(paddingValues: PaddingValues): ContentPaddingIn * Check whether the pointer position that the item is being dragged at is within the coordinates of * the remove button in the toolbar. Returns true if the item is removable. */ -private fun checkForDraggingToRemove( - offset: Offset, - removeButtonCoordinates: LayoutCoordinates?, - gridCoordinates: LayoutCoordinates?, +private fun isPointerWithinCoordinates( + offset: Offset?, + containerToCheck: LayoutCoordinates? ): Boolean { - if (removeButtonCoordinates == null || gridCoordinates == null) { + if (offset == null || containerToCheck == null) { return false } - val pointer = gridCoordinates.positionInWindow() + offset - val removeButton = removeButtonCoordinates.positionInWindow() - return pointer.x in removeButton.x..removeButton.x + removeButtonCoordinates.size.width && - pointer.y in removeButton.y..removeButton.y + removeButtonCoordinates.size.height + val container = containerToCheck.boundsInWindow() + return container.contains(offset) } private fun CommunalContentSize.dp(): Dp { @@ -654,13 +723,16 @@ private fun CommunalContentSize.dp(): Dp { } } -data class ContentPaddingInPx(val startPadding: Float, val topPadding: Float) +data class ContentPaddingInPx(val start: Float, val top: Float) { + fun toOffset(): Offset = Offset(start, top) +} object Dimensions { val CardWidth = 464.dp val CardHeightFull = 630.dp val CardHeightHalf = 307.dp val CardHeightThird = 199.dp + val CardOutlineWidth = 3.dp val GridHeight = CardHeightFull val Spacing = 16.dp diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/ContentListState.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/ContentListState.kt index 979991d7dc2a..45f98b879dd7 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/ContentListState.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/ContentListState.kt @@ -21,12 +21,12 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.runtime.toMutableStateList import com.android.systemui.communal.domain.model.CommunalContentModel -import com.android.systemui.communal.ui.viewmodel.CommunalEditModeViewModel +import com.android.systemui.communal.ui.viewmodel.BaseCommunalViewModel @Composable fun rememberContentListState( communalContent: List<CommunalContentModel>, - viewModel: CommunalEditModeViewModel, + viewModel: BaseCommunalViewModel, ): ContentListState { return remember(communalContent) { ContentListState( diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/GridDragDropState.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/GridDragDropState.kt index 113822167ca7..a1959532fbb9 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/GridDragDropState.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/GridDragDropState.kt @@ -17,6 +17,10 @@ package com.android.systemui.communal.ui.compose import android.util.SizeF +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.gestures.detectDragGesturesAfterLongPress import androidx.compose.foundation.gestures.scrollBy @@ -32,6 +36,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.input.pointer.pointerInput @@ -39,6 +44,7 @@ import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.toOffset import androidx.compose.ui.unit.toSize import androidx.compose.ui.zIndex +import com.android.systemui.communal.ui.compose.extensions.firstItemAtOffset import com.android.systemui.communal.ui.compose.extensions.plus import com.android.systemui.communal.ui.viewmodel.BaseCommunalViewModel import kotlinx.coroutines.CoroutineScope @@ -109,13 +115,10 @@ internal constructor( internal fun onDragStart(offset: Offset, contentOffset: Offset) { state.layoutInfo.visibleItemsInfo - .firstOrNull { item -> - // grid item offset is based off grid content container so we need to deduct - // before content padding from the initial pointer position - contentListState.isItemEditable(item.index) && - (offset.x - contentOffset.x).toInt() in item.offset.x..item.offsetEnd.x && - (offset.y - contentOffset.y).toInt() in item.offset.y..item.offsetEnd.y - } + .filter { item -> contentListState.isItemEditable(item.index) } + // grid item offset is based off grid content container so we need to deduct + // before content padding from the initial pointer position + .firstItemAtOffset(offset - contentOffset) ?.apply { dragStartPointerOffset = offset - this.offset.toOffset() draggingItemIndex = index @@ -148,12 +151,11 @@ internal constructor( val middleOffset = startOffset + (endOffset - startOffset) / 2f val targetItem = - state.layoutInfo.visibleItemsInfo.find { item -> - contentListState.isItemEditable(item.index) && - middleOffset.x.toInt() in item.offset.x..item.offsetEnd.x && - middleOffset.y.toInt() in item.offset.y..item.offsetEnd.y && - draggingItem.index != item.index - } + state.layoutInfo.visibleItemsInfo + .asSequence() + .filter { item -> contentListState.isItemEditable(item.index) } + .filter { item -> draggingItem.index != item.index } + .firstItemAtOffset(middleOffset) if (targetItem != null) { val scrollToIndex = @@ -208,32 +210,31 @@ internal constructor( fun Modifier.dragContainer( dragDropState: GridDragDropState, - beforeContentPadding: ContentPaddingInPx, + contentOffset: Offset, viewModel: BaseCommunalViewModel, ): Modifier { - return pointerInput(dragDropState, beforeContentPadding) { - detectDragGesturesAfterLongPress( - onDrag = { change, offset -> - change.consume() - dragDropState.onDrag(offset = offset) - }, - onDragStart = { offset -> - dragDropState.onDragStart( - offset, - Offset(beforeContentPadding.startPadding, beforeContentPadding.topPadding) - ) - viewModel.onReorderWidgetStart() - }, - onDragEnd = { - dragDropState.onDragInterrupted() - viewModel.onReorderWidgetEnd() - }, - onDragCancel = { - dragDropState.onDragInterrupted() - viewModel.onReorderWidgetCancel() - } - ) - } + return this.then( + pointerInput(dragDropState, contentOffset) { + detectDragGesturesAfterLongPress( + onDrag = { change, offset -> + change.consume() + dragDropState.onDrag(offset = offset) + }, + onDragStart = { offset -> + dragDropState.onDragStart(offset, contentOffset) + viewModel.onReorderWidgetStart() + }, + onDragEnd = { + dragDropState.onDragInterrupted() + viewModel.onReorderWidgetEnd() + }, + onDragCancel = { + dragDropState.onDragInterrupted() + viewModel.onReorderWidgetCancel() + } + ) + } + ) } /** Wrap LazyGrid item with additional modifier needed for drag and drop. */ @@ -243,6 +244,7 @@ fun LazyGridItemScope.DraggableItem( dragDropState: GridDragDropState, index: Int, enabled: Boolean, + selected: Boolean, size: SizeF, modifier: Modifier = Modifier, content: @Composable (isDragging: Boolean) -> Unit @@ -250,21 +252,31 @@ fun LazyGridItemScope.DraggableItem( if (!enabled) { return Box(modifier = modifier) { content(false) } } + val dragging = index == dragDropState.draggingItemIndex + val itemAlpha: Float by + animateFloatAsState( + targetValue = if (dragDropState.isDraggingToRemove) 0.5f else 1f, + label = "DraggableItemAlpha" + ) val draggingModifier = if (dragging) { Modifier.zIndex(1f).graphicsLayer { translationX = dragDropState.draggingItemOffset.x translationY = dragDropState.draggingItemOffset.y - alpha = if (dragDropState.isDraggingToRemove) 0.5f else 1f + alpha = itemAlpha } } else { Modifier.animateItemPlacement() } Box(modifier) { - if (dragging) { - WidgetPlaceholderContent(size) + AnimatedVisibility( + visible = (dragging || selected) && !dragDropState.isDraggingToRemove, + enter = fadeIn(), + exit = fadeOut() + ) { + HighlightedItem(size) } Box(modifier = draggingModifier, propagateMinConstraints = true) { content(dragging) } } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/extensions/LazyGridStateExt.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/extensions/LazyGridStateExt.kt new file mode 100644 index 000000000000..132093f034bb --- /dev/null +++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/extensions/LazyGridStateExt.kt @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.communal.ui.compose.extensions + +import androidx.compose.foundation.lazy.grid.LazyGridItemInfo +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.unit.IntRect +import androidx.compose.ui.unit.toRect + +/** + * Determine the item at the specified offset, or null if none exist. + * + * @param offset The offset in pixels, relative to the top start of the grid. + */ +fun Iterable<LazyGridItemInfo>.firstItemAtOffset(offset: Offset): LazyGridItemInfo? = + firstOrNull { item -> + isItemAtOffset(item, offset) + } + +/** + * Determine the item at the specified offset, or null if none exist. + * + * @param offset The offset in pixels, relative to the top start of the grid. + */ +fun Sequence<LazyGridItemInfo>.firstItemAtOffset(offset: Offset): LazyGridItemInfo? = + firstOrNull { item -> + isItemAtOffset(item, offset) + } + +private fun isItemAtOffset(item: LazyGridItemInfo, offset: Offset): Boolean { + val boundingBox = IntRect(item.offset, item.size) + return boundingBox.toRect().contains(offset) +} diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/extensions/ModifierExt.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/extensions/ModifierExt.kt new file mode 100644 index 000000000000..b31008e04593 --- /dev/null +++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/extensions/ModifierExt.kt @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.communal.ui.compose.extensions + +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.pointer.pointerInput + +/** Sets whether gestures are allowed on children of this element. */ +fun Modifier.allowGestures(allowed: Boolean): Modifier = + if (allowed) { + this + } else { + this.then(pointerInput(Unit) { consumeAllGestures() }) + } 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 new file mode 100644 index 000000000000..14074944259b --- /dev/null +++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/extensions/PointerInputScopeExt.kt @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.communal.ui.compose.extensions + +import androidx.compose.foundation.gestures.awaitEachGesture +import androidx.compose.foundation.gestures.awaitFirstDown +import androidx.compose.foundation.gestures.waitForUpOrCancellation +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.input.pointer.PointerEventPass +import androidx.compose.ui.input.pointer.PointerInputChange +import androidx.compose.ui.input.pointer.PointerInputScope +import kotlinx.coroutines.coroutineScope + +/** + * Observe taps without actually consuming them, so child elements can still respond to them. Long + * presses are excluded. + */ +suspend fun PointerInputScope.observeTapsWithoutConsuming( + pass: PointerEventPass = PointerEventPass.Initial, + onTap: ((Offset) -> Unit)? = null, +) = coroutineScope { + if (onTap == null) return@coroutineScope + awaitEachGesture { + awaitFirstDown(pass = pass) + val tapTimeout = viewConfiguration.longPressTimeoutMillis + val up = withTimeoutOrNull(tapTimeout) { waitForUpOrCancellation(pass = pass) } + if (up != null) { + onTap(up.position) + } + } +} + +/** Consume all gestures on the initial pass so that child elements do not receive them. */ +suspend fun PointerInputScope.consumeAllGestures() = coroutineScope { + awaitEachGesture { + awaitPointerEvent(pass = PointerEventPass.Initial) + .changes + .forEach(PointerInputChange::consume) + } +} 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 84d42463913c..56d6879e614e 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 @@ -17,13 +17,16 @@ package com.android.systemui.keyguard.ui.composable.blueprint import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.layout.Layout +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.IntRect import com.android.compose.animation.scene.SceneScope +import com.android.compose.modifiers.padding 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 @@ -66,6 +69,7 @@ constructor( override fun SceneScope.Content(modifier: Modifier) { val isUdfpsVisible = viewModel.isUdfpsVisible val burnIn = rememberBurnIn(clockInteractor) + val resources = LocalContext.current.resources LockscreenLongPress( viewModel = viewModel.longPress, @@ -88,13 +92,27 @@ constructor( SmartSpace( burnInParams = burnIn.parameters, onTopChanged = burnIn.onSmartspaceTopChanged, - modifier = Modifier.fillMaxWidth(), + modifier = + Modifier.fillMaxWidth() + .padding( + top = { viewModel.getSmartSpacePaddingTop(resources) } + ), ) } - with(clockSection) { LargeClock(modifier = Modifier.fillMaxWidth()) } - with(notificationSection) { - Notifications(modifier = Modifier.fillMaxWidth().weight(1f)) + + if (viewModel.isLargeClockVisible) { + Spacer(modifier = Modifier.weight(weight = 1f)) + with(clockSection) { LargeClock(modifier = Modifier.fillMaxWidth()) } + } + + if (viewModel.areNotificationsVisible) { + with(notificationSection) { + Notifications( + modifier = Modifier.fillMaxWidth().weight(weight = 1f) + ) + } } + if (!isUdfpsVisible && ambientIndicationSectionOptional.isPresent) { with(ambientIndicationSectionOptional.get()) { AmbientIndication(modifier = Modifier.fillMaxWidth()) diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/ShortcutsBesideUdfpsBlueprint.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/ShortcutsBesideUdfpsBlueprint.kt index 414846276b2a..d0aa4443d487 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/ShortcutsBesideUdfpsBlueprint.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/ShortcutsBesideUdfpsBlueprint.kt @@ -17,13 +17,16 @@ package com.android.systemui.keyguard.ui.composable.blueprint import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.layout.Layout +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.IntRect import com.android.compose.animation.scene.SceneScope +import com.android.compose.modifiers.padding 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 @@ -66,6 +69,7 @@ constructor( override fun SceneScope.Content(modifier: Modifier) { val isUdfpsVisible = viewModel.isUdfpsVisible val burnIn = rememberBurnIn(clockInteractor) + val resources = LocalContext.current.resources LockscreenLongPress( viewModel = viewModel.longPress, @@ -88,13 +92,27 @@ constructor( SmartSpace( burnInParams = burnIn.parameters, onTopChanged = burnIn.onSmartspaceTopChanged, - modifier = Modifier.fillMaxWidth(), + modifier = + Modifier.fillMaxWidth() + .padding( + top = { viewModel.getSmartSpacePaddingTop(resources) } + ), ) } - with(clockSection) { LargeClock(modifier = Modifier.fillMaxWidth()) } - with(notificationSection) { - Notifications(modifier = Modifier.fillMaxWidth().weight(1f)) + + if (viewModel.isLargeClockVisible) { + Spacer(modifier = Modifier.weight(weight = 1f)) + with(clockSection) { LargeClock(modifier = Modifier.fillMaxWidth()) } + } + + if (viewModel.areNotificationsVisible) { + with(notificationSection) { + Notifications( + modifier = Modifier.fillMaxWidth().weight(weight = 1f) + ) + } } + if (!isUdfpsVisible && ambientIndicationSectionOptional.isPresent) { with(ambientIndicationSectionOptional.get()) { AmbientIndication(modifier = Modifier.fillMaxWidth()) 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 f021bb6743c4..f40b871e923c 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 @@ -30,10 +30,10 @@ import com.android.compose.animation.scene.ElementKey import com.android.compose.animation.scene.SceneScope import com.android.compose.modifiers.padding import com.android.keyguard.KeyguardClockSwitch +import com.android.systemui.customization.R as customizationR import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor import com.android.systemui.keyguard.ui.composable.modifier.onTopPlacementChanged import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel -import com.android.systemui.res.R import javax.inject.Inject class ClockSection @@ -79,7 +79,7 @@ constructor( modifier = Modifier.padding( horizontal = - dimensionResource(R.dimen.keyguard_affordance_horizontal_offset) + dimensionResource(customizationR.dimen.clock_padding_start) ) .padding(top = { viewModel.getSmallClockTopMargin(view.context) }) .onTopPlacementChanged(onTopChanged), @@ -117,9 +117,7 @@ constructor( content { AndroidView( factory = { checkNotNull(currentClock).largeClock.view }, - modifier = - Modifier.fillMaxWidth() - .padding(top = { viewModel.getLargeClockTopMargin(view.context) }) + modifier = Modifier.fillMaxWidth() ) } } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt index 900616f6af89..42fcd1363f11 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt @@ -16,23 +16,55 @@ package com.android.systemui.keyguard.ui.composable.section +import android.content.Context +import android.view.ViewGroup import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import com.android.compose.animation.scene.SceneScope +import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.notifications.ui.composable.NotificationStack +import com.android.systemui.scene.shared.flag.SceneContainerFlags +import com.android.systemui.statusbar.notification.stack.AmbientState +import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout +import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController +import com.android.systemui.statusbar.notification.stack.shared.flexiNotifsEnabled +import com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificationContainer +import com.android.systemui.statusbar.notification.stack.ui.viewbinder.NotificationStackAppearanceViewBinder +import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationStackAppearanceViewModel import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationsPlaceholderViewModel import javax.inject.Inject class NotificationSection @Inject constructor( + @Application context: Context, private val viewModel: NotificationsPlaceholderViewModel, + controller: NotificationStackScrollLayoutController, + sceneContainerFlags: SceneContainerFlags, + sharedNotificationContainer: SharedNotificationContainer, + stackScrollLayout: NotificationStackScrollLayout, + notificationStackAppearanceViewModel: NotificationStackAppearanceViewModel, + ambientState: AmbientState, ) { + init { + if (sceneContainerFlags.flexiNotifsEnabled()) { + (stackScrollLayout.parent as? ViewGroup)?.removeView(stackScrollLayout) + sharedNotificationContainer.addNotificationStackScrollLayout(stackScrollLayout) + + NotificationStackAppearanceViewBinder.bind( + context, + sharedNotificationContainer, + notificationStackAppearanceViewModel, + ambientState, + controller, + ) + } + } + @Composable fun SceneScope.Notifications(modifier: Modifier = Modifier) { NotificationStack( viewModel = viewModel, - isScrimVisible = false, modifier = modifier, ) } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt index 0b26ae96de54..e835d3e576d5 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt @@ -22,6 +22,7 @@ import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box 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.shape.RoundedCornerShape import androidx.compose.material3.MaterialTheme @@ -43,8 +44,10 @@ import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.dp import com.android.compose.animation.scene.ElementKey import com.android.compose.animation.scene.SceneScope +import com.android.compose.modifiers.height import com.android.systemui.notifications.ui.composable.Notifications.Form import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationsPlaceholderViewModel +import kotlin.math.roundToInt object Notifications { object Elements { @@ -77,32 +80,52 @@ fun SceneScope.HeadsUpNotificationSpace( ) } -/** Adds the space where notification stack will appear in the scene. */ +/** Adds the space where notification stack should appear in the scene. */ @Composable fun SceneScope.NotificationStack( viewModel: NotificationsPlaceholderViewModel, - isScrimVisible: Boolean, + modifier: Modifier = Modifier, +) { + NotificationPlaceholder( + viewModel = viewModel, + form = Form.Stack, + modifier = modifier, + ) +} + +/** + * Adds the space where notification stack should appear in the scene, with a scrim and nested + * scrolling. + */ +@Composable +fun SceneScope.NotificationScrollingStack( + viewModel: NotificationsPlaceholderViewModel, modifier: Modifier = Modifier, ) { val cornerRadius by viewModel.cornerRadiusDp.collectAsState() - Box(modifier = modifier) { - if (isScrimVisible) { - Box( - modifier = - Modifier.element(Notifications.Elements.NotificationScrim) - .fillMaxSize() - .graphicsLayer { - shape = RoundedCornerShape(cornerRadius.dp) - clip = true - } - .background(MaterialTheme.colorScheme.surface) - ) - } + val contentHeight by viewModel.intrinsicContentHeight.collectAsState() + + val expansionFraction by viewModel.expandFraction.collectAsState(0f) + + Box( + modifier = + modifier + .verticalNestedScrollToScene() + .fillMaxWidth() + .element(Notifications.Elements.NotificationScrim) + .graphicsLayer { + shape = RoundedCornerShape(cornerRadius.dp) + clip = true + alpha = expansionFraction + } + .background(MaterialTheme.colorScheme.surface) + .debugBackground(viewModel, Color(0.5f, 0.5f, 0f, 0.2f)) + ) { NotificationPlaceholder( viewModel = viewModel, form = Form.Stack, - modifier = Modifier.fillMaxSize(), + modifier = Modifier.fillMaxWidth().height { contentHeight.roundToInt() } ) } } @@ -159,6 +182,7 @@ private fun SceneScope.NotificationPlaceholder( debugLog(viewModel) { "STACK onSizeChanged: size=$size" } } .onPlaced { coordinates: LayoutCoordinates -> + viewModel.onContentTopChanged(coordinates.positionInWindow().y) debugLog(viewModel) { "STACK onPlaced:" + " size=${coordinates.size}" + diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/GoneScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/GoneScene.kt index bded98d52481..747faabe514b 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/GoneScene.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/GoneScene.kt @@ -25,6 +25,7 @@ import androidx.compose.ui.unit.dp import com.android.compose.animation.scene.SceneScope import com.android.systemui.dagger.SysUISingleton import com.android.systemui.notifications.ui.composable.HeadsUpNotificationSpace +import com.android.systemui.notifications.ui.composable.Notifications import com.android.systemui.scene.shared.model.Direction import com.android.systemui.scene.shared.model.Edge import com.android.systemui.scene.shared.model.SceneKey @@ -66,6 +67,7 @@ constructor( modifier: Modifier, ) { Box(modifier = modifier) { + Box(modifier = Modifier.fillMaxSize().element(Notifications.Elements.NotificationScrim)) HeadsUpNotificationSpace( viewModel = notificationsViewModel, modifier = Modifier.padding(16.dp).fillMaxSize(), diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromGoneToShadeTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromGoneToShadeTransition.kt index 6bb525aa00fb..0c2c5195becc 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromGoneToShadeTransition.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromGoneToShadeTransition.kt @@ -3,12 +3,12 @@ package com.android.systemui.scene.ui.composable.transitions import androidx.compose.animation.core.tween import com.android.compose.animation.scene.Edge import com.android.compose.animation.scene.TransitionBuilder -import com.android.systemui.notifications.ui.composable.Notifications -import com.android.systemui.scene.ui.composable.Shade +import com.android.systemui.qs.ui.composable.QuickSettings +import com.android.systemui.shade.ui.composable.ShadeHeader fun TransitionBuilder.goneToShadeTransition() { spec = tween(durationMillis = 500) - translate(Shade.rootElementKey, Edge.Top, true) - fade(Notifications.Elements.NotificationScrim) + fractionRange(start = .58f) { fade(ShadeHeader.Elements.CollapsedContent) } + translate(QuickSettings.Elements.Content, Edge.Top, true) } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt index 9c0f1fe0ec68..1545372686c9 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt @@ -22,11 +22,9 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer -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.shape.RoundedCornerShape import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable @@ -44,10 +42,12 @@ import com.android.systemui.battery.BatteryMeterViewController import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.media.controls.ui.MediaCarouselController +import com.android.systemui.media.controls.ui.MediaHierarchyManager import com.android.systemui.media.controls.ui.MediaHost +import com.android.systemui.media.controls.ui.MediaHostState import com.android.systemui.media.controls.ui.composable.MediaCarousel import com.android.systemui.media.dagger.MediaModule.QUICK_QS_PANEL -import com.android.systemui.notifications.ui.composable.NotificationStack +import com.android.systemui.notifications.ui.composable.NotificationScrollingStack import com.android.systemui.qs.ui.composable.QuickSettings import com.android.systemui.res.R import com.android.systemui.scene.shared.model.Direction @@ -128,6 +128,12 @@ constructor( modifier = modifier, ) + init { + mediaHost.expansion = MediaHostState.EXPANDED + mediaHost.showsOnlyActiveMedia = true + mediaHost.init(MediaHierarchyManager.LOCATION_QQS) + } + private fun destinationScenes( up: SceneKey, ): Map<UserAction, SceneModel> { @@ -148,35 +154,27 @@ private fun SceneScope.ShadeScene( mediaHost: MediaHost, modifier: Modifier = Modifier, ) { + val localDensity = LocalDensity.current val layoutWidth = remember { mutableStateOf(0) } - Box(modifier.element(Shade.Elements.Scrim)) { - Spacer( - modifier = - Modifier.element(Shade.Elements.ScrimBackground) - .fillMaxSize() - .background(MaterialTheme.colorScheme.scrim, shape = Shade.Shapes.Scrim) - ) + Box( + modifier = + modifier.element(Shade.Elements.Scrim).background(MaterialTheme.colorScheme.scrim), + ) + Box { Column( horizontalAlignment = Alignment.CenterHorizontally, - modifier = - Modifier.fillMaxSize() - .clickable(onClick = { viewModel.onContentClicked() }) - .padding( - start = Shade.Dimensions.HorizontalPadding, - end = Shade.Dimensions.HorizontalPadding, - bottom = 48.dp - ) + modifier = Modifier.fillMaxWidth().clickable(onClick = { viewModel.onContentClicked() }) ) { CollapsedShadeHeader( viewModel = viewModel.shadeHeaderViewModel, createTintedIconManager = createTintedIconManager, createBatteryMeterViewController = createBatteryMeterViewController, statusBarIconController = statusBarIconController, + modifier = Modifier.padding(horizontal = Shade.Dimensions.HorizontalPadding) ) - Spacer(modifier = Modifier.height(16.dp)) QuickSettings( - modifier = Modifier.wrapContentHeight(), + modifier = Modifier.height(130.dp), viewModel.qsSceneAdapter, ) @@ -202,16 +200,15 @@ private fun SceneScope.ShadeScene( }, mediaHost = mediaHost, layoutWidth = layoutWidth.value, - layoutHeight = with(LocalDensity.current) { mediaHeight.toPx() }.toInt(), + layoutHeight = with(localDensity) { mediaHeight.toPx() }.toInt(), carouselController = mediaCarouselController, ) } Spacer(modifier = Modifier.height(16.dp)) - NotificationStack( + NotificationScrollingStack( viewModel = viewModel.notifications, - isScrimVisible = true, - modifier = Modifier.weight(1f), + modifier = Modifier.fillMaxWidth().weight(1f), ) } } diff --git a/packages/SystemUI/compose/features/tests/AndroidManifest.xml b/packages/SystemUI/compose/features/tests/AndroidManifest.xml index 8fe9656c1879..fc337fb19f43 100644 --- a/packages/SystemUI/compose/features/tests/AndroidManifest.xml +++ b/packages/SystemUI/compose/features/tests/AndroidManifest.xml @@ -30,11 +30,6 @@ android:enabled="false" tools:replace="android:authorities" tools:node="remove" /> - <provider android:name="com.google.android.systemui.keyguard.KeyguardSliceProviderGoogle" - android:authorities="com.android.systemui.test.keyguard.disabled" - android:enabled="false" - tools:replace="android:authorities" - tools:node="remove" /> <provider android:name="com.android.systemui.keyguard.CustomizationProvider" android:authorities="com.android.systemui.test.keyguard.quickaffordance.disabled" android:enabled="false" 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 64388b7653e0..ff054786cf52 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 @@ -73,7 +73,7 @@ internal class SceneGestureHandler( private val positionalThreshold get() = with(layoutImpl.density) { 56.dp.toPx() } - internal var gestureWithPriority: Any? = null + internal var currentSource: Any? = null /** The [UserAction]s associated to the current swipe. */ private var actionUpOrLeft: UserAction? = null @@ -520,20 +520,22 @@ internal class SceneGestureHandler( private class SceneDraggableHandler( private val gestureHandler: SceneGestureHandler, ) : DraggableHandler { + private val source = this + override fun onDragStarted(startedPosition: Offset, overSlop: Float, pointersDown: Int) { - gestureHandler.gestureWithPriority = this + gestureHandler.currentSource = source gestureHandler.onDragStarted(pointersDown, startedPosition, overSlop) } override fun onDelta(pixels: Float) { - if (gestureHandler.gestureWithPriority == this) { + if (gestureHandler.currentSource == source) { gestureHandler.onDrag(delta = pixels) } } override fun onDragStopped(velocity: Float) { - if (gestureHandler.gestureWithPriority == this) { - gestureHandler.gestureWithPriority = null + if (gestureHandler.currentSource == source) { + gestureHandler.currentSource = null gestureHandler.onDragStopped(velocity = velocity, canChangeScene = true) } } @@ -586,6 +588,8 @@ internal class SceneNestedScrollHandler( return nextScene != null } + val source = this + return PriorityNestedScrollConnection( orientation = orientation, canStartPreScroll = { offsetAvailable, offsetBeforeStart -> @@ -656,7 +660,7 @@ internal class SceneNestedScrollHandler( canContinueScroll = { true }, canScrollOnFling = false, onStart = { offsetAvailable -> - gestureHandler.gestureWithPriority = this + gestureHandler.currentSource = source gestureHandler.onDragStarted( pointersDown = 1, startedPosition = null, @@ -664,7 +668,7 @@ internal class SceneNestedScrollHandler( ) }, onScroll = { offsetAvailable -> - if (gestureHandler.gestureWithPriority != this) { + if (gestureHandler.currentSource != source) { return@PriorityNestedScrollConnection 0f } @@ -675,7 +679,7 @@ internal class SceneNestedScrollHandler( offsetAvailable }, onStop = { velocityAvailable -> - if (gestureHandler.gestureWithPriority != this) { + if (gestureHandler.currentSource != source) { return@PriorityNestedScrollConnection 0f } diff --git a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardPinBasedInputViewControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardPinBasedInputViewControllerTest.java index e89316997fb2..c4bcb536de78 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardPinBasedInputViewControllerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardPinBasedInputViewControllerTest.java @@ -35,6 +35,7 @@ import com.android.systemui.SysuiTestCase; import com.android.systemui.classifier.FalsingCollector; import com.android.systemui.classifier.FalsingCollectorFake; import com.android.systemui.flags.FakeFeatureFlags; +import com.android.systemui.keyboard.data.repository.FakeKeyboardRepository; import com.android.systemui.res.R; import com.android.systemui.user.domain.interactor.SelectedUserInteractor; @@ -107,7 +108,7 @@ public class KeyguardPinBasedInputViewControllerTest extends SysuiTestCase { mKeyguardUpdateMonitor, mSecurityMode, mLockPatternUtils, mKeyguardSecurityCallback, mKeyguardMessageAreaControllerFactory, mLatencyTracker, mLiftToactivateListener, mEmergencyButtonController, mFalsingCollector, featureFlags, - mSelectedUserInteractor) { + mSelectedUserInteractor, new FakeKeyboardRepository()) { @Override public void onResume(int reason) { super.onResume(reason); diff --git a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt index 78b854e39d13..c2efc05132ba 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt @@ -33,6 +33,7 @@ import com.android.systemui.classifier.FalsingCollector import com.android.systemui.classifier.FalsingCollectorFake import com.android.systemui.flags.FeatureFlags import com.android.systemui.flags.Flags +import com.android.systemui.keyboard.data.repository.FakeKeyboardRepository import com.android.systemui.res.R import com.android.systemui.statusbar.policy.DevicePostureController import com.android.systemui.statusbar.policy.DevicePostureController.DEVICE_POSTURE_HALF_OPENED @@ -141,7 +142,8 @@ class KeyguardPinViewControllerTest : SysuiTestCase() { postureController, featureFlags, mSelectedUserInteractor, - uiEventLogger + uiEventLogger, + FakeKeyboardRepository() ) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt index 02d30c5ea20a..2a793ea70292 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt @@ -47,16 +47,20 @@ import com.android.systemui.classifier.FalsingA11yDelegate import com.android.systemui.classifier.FalsingCollector import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFaceAuthInteractor import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor +import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor import com.android.systemui.flags.FakeFeatureFlags import com.android.systemui.flags.Flags import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractorFactory +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.testScope import com.android.systemui.log.SessionTracker import com.android.systemui.plugins.ActivityStarter.OnDismissAction import com.android.systemui.plugins.FalsingManager import com.android.systemui.res.R -import com.android.systemui.scene.SceneTestUtils import com.android.systemui.scene.domain.interactor.SceneInteractor +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 @@ -66,6 +70,7 @@ import com.android.systemui.statusbar.policy.DevicePostureController import com.android.systemui.statusbar.policy.DeviceProvisionedController import com.android.systemui.statusbar.policy.KeyguardStateController import com.android.systemui.statusbar.policy.UserSwitcherController +import com.android.systemui.testKosmos import com.android.systemui.user.domain.interactor.SelectedUserInteractor import com.android.systemui.util.kotlin.JavaAdapter import com.android.systemui.util.mockito.any @@ -156,7 +161,7 @@ class KeyguardSecurityContainerControllerTest : SysuiTestCase() { private lateinit var keyguardPasswordViewController: KeyguardPasswordViewController private lateinit var keyguardPasswordView: KeyguardPasswordView private lateinit var testableResources: TestableResources - private lateinit var sceneTestUtils: SceneTestUtils + private lateinit var kosmos: Kosmos private lateinit var sceneInteractor: SceneInteractor private lateinit var keyguardTransitionInteractor: KeyguardTransitionInteractor private lateinit var deviceEntryInteractor: DeviceEntryInteractor @@ -222,19 +227,15 @@ class KeyguardSecurityContainerControllerTest : SysuiTestCase() { mSelectedUserInteractor, ) - sceneTestUtils = SceneTestUtils(this) - sceneInteractor = sceneTestUtils.sceneInteractor() + kosmos = testKosmos() + sceneInteractor = kosmos.sceneInteractor keyguardTransitionInteractor = - KeyguardTransitionInteractorFactory.create(sceneTestUtils.testScope.backgroundScope) + KeyguardTransitionInteractorFactory.create(kosmos.testScope.backgroundScope) .keyguardTransitionInteractor sceneTransitionStateFlow = MutableStateFlow(ObservableTransitionState.Idle(SceneKey.Lockscreen)) sceneInteractor.setTransitionState(sceneTransitionStateFlow) - deviceEntryInteractor = - sceneTestUtils.deviceEntryInteractor( - authenticationInteractor = sceneTestUtils.authenticationInteractor(), - sceneInteractor = sceneInteractor, - ) + deviceEntryInteractor = kosmos.deviceEntryInteractor mSetFlagsRule.disableFlags(FLAG_SIDEFPS_CONTROLLER_REFACTOR) underTest = @@ -253,7 +254,7 @@ class KeyguardSecurityContainerControllerTest : SysuiTestCase() { falsingManager, userSwitcherController, featureFlags, - sceneTestUtils.sceneContainerFlags, + kosmos.fakeSceneContainerFlags, globalSettings, sessionTracker, Optional.of(sideFpsController), @@ -263,7 +264,7 @@ class KeyguardSecurityContainerControllerTest : SysuiTestCase() { audioManager, faceAuthInteractor, mock(), - { JavaAdapter(sceneTestUtils.testScope.backgroundScope) }, + { JavaAdapter(kosmos.testScope.backgroundScope) }, mSelectedUserInteractor, deviceProvisionedController, faceAuthAccessibilityDelegate, @@ -790,7 +791,8 @@ class KeyguardSecurityContainerControllerTest : SysuiTestCase() { @Test fun dismissesKeyguard_whenSceneChangesToGone() = - sceneTestUtils.testScope.runTest { + kosmos.testScope.runTest { + kosmos.fakeSceneContainerFlags.enabled = true // Upon init, we have never dismisses the keyguard. underTest.onInit() runCurrent() diff --git a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSimPinViewControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSimPinViewControllerTest.kt index f7751753cc18..0959f1b2bcf6 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSimPinViewControllerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSimPinViewControllerTest.kt @@ -16,7 +16,6 @@ package com.android.keyguard -import android.telephony.PinResult import android.telephony.TelephonyManager import android.testing.TestableLooper import android.view.LayoutInflater @@ -28,9 +27,11 @@ import com.android.systemui.Flags import com.android.systemui.SysuiTestCase import com.android.systemui.classifier.FalsingCollector import com.android.systemui.flags.FakeFeatureFlags +import com.android.systemui.keyboard.data.repository.FakeKeyboardRepository import com.android.systemui.res.R import com.android.systemui.user.domain.interactor.SelectedUserInteractor import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.mock import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -39,7 +40,6 @@ import org.mockito.ArgumentMatchers.anyString import org.mockito.Mock import org.mockito.Mockito import org.mockito.Mockito.anyInt -import org.mockito.Mockito.mock import org.mockito.Mockito.never import org.mockito.Mockito.reset import org.mockito.Mockito.verify @@ -75,8 +75,7 @@ class KeyguardSimPinViewControllerTest : SysuiTestCase() { `when`(messageAreaControllerFactory.create(Mockito.any(KeyguardMessageArea::class.java))) .thenReturn(keyguardMessageAreaController) `when`(telephonyManager.createForSubscriptionId(anyInt())).thenReturn(telephonyManager) - `when`(telephonyManager.supplyIccLockPin(anyString())) - .thenReturn(mock(PinResult::class.java)) + `when`(telephonyManager.supplyIccLockPin(anyString())).thenReturn(mock()) simPinView = LayoutInflater.from(context).inflate(R.layout.keyguard_sim_pin_view, null) as KeyguardSimPinView @@ -97,7 +96,8 @@ class KeyguardSimPinViewControllerTest : SysuiTestCase() { falsingCollector, emergencyButtonController, fakeFeatureFlags, - mSelectedUserInteractor + mSelectedUserInteractor, + FakeKeyboardRepository() ) underTest.init() underTest.onViewAttached() diff --git a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSimPukViewControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSimPukViewControllerTest.kt index 45a60199984b..1281e4409a83 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSimPukViewControllerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSimPukViewControllerTest.kt @@ -28,6 +28,7 @@ import com.android.systemui.Flags import com.android.systemui.SysuiTestCase import com.android.systemui.classifier.FalsingCollector import com.android.systemui.flags.FakeFeatureFlags +import com.android.systemui.keyboard.data.repository.FakeKeyboardRepository import com.android.systemui.res.R import com.android.systemui.user.domain.interactor.SelectedUserInteractor import com.android.systemui.util.mockito.any @@ -91,6 +92,7 @@ class KeyguardSimPukViewControllerTest : SysuiTestCase() { emergencyButtonController, fakeFeatureFlags, mSelectedUserInteractor, + FakeKeyboardRepository() ) underTest.init() } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/ColorCorrectionRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/ColorCorrectionRepositoryImplTest.kt index 27d1eb741bb0..c86c7470909b 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/ColorCorrectionRepositoryImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/ColorCorrectionRepositoryImplTest.kt @@ -17,14 +17,15 @@ package com.android.systemui.accessibility.data.repository import android.os.UserHandle +import android.provider.Settings.Secure.ACCESSIBILITY_DISPLAY_DALTONIZER_ENABLED 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.coroutines.collectValues import com.android.systemui.util.settings.FakeSettings import com.google.common.truth.Truth import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runCurrent @@ -51,6 +52,7 @@ class ColorCorrectionRepositoryImplTest : SysuiTestCase() { underTest = ColorCorrectionRepositoryImpl( testDispatcher, + scope.backgroundScope, settings, ) } @@ -58,83 +60,78 @@ class ColorCorrectionRepositoryImplTest : SysuiTestCase() { @Test fun isEnabled_initiallyGetsSettingsValue() = scope.runTest { + val actualValue by collectLastValue(underTest.isEnabled(testUser1)) + settings.putIntForUser( - ColorCorrectionRepositoryImpl.SETTING_NAME, - 1, + SETTING_NAME, + ENABLED, testUser1.identifier ) - - underTest = - ColorCorrectionRepositoryImpl( - testDispatcher, - settings, - ) - - underTest.isEnabled(testUser1).launchIn(backgroundScope) runCurrent() - val actualValue: Boolean = underTest.isEnabled(testUser1).first() Truth.assertThat(actualValue).isTrue() } @Test fun isEnabled_settingUpdated_valueUpdated() = scope.runTest { - underTest.isEnabled(testUser1).launchIn(backgroundScope) + val flowValues: List<Boolean> by collectValues(underTest.isEnabled(testUser1)) settings.putIntForUser( - ColorCorrectionRepositoryImpl.SETTING_NAME, - ColorCorrectionRepositoryImpl.DISABLED, + SETTING_NAME, + DISABLED, testUser1.identifier ) runCurrent() - Truth.assertThat(underTest.isEnabled(testUser1).first()).isFalse() settings.putIntForUser( - ColorCorrectionRepositoryImpl.SETTING_NAME, - ColorCorrectionRepositoryImpl.ENABLED, + SETTING_NAME, + ENABLED, testUser1.identifier ) runCurrent() - Truth.assertThat(underTest.isEnabled(testUser1).first()).isTrue() settings.putIntForUser( - ColorCorrectionRepositoryImpl.SETTING_NAME, - ColorCorrectionRepositoryImpl.DISABLED, + SETTING_NAME, + DISABLED, testUser1.identifier ) runCurrent() - Truth.assertThat(underTest.isEnabled(testUser1).first()).isFalse() + + Truth.assertThat(flowValues.size).isEqualTo(3) + Truth.assertThat(flowValues).containsExactly(false, true, false).inOrder() } @Test fun isEnabled_settingForUserOneOnly_valueUpdatedForUserOneOnly() = scope.runTest { - underTest.isEnabled(testUser1).launchIn(backgroundScope) + val lastValueUser1 by collectLastValue(underTest.isEnabled(testUser1)) + val lastValueUser2 by collectLastValue(underTest.isEnabled(testUser2)) + settings.putIntForUser( - ColorCorrectionRepositoryImpl.SETTING_NAME, - ColorCorrectionRepositoryImpl.DISABLED, + SETTING_NAME, + DISABLED, testUser1.identifier ) - underTest.isEnabled(testUser2).launchIn(backgroundScope) settings.putIntForUser( - ColorCorrectionRepositoryImpl.SETTING_NAME, - ColorCorrectionRepositoryImpl.DISABLED, + SETTING_NAME, + DISABLED, testUser2.identifier ) - runCurrent() - Truth.assertThat(underTest.isEnabled(testUser1).first()).isFalse() - Truth.assertThat(underTest.isEnabled(testUser2).first()).isFalse() + + Truth.assertThat(lastValueUser1).isFalse() + Truth.assertThat(lastValueUser2).isFalse() settings.putIntForUser( - ColorCorrectionRepositoryImpl.SETTING_NAME, - ColorCorrectionRepositoryImpl.ENABLED, + SETTING_NAME, + ENABLED, testUser1.identifier ) runCurrent() - Truth.assertThat(underTest.isEnabled(testUser1).first()).isTrue() - Truth.assertThat(underTest.isEnabled(testUser2).first()).isFalse() + + Truth.assertThat(lastValueUser1).isTrue() + Truth.assertThat(lastValueUser2).isFalse() } @Test @@ -146,10 +143,10 @@ class ColorCorrectionRepositoryImplTest : SysuiTestCase() { val actualValue = settings.getIntForUser( - ColorCorrectionRepositoryImpl.SETTING_NAME, + SETTING_NAME, testUser1.identifier ) - Truth.assertThat(actualValue).isEqualTo(ColorCorrectionRepositoryImpl.ENABLED) + Truth.assertThat(actualValue).isEqualTo(ENABLED) } @Test @@ -161,9 +158,15 @@ class ColorCorrectionRepositoryImplTest : SysuiTestCase() { val actualValue = settings.getIntForUser( - ColorCorrectionRepositoryImpl.SETTING_NAME, + SETTING_NAME, testUser1.identifier ) - Truth.assertThat(actualValue).isEqualTo(ColorCorrectionRepositoryImpl.DISABLED) + Truth.assertThat(actualValue).isEqualTo(DISABLED) } + + companion object { + private const val SETTING_NAME = ACCESSIBILITY_DISPLAY_DALTONIZER_ENABLED + private const val DISABLED = 0 + private const val ENABLED = 1 + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/ColorInversionRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/ColorInversionRepositoryImplTest.kt index 423e124bbc84..4853529229fe 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/ColorInversionRepositoryImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/ColorInversionRepositoryImplTest.kt @@ -21,11 +21,11 @@ import android.provider.Settings.Secure.ACCESSIBILITY_DISPLAY_INVERSION_ENABLED 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.coroutines.collectValues import com.android.systemui.util.settings.FakeSettings import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runCurrent @@ -52,6 +52,7 @@ class ColorInversionRepositoryImplTest : SysuiTestCase() { underTest = ColorInversionRepositoryImpl( testDispatcher, + scope.backgroundScope, settings, ) } @@ -59,55 +60,47 @@ class ColorInversionRepositoryImplTest : SysuiTestCase() { @Test fun isEnabled_initiallyGetsSettingsValue() = scope.runTest { - settings.putIntForUser(SETTING_NAME, 1, testUser1.identifier) + val actualValue by collectLastValue(underTest.isEnabled(testUser1)) - underTest = - ColorInversionRepositoryImpl( - testDispatcher, - settings, - ) - - underTest.isEnabled(testUser1).launchIn(backgroundScope) + settings.putIntForUser(SETTING_NAME, ENABLED, testUser1.identifier) runCurrent() - val actualValue: Boolean = underTest.isEnabled(testUser1).first() assertThat(actualValue).isTrue() } @Test fun isEnabled_settingUpdated_valueUpdated() = scope.runTest { - underTest.isEnabled(testUser1).launchIn(backgroundScope) + val flowValues: List<Boolean> by + collectValues(underTest.isEnabled(testUser1)) settings.putIntForUser(SETTING_NAME, DISABLED, testUser1.identifier) runCurrent() - assertThat(underTest.isEnabled(testUser1).first()).isFalse() - settings.putIntForUser(SETTING_NAME, ENABLED, testUser1.identifier) runCurrent() - assertThat(underTest.isEnabled(testUser1).first()).isTrue() - settings.putIntForUser(SETTING_NAME, DISABLED, testUser1.identifier) runCurrent() - assertThat(underTest.isEnabled(testUser1).first()).isFalse() + + assertThat(flowValues.size).isEqualTo(3) + assertThat(flowValues).containsExactly(false, true, false).inOrder() } @Test fun isEnabled_settingForUserOneOnly_valueUpdatedForUserOneOnly() = scope.runTest { - underTest.isEnabled(testUser1).launchIn(backgroundScope) + val lastValueUser1 by collectLastValue(underTest.isEnabled(testUser1)) + val lastValueUser2 by collectLastValue(underTest.isEnabled(testUser2)) + settings.putIntForUser(SETTING_NAME, DISABLED, testUser1.identifier) - underTest.isEnabled(testUser2).launchIn(backgroundScope) settings.putIntForUser(SETTING_NAME, DISABLED, testUser2.identifier) - runCurrent() - assertThat(underTest.isEnabled(testUser1).first()).isFalse() - assertThat(underTest.isEnabled(testUser2).first()).isFalse() + assertThat(lastValueUser1).isFalse() + assertThat(lastValueUser2).isFalse() settings.putIntForUser(SETTING_NAME, ENABLED, testUser1.identifier) runCurrent() - assertThat(underTest.isEnabled(testUser1).first()).isTrue() - assertThat(underTest.isEnabled(testUser2).first()).isFalse() + assertThat(lastValueUser1).isTrue() + assertThat(lastValueUser2).isFalse() } @Test diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/authentication/data/repository/AuthenticationRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/authentication/data/repository/AuthenticationRepositoryTest.kt index b9759cc145f2..caf92199737c 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/authentication/data/repository/AuthenticationRepositoryTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/authentication/data/repository/AuthenticationRepositoryTest.kt @@ -29,10 +29,13 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.authentication.shared.model.AuthenticationMethodModel import com.android.systemui.coroutines.collectLastValue import com.android.systemui.coroutines.collectValues +import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.kosmos.testScope import com.android.systemui.log.table.TableLogBuffer -import com.android.systemui.scene.SceneTestUtils +import com.android.systemui.scene.shared.flag.sceneContainerFlags import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionsRepository import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy +import com.android.systemui.testKosmos import com.android.systemui.user.data.repository.FakeUserRepository import com.android.systemui.util.mockito.whenever import com.android.systemui.util.time.FakeSystemClock @@ -59,8 +62,8 @@ class AuthenticationRepositoryTest : SysuiTestCase() { @Mock private lateinit var tableLogger: TableLogBuffer @Mock private lateinit var devicePolicyManager: DevicePolicyManager - private val testUtils = SceneTestUtils(this) - private val testScope = testUtils.testScope + private val kosmos = testKosmos() + private val testScope = kosmos.testScope private val clock = FakeSystemClock() private val userRepository = FakeUserRepository() private lateinit var mobileConnectionsRepository: FakeMobileConnectionsRepository @@ -82,8 +85,8 @@ class AuthenticationRepositoryTest : SysuiTestCase() { underTest = AuthenticationRepositoryImpl( applicationScope = testScope.backgroundScope, - backgroundDispatcher = testUtils.testDispatcher, - flags = testUtils.sceneContainerFlags, + backgroundDispatcher = kosmos.testDispatcher, + flags = kosmos.sceneContainerFlags, clock = clock, getSecurityMode = getSecurityMode, userRepository = userRepository, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorTest.kt index 10c16bd2f3ed..cb8cebf80767 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorTest.kt @@ -22,6 +22,7 @@ import androidx.test.filters.SmallTest import com.android.internal.widget.LockPatternUtils import com.android.systemui.SysuiTestCase import com.android.systemui.authentication.data.repository.FakeAuthenticationRepository +import com.android.systemui.authentication.data.repository.fakeAuthenticationRepository import com.android.systemui.authentication.shared.model.AuthenticationMethodModel.None import com.android.systemui.authentication.shared.model.AuthenticationMethodModel.Password import com.android.systemui.authentication.shared.model.AuthenticationMethodModel.Pattern @@ -29,7 +30,8 @@ import com.android.systemui.authentication.shared.model.AuthenticationMethodMode import com.android.systemui.authentication.shared.model.AuthenticationPatternCoordinate import com.android.systemui.authentication.shared.model.AuthenticationWipeModel import com.android.systemui.coroutines.collectLastValue -import com.android.systemui.scene.SceneTestUtils +import com.android.systemui.kosmos.testScope +import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -45,9 +47,9 @@ import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class AuthenticationInteractorTest : SysuiTestCase() { - private val utils = SceneTestUtils(this) - private val testScope = utils.testScope - private val underTest = utils.authenticationInteractor() + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + private val underTest = kosmos.authenticationInteractor private val onAuthenticationResult by testScope.collectLastValue(underTest.onAuthenticationResult) @@ -62,7 +64,7 @@ class AuthenticationInteractorTest : SysuiTestCase() { assertThat(authMethod).isEqualTo(Pin) assertThat(underTest.getAuthenticationMethod()).isEqualTo(Pin) - utils.authenticationRepository.setAuthenticationMethod(Password) + kosmos.fakeAuthenticationRepository.setAuthenticationMethod(Password) assertThat(authMethod).isEqualTo(Password) assertThat(underTest.getAuthenticationMethod()).isEqualTo(Password) @@ -74,7 +76,7 @@ class AuthenticationInteractorTest : SysuiTestCase() { val authMethod by collectLastValue(underTest.authenticationMethod) runCurrent() - utils.authenticationRepository.setAuthenticationMethod(None) + kosmos.fakeAuthenticationRepository.setAuthenticationMethod(None) assertThat(authMethod).isEqualTo(None) assertThat(underTest.getAuthenticationMethod()).isEqualTo(None) @@ -83,7 +85,7 @@ class AuthenticationInteractorTest : SysuiTestCase() { @Test fun authenticate_withCorrectPin_succeeds() = testScope.runTest { - utils.authenticationRepository.setAuthenticationMethod(Pin) + kosmos.fakeAuthenticationRepository.setAuthenticationMethod(Pin) assertSucceeded(underTest.authenticate(FakeAuthenticationRepository.DEFAULT_PIN)) } @@ -91,7 +93,7 @@ class AuthenticationInteractorTest : SysuiTestCase() { @Test fun authenticate_withIncorrectPin_fails() = testScope.runTest { - utils.authenticationRepository.setAuthenticationMethod(Pin) + kosmos.fakeAuthenticationRepository.setAuthenticationMethod(Pin) assertFailed(underTest.authenticate(listOf(9, 8, 7, 6, 5, 4))) } @@ -99,7 +101,7 @@ class AuthenticationInteractorTest : SysuiTestCase() { @Test(expected = IllegalArgumentException::class) fun authenticate_withEmptyPin_throwsException() = testScope.runTest { - utils.authenticationRepository.setAuthenticationMethod(Pin) + kosmos.fakeAuthenticationRepository.setAuthenticationMethod(Pin) underTest.authenticate(listOf()) } @@ -107,7 +109,7 @@ class AuthenticationInteractorTest : SysuiTestCase() { fun authenticate_withCorrectMaxLengthPin_succeeds() = testScope.runTest { val correctMaxLengthPin = List(16) { 9 } - utils.authenticationRepository.apply { + kosmos.fakeAuthenticationRepository.apply { setAuthenticationMethod(Pin) overrideCredential(correctMaxLengthPin) } @@ -124,7 +126,7 @@ class AuthenticationInteractorTest : SysuiTestCase() { // If the policy changes, there is work to do in SysUI. assertThat(DevicePolicyManager.MAX_PASSWORD_LENGTH).isLessThan(17) - utils.authenticationRepository.setAuthenticationMethod(Pin) + kosmos.fakeAuthenticationRepository.setAuthenticationMethod(Pin) assertFailed(underTest.authenticate(List(17) { 9 })) } @@ -132,7 +134,7 @@ class AuthenticationInteractorTest : SysuiTestCase() { @Test fun authenticate_withCorrectPassword_succeeds() = testScope.runTest { - utils.authenticationRepository.setAuthenticationMethod(Password) + kosmos.fakeAuthenticationRepository.setAuthenticationMethod(Password) assertSucceeded(underTest.authenticate("password".toList())) } @@ -140,7 +142,7 @@ class AuthenticationInteractorTest : SysuiTestCase() { @Test fun authenticate_withIncorrectPassword_fails() = testScope.runTest { - utils.authenticationRepository.setAuthenticationMethod(Password) + kosmos.fakeAuthenticationRepository.setAuthenticationMethod(Password) assertFailed(underTest.authenticate("alohomora".toList())) } @@ -148,7 +150,7 @@ class AuthenticationInteractorTest : SysuiTestCase() { @Test fun authenticate_withCorrectPattern_succeeds() = testScope.runTest { - utils.authenticationRepository.setAuthenticationMethod(Pattern) + kosmos.fakeAuthenticationRepository.setAuthenticationMethod(Pattern) assertSucceeded(underTest.authenticate(FakeAuthenticationRepository.PATTERN)) } @@ -156,7 +158,7 @@ class AuthenticationInteractorTest : SysuiTestCase() { @Test fun authenticate_withIncorrectPattern_fails() = testScope.runTest { - utils.authenticationRepository.setAuthenticationMethod(Pattern) + kosmos.fakeAuthenticationRepository.setAuthenticationMethod(Pattern) val wrongPattern = listOf( AuthenticationPatternCoordinate(x = 2, y = 0), @@ -172,7 +174,7 @@ class AuthenticationInteractorTest : SysuiTestCase() { fun tryAutoConfirm_withAutoConfirmPinAndShorterPin_returnsNull() = testScope.runTest { val isAutoConfirmEnabled by collectLastValue(underTest.isAutoConfirmEnabled) - utils.authenticationRepository.apply { + kosmos.fakeAuthenticationRepository.apply { setAuthenticationMethod(Pin) setAutoConfirmFeatureEnabled(true) } @@ -182,14 +184,14 @@ class AuthenticationInteractorTest : SysuiTestCase() { assertSkipped(underTest.authenticate(shorterPin, tryAutoConfirm = true)) assertThat(underTest.lockoutEndTimestamp).isNull() - assertThat(utils.authenticationRepository.lockoutStartedReportCount).isEqualTo(0) + assertThat(kosmos.fakeAuthenticationRepository.lockoutStartedReportCount).isEqualTo(0) } @Test fun tryAutoConfirm_withAutoConfirmWrongPinCorrectLength_returnsFalse() = testScope.runTest { val isAutoConfirmEnabled by collectLastValue(underTest.isAutoConfirmEnabled) - utils.authenticationRepository.apply { + kosmos.fakeAuthenticationRepository.apply { setAuthenticationMethod(Pin) setAutoConfirmFeatureEnabled(true) } @@ -207,7 +209,7 @@ class AuthenticationInteractorTest : SysuiTestCase() { fun tryAutoConfirm_withAutoConfirmLongerPin_returnsFalse() = testScope.runTest { val isAutoConfirmEnabled by collectLastValue(underTest.isAutoConfirmEnabled) - utils.authenticationRepository.apply { + kosmos.fakeAuthenticationRepository.apply { setAuthenticationMethod(Pin) setAutoConfirmFeatureEnabled(true) } @@ -225,7 +227,7 @@ class AuthenticationInteractorTest : SysuiTestCase() { fun tryAutoConfirm_withAutoConfirmCorrectPin_returnsTrue() = testScope.runTest { val isAutoConfirmEnabled by collectLastValue(underTest.isAutoConfirmEnabled) - utils.authenticationRepository.apply { + kosmos.fakeAuthenticationRepository.apply { setAuthenticationMethod(Pin) setAutoConfirmFeatureEnabled(true) } @@ -241,7 +243,7 @@ class AuthenticationInteractorTest : SysuiTestCase() { testScope.runTest { val isAutoConfirmEnabled by collectLastValue(underTest.isAutoConfirmEnabled) val hintedPinLength by collectLastValue(underTest.hintedPinLength) - utils.authenticationRepository.apply { + kosmos.fakeAuthenticationRepository.apply { setAuthenticationMethod(Pin) setAutoConfirmFeatureEnabled(true) reportLockoutStarted(42) @@ -258,7 +260,7 @@ class AuthenticationInteractorTest : SysuiTestCase() { @Test fun tryAutoConfirm_withoutAutoConfirmButCorrectPin_returnsNull() = testScope.runTest { - utils.authenticationRepository.apply { + kosmos.fakeAuthenticationRepository.apply { setAuthenticationMethod(Pin) setAutoConfirmFeatureEnabled(false) } @@ -271,7 +273,7 @@ class AuthenticationInteractorTest : SysuiTestCase() { @Test fun tryAutoConfirm_withoutCorrectPassword_returnsNull() = testScope.runTest { - utils.authenticationRepository.setAuthenticationMethod(Password) + kosmos.fakeAuthenticationRepository.setAuthenticationMethod(Password) assertSkipped(underTest.authenticate("password".toList(), tryAutoConfirm = true)) } @@ -280,7 +282,7 @@ class AuthenticationInteractorTest : SysuiTestCase() { fun isAutoConfirmEnabled_featureDisabled_returnsFalse() = testScope.runTest { val isAutoConfirmEnabled by collectLastValue(underTest.isAutoConfirmEnabled) - utils.authenticationRepository.setAutoConfirmFeatureEnabled(false) + kosmos.fakeAuthenticationRepository.setAutoConfirmFeatureEnabled(false) assertThat(isAutoConfirmEnabled).isFalse() } @@ -289,7 +291,7 @@ class AuthenticationInteractorTest : SysuiTestCase() { fun isAutoConfirmEnabled_featureEnabled_returnsTrue() = testScope.runTest { val isAutoConfirmEnabled by collectLastValue(underTest.isAutoConfirmEnabled) - utils.authenticationRepository.setAutoConfirmFeatureEnabled(true) + kosmos.fakeAuthenticationRepository.setAutoConfirmFeatureEnabled(true) assertThat(isAutoConfirmEnabled).isTrue() } @@ -298,7 +300,7 @@ class AuthenticationInteractorTest : SysuiTestCase() { fun isAutoConfirmEnabled_featureEnabledButDisabledByLockout() = testScope.runTest { val isAutoConfirmEnabled by collectLastValue(underTest.isAutoConfirmEnabled) - utils.authenticationRepository.setAutoConfirmFeatureEnabled(true) + kosmos.fakeAuthenticationRepository.setAutoConfirmFeatureEnabled(true) // The feature is enabled. assertThat(isAutoConfirmEnabled).isTrue() @@ -308,7 +310,7 @@ class AuthenticationInteractorTest : SysuiTestCase() { assertFailed(underTest.authenticate(listOf(5, 6, 7))) // Wrong PIN } assertThat(underTest.lockoutEndTimestamp).isNotNull() - assertThat(utils.authenticationRepository.lockoutStartedReportCount).isEqualTo(1) + assertThat(kosmos.fakeAuthenticationRepository.lockoutStartedReportCount).isEqualTo(1) // Lockout disabled auto-confirm. assertThat(isAutoConfirmEnabled).isFalse() @@ -336,7 +338,7 @@ class AuthenticationInteractorTest : SysuiTestCase() { val failedAuthenticationAttempts by collectLastValue(underTest.failedAuthenticationAttempts) - utils.authenticationRepository.setAuthenticationMethod(Pin) + kosmos.fakeAuthenticationRepository.setAuthenticationMethod(Pin) val correctPin = FakeAuthenticationRepository.DEFAULT_PIN assertSucceeded(underTest.authenticate(correctPin)) @@ -366,7 +368,7 @@ class AuthenticationInteractorTest : SysuiTestCase() { @Test fun lockoutEndTimestamp() = testScope.runTest { - utils.authenticationRepository.setAuthenticationMethod(Pin) + kosmos.fakeAuthenticationRepository.setAuthenticationMethod(Pin) val correctPin = FakeAuthenticationRepository.DEFAULT_PIN underTest.authenticate(correctPin) @@ -384,7 +386,7 @@ class AuthenticationInteractorTest : SysuiTestCase() { val expectedLockoutEndTimestamp = testScope.currentTime + FakeAuthenticationRepository.LOCKOUT_DURATION_MS assertThat(underTest.lockoutEndTimestamp).isEqualTo(expectedLockoutEndTimestamp) - assertThat(utils.authenticationRepository.lockoutStartedReportCount).isEqualTo(1) + assertThat(kosmos.fakeAuthenticationRepository.lockoutStartedReportCount).isEqualTo(1) // Correct PIN, but locked out, so doesn't attempt it: assertSkipped(underTest.authenticate(correctPin), assertNoResultEvents = false) @@ -409,7 +411,7 @@ class AuthenticationInteractorTest : SysuiTestCase() { fun upcomingWipe() = testScope.runTest { val upcomingWipe by collectLastValue(underTest.upcomingWipe) - utils.authenticationRepository.setAuthenticationMethod(Pin) + kosmos.fakeAuthenticationRepository.setAuthenticationMethod(Pin) val correctPin = FakeAuthenticationRepository.DEFAULT_PIN val wrongPin = FakeAuthenticationRepository.DEFAULT_PIN.map { it + 1 } @@ -418,7 +420,7 @@ class AuthenticationInteractorTest : SysuiTestCase() { var expectedFailedAttempts = 0 var remainingFailedAttempts = - utils.authenticationRepository.getMaxFailedUnlockAttemptsForWipe() + kosmos.fakeAuthenticationRepository.getMaxFailedUnlockAttemptsForWipe() assertThat(remainingFailedAttempts) .isGreaterThan(LockPatternUtils.FAILED_ATTEMPTS_BEFORE_WIPE_GRACE) @@ -458,7 +460,7 @@ class AuthenticationInteractorTest : SysuiTestCase() { fun hintedPinLength_withoutAutoConfirm_isNull() = testScope.runTest { val hintedPinLength by collectLastValue(underTest.hintedPinLength) - utils.authenticationRepository.apply { + kosmos.fakeAuthenticationRepository.apply { setAuthenticationMethod(Pin) setAutoConfirmFeatureEnabled(false) } @@ -470,11 +472,13 @@ class AuthenticationInteractorTest : SysuiTestCase() { fun hintedPinLength_withAutoConfirmPinTooShort_isNull() = testScope.runTest { val hintedPinLength by collectLastValue(underTest.hintedPinLength) - utils.authenticationRepository.apply { + kosmos.fakeAuthenticationRepository.apply { setAuthenticationMethod(Pin) overrideCredential( buildList { - repeat(utils.authenticationRepository.hintedPinLength - 1) { add(it + 1) } + repeat(kosmos.fakeAuthenticationRepository.hintedPinLength - 1) { + add(it + 1) + } } ) setAutoConfirmFeatureEnabled(true) @@ -487,28 +491,31 @@ class AuthenticationInteractorTest : SysuiTestCase() { fun hintedPinLength_withAutoConfirmPinAtRightLength_isSameLength() = testScope.runTest { val hintedPinLength by collectLastValue(underTest.hintedPinLength) - utils.authenticationRepository.apply { + kosmos.fakeAuthenticationRepository.apply { setAuthenticationMethod(Pin) setAutoConfirmFeatureEnabled(true) overrideCredential( buildList { - repeat(utils.authenticationRepository.hintedPinLength) { add(it + 1) } + repeat(kosmos.fakeAuthenticationRepository.hintedPinLength) { add(it + 1) } } ) } - assertThat(hintedPinLength).isEqualTo(utils.authenticationRepository.hintedPinLength) + assertThat(hintedPinLength) + .isEqualTo(kosmos.fakeAuthenticationRepository.hintedPinLength) } @Test fun hintedPinLength_withAutoConfirmPinTooLong_isNull() = testScope.runTest { val hintedPinLength by collectLastValue(underTest.hintedPinLength) - utils.authenticationRepository.apply { + kosmos.fakeAuthenticationRepository.apply { setAuthenticationMethod(Pin) overrideCredential( buildList { - repeat(utils.authenticationRepository.hintedPinLength + 1) { add(it + 1) } + repeat(kosmos.fakeAuthenticationRepository.hintedPinLength + 1) { + add(it + 1) + } } ) setAutoConfirmFeatureEnabled(true) @@ -520,10 +527,10 @@ class AuthenticationInteractorTest : SysuiTestCase() { @Test fun authenticate_withTooShortPassword() = testScope.runTest { - utils.authenticationRepository.setAuthenticationMethod(Password) + kosmos.fakeAuthenticationRepository.setAuthenticationMethod(Password) val tooShortPassword = buildList { - repeat(utils.authenticationRepository.minPasswordLength - 1) { time -> + repeat(kosmos.fakeAuthenticationRepository.minPasswordLength - 1) { time -> add("$time") } } @@ -534,7 +541,7 @@ class AuthenticationInteractorTest : SysuiTestCase() { assertThat(authenticationResult).isEqualTo(AuthenticationResult.SUCCEEDED) assertThat(onAuthenticationResult).isTrue() assertThat(underTest.lockoutEndTimestamp).isNull() - assertThat(utils.authenticationRepository.lockoutStartedReportCount).isEqualTo(0) + assertThat(kosmos.fakeAuthenticationRepository.lockoutStartedReportCount).isEqualTo(0) assertThat(failedAuthenticationAttempts).isEqualTo(0) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/BiometricTestExtensions.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/BiometricTestExtensions.kt index 4a39799fd64f..72e884e9e5d6 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/BiometricTestExtensions.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/BiometricTestExtensions.kt @@ -16,6 +16,7 @@ package com.android.systemui.biometrics +import android.graphics.Bitmap import android.hardware.biometrics.BiometricManager.Authenticators import android.hardware.biometrics.ComponentInfoInternal import android.hardware.biometrics.PromptContentView @@ -117,6 +118,8 @@ internal fun Collection<SensorPropertiesInternal?>.extractAuthenticatorTypes(): } internal fun promptInfo( + logoRes: Int = -1, + logoBitmap: Bitmap? = null, title: String = "title", subtitle: String = "sub", description: String = "desc", @@ -127,6 +130,8 @@ internal fun promptInfo( negativeButton: String = "neg", ): PromptInfo { val info = PromptInfo() + info.logoRes = logoRes + info.logoBitmap = logoBitmap info.title = title info.subtitle = subtitle info.description = description diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt index b0beab932e21..f7743e2814f0 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt @@ -38,6 +38,7 @@ import com.android.keyguard.KeyguardUpdateMonitor import com.android.systemui.Flags import com.android.systemui.SysuiTestCase import com.android.systemui.animation.ActivityLaunchAnimator +import com.android.systemui.biometrics.domain.interactor.UdfpsOverlayInteractor import com.android.systemui.biometrics.shared.model.UdfpsOverlayParams import com.android.systemui.biometrics.ui.viewmodel.DefaultUdfpsTouchOverlayViewModel import com.android.systemui.biometrics.ui.viewmodel.DeviceEntryUdfpsTouchOverlayViewModel @@ -118,6 +119,7 @@ class UdfpsControllerOverlayTest : SysuiTestCase() { @Mock private lateinit var keyguardTransitionInteractor: KeyguardTransitionInteractor @Mock private lateinit var shadeInteractor: ShadeInteractor @Captor private lateinit var layoutParamsCaptor: ArgumentCaptor<WindowManager.LayoutParams> + @Mock private lateinit var udfpsOverlayInteractor: UdfpsOverlayInteractor private val onTouch = { _: View, _: MotionEvent, _: Boolean -> true } private var overlayParams: UdfpsOverlayParams = UdfpsOverlayParams() @@ -174,7 +176,8 @@ class UdfpsControllerOverlayTest : SysuiTestCase() { mSelectedUserInteractor, { deviceEntryUdfpsTouchOverlayViewModel }, { defaultUdfpsTouchOverlayViewModel }, - shadeInteractor + shadeInteractor, + udfpsOverlayInteractor, ) block() } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsControllerTest.java index a59a4b864ac4..90c3c14bbc4f 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsControllerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsControllerTest.java @@ -76,6 +76,7 @@ import com.android.keyguard.KeyguardUpdateMonitor; import com.android.systemui.Flags; import com.android.systemui.SysuiTestCase; import com.android.systemui.animation.ActivityLaunchAnimator; +import com.android.systemui.biometrics.domain.interactor.UdfpsOverlayInteractor; import com.android.systemui.biometrics.shared.model.UdfpsOverlayParams; import com.android.systemui.biometrics.udfps.InteractionEvent; import com.android.systemui.biometrics.udfps.NormalizedTouchData; @@ -214,6 +215,8 @@ public class UdfpsControllerTest extends SysuiTestCase { @Mock private AlternateBouncerInteractor mAlternateBouncerInteractor; @Mock + private UdfpsOverlayInteractor mUdfpsOverlayInteractor; + @Mock private UdfpsKeyguardAccessibilityDelegate mUdfpsKeyguardAccessibilityDelegate; @Mock private SelectedUserInteractor mSelectedUserInteractor; @@ -342,8 +345,8 @@ public class UdfpsControllerTest extends SysuiTestCase { mFpsUnlockTracker, mKeyguardTransitionInteractor, mDeviceEntryUdfpsTouchOverlayViewModel, - mDefaultUdfpsTouchOverlayViewModel - + mDefaultUdfpsTouchOverlayViewModel, + mUdfpsOverlayInteractor ); verify(mFingerprintManager).setUdfpsOverlayController(mOverlayCaptor.capture()); mOverlayController = mOverlayCaptor.getValue(); diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsKeyguardViewLegacyControllerBaseTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsKeyguardViewLegacyControllerBaseTest.java index 13b53a896b70..7d9c2f96ef58 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsKeyguardViewLegacyControllerBaseTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsKeyguardViewLegacyControllerBaseTest.java @@ -27,6 +27,7 @@ import com.android.keyguard.KeyguardUpdateMonitor; import com.android.systemui.Flags; import com.android.systemui.SysuiTestCase; import com.android.systemui.animation.ActivityLaunchAnimator; +import com.android.systemui.biometrics.domain.interactor.UdfpsOverlayInteractor; import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor; import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor; import com.android.systemui.dump.DumpManager; @@ -76,6 +77,7 @@ public class UdfpsKeyguardViewLegacyControllerBaseTest extends SysuiTestCase { protected @Mock UdfpsKeyguardAccessibilityDelegate mUdfpsKeyguardAccessibilityDelegate; protected @Mock SelectedUserInteractor mSelectedUserInteractor; protected @Mock KeyguardTransitionInteractor mKeyguardTransitionInteractor; + protected @Mock UdfpsOverlayInteractor mUdfpsOverlayInteractor; protected FakeFeatureFlags mFeatureFlags = new FakeFeatureFlags(); @@ -152,7 +154,8 @@ public class UdfpsKeyguardViewLegacyControllerBaseTest extends SysuiTestCase { mUdfpsKeyguardAccessibilityDelegate, mSelectedUserInteractor, mKeyguardTransitionInteractor, - mShadeInteractor); + mShadeInteractor, + mUdfpsOverlayInteractor); return controller; } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsKeyguardViewLegacyControllerWithCoroutinesTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsKeyguardViewLegacyControllerWithCoroutinesTest.kt index 335ac9d42e77..0e257bcfecc3 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsKeyguardViewLegacyControllerWithCoroutinesTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsKeyguardViewLegacyControllerWithCoroutinesTest.kt @@ -24,6 +24,7 @@ import com.android.keyguard.KeyguardSecurityModel import com.android.systemui.biometrics.UdfpsKeyguardViewLegacy.ANIMATE_APPEAR_ON_SCREEN_OFF import com.android.systemui.biometrics.UdfpsKeyguardViewLegacy.ANIMATION_BETWEEN_AOD_AND_LOCKSCREEN import com.android.systemui.biometrics.data.repository.FakeFingerprintPropertyRepository +import com.android.systemui.biometrics.domain.interactor.UdfpsOverlayInteractor import com.android.systemui.bouncer.data.repository.KeyguardBouncerRepository import com.android.systemui.bouncer.data.repository.KeyguardBouncerRepositoryImpl import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor @@ -32,6 +33,7 @@ import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor import com.android.systemui.bouncer.shared.constants.KeyguardBouncerConstants import com.android.systemui.bouncer.ui.BouncerView import com.android.systemui.classifier.FalsingCollector +import com.android.systemui.coroutines.collectLastValue import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFaceAuthInteractor import com.android.systemui.keyguard.DismissCallbackRegistry import com.android.systemui.keyguard.data.repository.BiometricSettingsRepository @@ -45,9 +47,11 @@ import com.android.systemui.log.table.TableLogBuffer import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.statusbar.StatusBarState import com.android.systemui.statusbar.policy.KeyguardStateController +import com.android.systemui.user.domain.interactor.SelectedUserInteractor import com.android.systemui.util.mockito.whenever import com.android.systemui.util.time.FakeSystemClock import com.android.systemui.util.time.SystemClock +import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runCurrent @@ -130,6 +134,13 @@ class UdfpsKeyguardViewLegacyControllerWithCoroutinesTest : repository = transitionRepository, ) .keyguardTransitionInteractor + mUdfpsOverlayInteractor = + UdfpsOverlayInteractor( + context, + mock(AuthController::class.java), + mock(SelectedUserInteractor::class.java), + testScope.backgroundScope, + ) return createUdfpsKeyguardViewController(/* useModernBouncer */ true) } @@ -239,6 +250,31 @@ class UdfpsKeyguardViewLegacyControllerWithCoroutinesTest : } @Test + fun shouldHandleTouchesChange() = + testScope.runTest { + val shouldHandleTouches by collectLastValue(mUdfpsOverlayInteractor.shouldHandleTouches) + + // GIVEN view is attached + on the keyguard + mController.onViewAttached() + captureStatusBarStateListeners() + sendStatusBarStateChanged(StatusBarState.KEYGUARD) + whenever(mView.setPauseAuth(true)).thenReturn(true) + whenever(mView.unpausedAlpha).thenReturn(0) + + // WHEN panelViewExpansion changes to expanded + val job = mController.listenForBouncerExpansion(this) + keyguardBouncerRepository.setPrimaryShow(true) + keyguardBouncerRepository.setPanelExpansion(KeyguardBouncerConstants.EXPANSION_VISIBLE) + runCurrent() + + // THEN UDFPS auth is paused and should not handle touches + assertThat(mController.shouldPauseAuth()).isTrue() + assertThat(shouldHandleTouches!!).isFalse() + + job.cancel() + } + + @Test fun fadeFromDialogSuggestedAlpha() = testScope.runTest { // GIVEN view is attached and status bar expansion is 1f diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/data/repository/EmergencyServicesRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/data/repository/EmergencyServicesRepositoryImplTest.kt index c2117ae5bda4..a67b0931f171 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/data/repository/EmergencyServicesRepositoryImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/data/repository/EmergencyServicesRepositoryImplTest.kt @@ -20,8 +20,11 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.internal.R import com.android.systemui.SysuiTestCase +import com.android.systemui.common.ui.data.repository.configurationRepository +import com.android.systemui.common.ui.data.repository.fakeConfigurationRepository import com.android.systemui.coroutines.collectLastValue -import com.android.systemui.scene.SceneTestUtils +import com.android.systemui.kosmos.testScope +import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.TestScope @@ -36,8 +39,8 @@ import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class EmergencyServicesRepositoryImplTest : SysuiTestCase() { - private val utils = SceneTestUtils(this) - private val testScope = utils.testScope + private val kosmos = testKosmos() + private val testScope = kosmos.testScope private lateinit var underTest: EmergencyServicesRepository @@ -52,7 +55,7 @@ class EmergencyServicesRepositoryImplTest : SysuiTestCase() { EmergencyServicesRepository( resources = context.resources, applicationScope = testScope.backgroundScope, - configurationRepository = utils.configurationRepository, + configurationRepository = kosmos.configurationRepository, ) } @@ -71,7 +74,7 @@ class EmergencyServicesRepositoryImplTest : SysuiTestCase() { private fun TestScope.setEmergencyCallWhileSimLocked(isEnabled: Boolean) { overrideResource(R.bool.config_enable_emergency_call_while_sim_locked, isEnabled) - utils.configurationRepository.onConfigurationChange() + kosmos.fakeConfigurationRepository.onConfigurationChange() runCurrent() } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/BouncerActionButtonInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/BouncerActionButtonInteractorTest.kt index 67ce86b4e137..741cde82354a 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/BouncerActionButtonInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/BouncerActionButtonInteractorTest.kt @@ -16,27 +16,32 @@ package com.android.systemui.bouncer.domain.interactor -import android.app.ActivityTaskManager import android.telecom.TelecomManager import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest +import com.android.app.activityTaskManager import com.android.internal.R +import com.android.internal.logging.fakeMetricsLogger import com.android.internal.logging.nano.MetricsProto -import com.android.internal.logging.testing.FakeMetricsLogger -import com.android.internal.util.EmergencyAffordanceManager +import com.android.internal.util.emergencyAffordanceManager import com.android.systemui.SysuiTestCase +import com.android.systemui.authentication.data.repository.fakeAuthenticationRepository import com.android.systemui.authentication.shared.model.AuthenticationMethodModel +import com.android.systemui.common.ui.data.repository.fakeConfigurationRepository import com.android.systemui.coroutines.collectLastValue import com.android.systemui.flags.Flags.REFACTOR_GETCURRENTUSER -import com.android.systemui.log.table.TableLogBuffer -import com.android.systemui.scene.SceneTestUtils +import com.android.systemui.flags.fakeFeatureFlagsClassic +import com.android.systemui.kosmos.testScope +import com.android.systemui.scene.shared.flag.fakeSceneContainerFlags import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionsRepository -import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy +import com.android.systemui.statusbar.pipeline.mobile.data.repository.fakeMobileConnectionsRepository +import com.android.systemui.telephony.data.repository.fakeTelephonyRepository +import com.android.systemui.testKosmos import com.android.systemui.user.domain.interactor.SelectedUserInteractor import com.android.systemui.util.mockito.whenever +import com.android.telecom.telecomManager import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Before @@ -53,27 +58,26 @@ import org.mockito.MockitoAnnotations @RunWith(AndroidJUnit4::class) class BouncerActionButtonInteractorTest : SysuiTestCase() { - @Mock private lateinit var activityTaskManager: ActivityTaskManager - @Mock private lateinit var emergencyAffordanceManager: EmergencyAffordanceManager @Mock private lateinit var selectedUserInteractor: SelectedUserInteractor - @Mock private lateinit var tableLogger: TableLogBuffer @Mock private lateinit var telecomManager: TelecomManager - private lateinit var utils: SceneTestUtils - private lateinit var testScope: TestScope + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + private val metricsLogger = kosmos.fakeMetricsLogger + private val activityTaskManager = kosmos.activityTaskManager + private val emergencyAffordanceManager = kosmos.emergencyAffordanceManager + private lateinit var mobileConnectionsRepository: FakeMobileConnectionsRepository - private val metricsLogger = FakeMetricsLogger() private var currentUserId: Int = 0 private var needsEmergencyAffordance = true - private lateinit var underTest: BouncerActionButtonInteractor - @Before fun setUp() { - utils = SceneTestUtils(this) - testScope = utils.testScope MockitoAnnotations.initMocks(this) + kosmos.fakeSceneContainerFlags.enabled = true + + mobileConnectionsRepository = kosmos.fakeMobileConnectionsRepository overrideResource(R.string.lockscreen_emergency_call, MESSAGE_EMERGENCY_CALL) overrideResource(R.string.lockscreen_return_to_call, MESSAGE_RETURN_TO_CALL) @@ -86,34 +90,18 @@ class BouncerActionButtonInteractorTest : SysuiTestCase() { .thenReturn(needsEmergencyAffordance) whenever(telecomManager.isInCall).thenReturn(false) - utils.featureFlags.set(REFACTOR_GETCURRENTUSER, true) - - mobileConnectionsRepository = - FakeMobileConnectionsRepository(FakeMobileMappingsProxy(), tableLogger) + kosmos.fakeFeatureFlagsClassic.set(REFACTOR_GETCURRENTUSER, true) - utils.telephonyRepository.setHasTelephonyRadio(true) + kosmos.fakeTelephonyRepository.setHasTelephonyRadio(true) - underTest = - utils.bouncerActionButtonInteractor( - mobileConnectionsRepository = mobileConnectionsRepository, - activityTaskManager = activityTaskManager, - telecomManager = telecomManager, - emergencyAffordanceManager = emergencyAffordanceManager, - metricsLogger = metricsLogger, - ) + kosmos.telecomManager = telecomManager } @Test fun noTelephonyRadio_noButton() = testScope.runTest { - utils.telephonyRepository.setHasTelephonyRadio(false) - underTest = - utils.bouncerActionButtonInteractor( - mobileConnectionsRepository = mobileConnectionsRepository, - activityTaskManager = activityTaskManager, - telecomManager = telecomManager, - ) - + kosmos.fakeTelephonyRepository.setHasTelephonyRadio(false) + val underTest = kosmos.bouncerActionButtonInteractor val actionButton by collectLastValue(underTest.actionButton) assertThat(actionButton).isNull() } @@ -121,12 +109,8 @@ class BouncerActionButtonInteractorTest : SysuiTestCase() { @Test fun noTelecomManager_noButton() = testScope.runTest { - underTest = - utils.bouncerActionButtonInteractor( - mobileConnectionsRepository = mobileConnectionsRepository, - activityTaskManager = activityTaskManager, - telecomManager = null, - ) + kosmos.telecomManager = null + val underTest = kosmos.bouncerActionButtonInteractor val actionButton by collectLastValue(underTest.actionButton) assertThat(actionButton).isNull() } @@ -134,8 +118,9 @@ class BouncerActionButtonInteractorTest : SysuiTestCase() { @Test fun duringCall_returnToCallButton() = testScope.runTest { + val underTest = kosmos.bouncerActionButtonInteractor val actionButton by collectLastValue(underTest.actionButton) - utils.telephonyRepository.setIsInCall(true) + kosmos.fakeTelephonyRepository.setIsInCall(true) assertThat(actionButton).isNotNull() assertThat(actionButton?.label).isEqualTo(MESSAGE_RETURN_TO_CALL) @@ -143,6 +128,7 @@ class BouncerActionButtonInteractorTest : SysuiTestCase() { assertThat(actionButton?.onLongClick).isNull() actionButton?.onClick?.invoke() + runCurrent() assertThat(metricsLogger.logs.size).isEqualTo(1) assertThat(metricsLogger.logs.element().category) @@ -154,10 +140,13 @@ class BouncerActionButtonInteractorTest : SysuiTestCase() { @Test fun noCall_secureAuthMethod_emergencyCallButton() = testScope.runTest { + val underTest = kosmos.bouncerActionButtonInteractor val actionButton by collectLastValue(underTest.actionButton) mobileConnectionsRepository.isAnySimSecure.value = false - utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin) - utils.telephonyRepository.setIsInCall(false) + kosmos.fakeAuthenticationRepository.setAuthenticationMethod( + AuthenticationMethodModel.Pin + ) + kosmos.fakeTelephonyRepository.setIsInCall(false) assertThat(actionButton).isNotNull() assertThat(actionButton?.label).isEqualTo(MESSAGE_EMERGENCY_CALL) @@ -165,6 +154,7 @@ class BouncerActionButtonInteractorTest : SysuiTestCase() { assertThat(actionButton?.onLongClick).isNotNull() actionButton?.onClick?.invoke() + runCurrent() assertThat(metricsLogger.logs.size).isEqualTo(1) assertThat(metricsLogger.logs.element().category) @@ -182,10 +172,14 @@ class BouncerActionButtonInteractorTest : SysuiTestCase() { @Test fun noCall_insecureAuthMethodButSecureSim_emergencyCallButton() = testScope.runTest { + val underTest = kosmos.bouncerActionButtonInteractor val actionButton by collectLastValue(underTest.actionButton) mobileConnectionsRepository.isAnySimSecure.value = true - utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.None) - utils.telephonyRepository.setIsInCall(false) + kosmos.fakeAuthenticationRepository.setAuthenticationMethod( + AuthenticationMethodModel.None + ) + kosmos.fakeTelephonyRepository.setIsInCall(false) + runCurrent() assertThat(actionButton).isNotNull() assertThat(actionButton?.label).isEqualTo(MESSAGE_EMERGENCY_CALL) @@ -196,10 +190,13 @@ class BouncerActionButtonInteractorTest : SysuiTestCase() { @Test fun noCall_insecure_noButton() = testScope.runTest { + val underTest = kosmos.bouncerActionButtonInteractor val actionButton by collectLastValue(underTest.actionButton) mobileConnectionsRepository.isAnySimSecure.value = false - utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.None) - utils.telephonyRepository.setIsInCall(false) + kosmos.fakeAuthenticationRepository.setAuthenticationMethod( + AuthenticationMethodModel.None + ) + kosmos.fakeTelephonyRepository.setIsInCall(false) assertThat(actionButton).isNull() } @@ -207,12 +204,15 @@ class BouncerActionButtonInteractorTest : SysuiTestCase() { @Test fun noCall_simSecureButEmergencyNotSupported_noButton() = testScope.runTest { + val underTest = kosmos.bouncerActionButtonInteractor val actionButton by collectLastValue(underTest.actionButton) mobileConnectionsRepository.isAnySimSecure.value = true overrideResource(R.bool.config_enable_emergency_call_while_sim_locked, false) - utils.configurationRepository.onConfigurationChange() - utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.None) - utils.telephonyRepository.setIsInCall(false) + kosmos.fakeConfigurationRepository.onConfigurationChange() + kosmos.fakeAuthenticationRepository.setAuthenticationMethod( + AuthenticationMethodModel.None + ) + kosmos.fakeTelephonyRepository.setIsInCall(false) runCurrent() assertThat(actionButton).isNull() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorTest.kt index 93ba6a48f3dd..707777b9f728 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorTest.kt @@ -20,14 +20,20 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.authentication.data.repository.FakeAuthenticationRepository +import com.android.systemui.authentication.data.repository.fakeAuthenticationRepository import com.android.systemui.authentication.domain.interactor.AuthenticationResult +import com.android.systemui.authentication.domain.interactor.authenticationInteractor import com.android.systemui.authentication.shared.model.AuthenticationMethodModel import com.android.systemui.authentication.shared.model.AuthenticationPatternCoordinate import com.android.systemui.coroutines.collectLastValue import com.android.systemui.coroutines.collectValues -import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFaceAuthInteractor +import com.android.systemui.deviceentry.domain.interactor.deviceEntryFaceAuthInteractor +import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFaceAuthRepository +import com.android.systemui.kosmos.testScope +import com.android.systemui.power.data.repository.fakePowerRepository import com.android.systemui.res.R -import com.android.systemui.scene.SceneTestUtils +import com.android.systemui.scene.shared.flag.fakeSceneContainerFlags +import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -37,8 +43,6 @@ 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.verify import org.mockito.MockitoAnnotations @OptIn(ExperimentalCoroutinesApi::class) @@ -46,17 +50,16 @@ import org.mockito.MockitoAnnotations @RunWith(AndroidJUnit4::class) class BouncerInteractorTest : SysuiTestCase() { - @Mock private lateinit var mDeviceEntryFaceAuthInteractor: DeviceEntryFaceAuthInteractor - - private val utils = SceneTestUtils(this) - private val testScope = utils.testScope - private val authenticationInteractor = utils.authenticationInteractor() + private val kosmos = testKosmos().apply { fakeSceneContainerFlags.enabled = true } + private val testScope = kosmos.testScope + private val authenticationInteractor = kosmos.authenticationInteractor private lateinit var underTest: BouncerInteractor @Before fun setUp() { MockitoAnnotations.initMocks(this) + overrideResource(R.string.keyguard_enter_your_pin, MESSAGE_ENTER_YOUR_PIN) overrideResource(R.string.keyguard_enter_your_password, MESSAGE_ENTER_YOUR_PASSWORD) overrideResource(R.string.keyguard_enter_your_pattern, MESSAGE_ENTER_YOUR_PATTERN) @@ -64,11 +67,7 @@ class BouncerInteractorTest : SysuiTestCase() { overrideResource(R.string.kg_wrong_password, MESSAGE_WRONG_PASSWORD) overrideResource(R.string.kg_wrong_pattern, MESSAGE_WRONG_PATTERN) - underTest = - utils.bouncerInteractor( - authenticationInteractor = authenticationInteractor, - deviceEntryFaceAuthInteractor = mDeviceEntryFaceAuthInteractor, - ) + underTest = kosmos.bouncerInteractor } @Test @@ -76,7 +75,9 @@ class BouncerInteractorTest : SysuiTestCase() { testScope.runTest { val message by collectLastValue(underTest.message) - utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin) + kosmos.fakeAuthenticationRepository.setAuthenticationMethod( + AuthenticationMethodModel.Pin + ) runCurrent() underTest.clearMessage() assertThat(message).isNull() @@ -100,7 +101,9 @@ class BouncerInteractorTest : SysuiTestCase() { @Test fun pinAuthMethod_sim_skipsAuthentication() = testScope.runTest { - utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Sim) + kosmos.fakeAuthenticationRepository.setAuthenticationMethod( + AuthenticationMethodModel.Sim + ) runCurrent() // We rely on TelephonyManager to authenticate the sim card. @@ -115,9 +118,11 @@ class BouncerInteractorTest : SysuiTestCase() { testScope.runTest { val isAutoConfirmEnabled by collectLastValue(underTest.isAutoConfirmEnabled) - utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin) + kosmos.fakeAuthenticationRepository.setAuthenticationMethod( + AuthenticationMethodModel.Pin + ) runCurrent() - utils.authenticationRepository.setAutoConfirmFeatureEnabled(true) + kosmos.fakeAuthenticationRepository.setAutoConfirmFeatureEnabled(true) assertThat(isAutoConfirmEnabled).isTrue() // Incomplete input. @@ -143,7 +148,9 @@ class BouncerInteractorTest : SysuiTestCase() { testScope.runTest { val message by collectLastValue(underTest.message) - utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin) + kosmos.fakeAuthenticationRepository.setAuthenticationMethod( + AuthenticationMethodModel.Pin + ) runCurrent() // Incomplete input. @@ -166,7 +173,7 @@ class BouncerInteractorTest : SysuiTestCase() { fun passwordAuthMethod() = testScope.runTest { val message by collectLastValue(underTest.message) - utils.authenticationRepository.setAuthenticationMethod( + kosmos.fakeAuthenticationRepository.setAuthenticationMethod( AuthenticationMethodModel.Password ) runCurrent() @@ -186,7 +193,8 @@ class BouncerInteractorTest : SysuiTestCase() { assertThat( underTest.authenticate( buildList { - repeat(utils.authenticationRepository.minPasswordLength - 1) { time -> + repeat(kosmos.fakeAuthenticationRepository.minPasswordLength - 1) { time + -> add("$time") } } @@ -204,7 +212,7 @@ class BouncerInteractorTest : SysuiTestCase() { fun patternAuthMethod() = testScope.runTest { val message by collectLastValue(underTest.message) - utils.authenticationRepository.setAuthenticationMethod( + kosmos.fakeAuthenticationRepository.setAuthenticationMethod( AuthenticationMethodModel.Pattern ) runCurrent() @@ -220,7 +228,8 @@ class BouncerInteractorTest : SysuiTestCase() { AuthenticationPatternCoordinate(0, 1), ) assertThat(wrongPattern).isNotEqualTo(FakeAuthenticationRepository.PATTERN) - assertThat(wrongPattern.size).isAtLeast(utils.authenticationRepository.minPatternLength) + assertThat(wrongPattern.size) + .isAtLeast(kosmos.fakeAuthenticationRepository.minPatternLength) assertThat(underTest.authenticate(wrongPattern)).isEqualTo(AuthenticationResult.FAILED) assertThat(message).isEqualTo(MESSAGE_WRONG_PATTERN) @@ -231,7 +240,7 @@ class BouncerInteractorTest : SysuiTestCase() { val tooShortPattern = FakeAuthenticationRepository.PATTERN.subList( 0, - utils.authenticationRepository.minPatternLength - 1 + kosmos.fakeAuthenticationRepository.minPatternLength - 1 ) assertThat(underTest.authenticate(tooShortPattern)) .isEqualTo(AuthenticationResult.SKIPPED) @@ -251,7 +260,9 @@ class BouncerInteractorTest : SysuiTestCase() { val lockoutStartedEvents by collectValues(underTest.onLockoutStarted) val message by collectLastValue(underTest.message) - utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin) + kosmos.fakeAuthenticationRepository.setAuthenticationMethod( + AuthenticationMethodModel.Pin + ) assertThat(lockoutStartedEvents).isEmpty() // Try the wrong PIN repeatedly, until lockout is triggered: @@ -297,16 +308,24 @@ class BouncerInteractorTest : SysuiTestCase() { @Test fun intentionalUserInputEvent_registersTouchEvent() = testScope.runTest { - assertThat(utils.powerRepository.userTouchRegistered).isFalse() + assertThat(kosmos.fakePowerRepository.userTouchRegistered).isFalse() underTest.onIntentionalUserInput() - assertThat(utils.powerRepository.userTouchRegistered).isTrue() + assertThat(kosmos.fakePowerRepository.userTouchRegistered).isTrue() } @Test fun intentionalUserInputEvent_notifiesFaceAuthInteractor() = testScope.runTest { + val isFaceAuthRunning by + collectLastValue(kosmos.fakeDeviceEntryFaceAuthRepository.isAuthRunning) + kosmos.deviceEntryFaceAuthInteractor.onDeviceLifted() + runCurrent() + assertThat(isFaceAuthRunning).isTrue() + underTest.onIntentionalUserInput() - verify(mDeviceEntryFaceAuthInteractor).onPrimaryBouncerUserInput() + runCurrent() + + assertThat(isFaceAuthRunning).isFalse() } companion object { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/SimBouncerInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/SimBouncerInteractorTest.kt index 8c53c0e3f267..09fdd11a99dd 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/SimBouncerInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/SimBouncerInteractorTest.kt @@ -28,8 +28,11 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.bouncer.data.repository.FakeSimBouncerRepository import com.android.systemui.bouncer.domain.interactor.SimBouncerInteractor.Companion.INVALID_SUBSCRIPTION_ID import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.kosmos.testScope import com.android.systemui.res.R -import com.android.systemui.scene.SceneTestUtils +import com.android.systemui.statusbar.pipeline.mobile.data.repository.mobileConnectionsRepository +import com.android.systemui.testKosmos import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -54,10 +57,10 @@ class SimBouncerInteractorTest : SysuiTestCase() { @Mock lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor @Mock lateinit var euiccManager: EuiccManager - private val utils = SceneTestUtils(this) + private val kosmos = testKosmos() private val bouncerSimRepository = FakeSimBouncerRepository() private val resources: Resources = context.resources - private val testScope = utils.testScope + private val testScope = kosmos.testScope private lateinit var underTest: SimBouncerInteractor @@ -68,13 +71,13 @@ class SimBouncerInteractorTest : SysuiTestCase() { SimBouncerInteractor( context, testScope.backgroundScope, - utils.testDispatcher, + kosmos.testDispatcher, bouncerSimRepository, telephonyManager, resources, keyguardUpdateMonitor, euiccManager, - utils.mobileConnectionsRepository, + kosmos.mobileConnectionsRepository, ) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModelTest.kt index 2f0843b202a0..27b84b2ffabc 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModelTest.kt @@ -20,9 +20,13 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.authentication.data.repository.FakeAuthenticationRepository +import com.android.systemui.authentication.data.repository.fakeAuthenticationRepository import com.android.systemui.authentication.shared.model.AuthenticationMethodModel +import com.android.systemui.bouncer.domain.interactor.bouncerInteractor +import com.android.systemui.bouncer.domain.interactor.simBouncerInteractor import com.android.systemui.coroutines.collectLastValue -import com.android.systemui.scene.SceneTestUtils +import com.android.systemui.kosmos.testScope +import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.test.runTest @@ -33,19 +37,16 @@ import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class AuthMethodBouncerViewModelTest : SysuiTestCase() { - private val utils = SceneTestUtils(this) - private val testScope = utils.testScope - private val bouncerInteractor = - utils.bouncerInteractor( - authenticationInteractor = utils.authenticationInteractor(), - ) + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + private val bouncerInteractor = kosmos.bouncerInteractor private val underTest = PinBouncerViewModel( applicationContext = context, viewModelScope = testScope.backgroundScope, interactor = bouncerInteractor, isInputEnabled = MutableStateFlow(true), - simBouncerInteractor = utils.simBouncerInteractor, + simBouncerInteractor = kosmos.simBouncerInteractor, authenticationMethod = AuthenticationMethodModel.Pin, ) @@ -53,7 +54,9 @@ class AuthMethodBouncerViewModelTest : SysuiTestCase() { fun animateFailure() = testScope.runTest { val animateFailure by collectLastValue(underTest.animateFailure) - utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin) + kosmos.fakeAuthenticationRepository.setAuthenticationMethod( + AuthenticationMethodModel.Pin + ) assertThat(animateFailure).isFalse() // Wrong PIN: diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelTest.kt index 47bbe6f49a5e..cfe8c5d52c18 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelTest.kt @@ -20,15 +20,21 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.authentication.data.repository.FakeAuthenticationRepository +import com.android.systemui.authentication.data.repository.fakeAuthenticationRepository +import com.android.systemui.authentication.domain.interactor.authenticationInteractor import com.android.systemui.authentication.shared.model.AuthenticationMethodModel import com.android.systemui.authentication.shared.model.AuthenticationMethodModel.None import com.android.systemui.authentication.shared.model.AuthenticationMethodModel.Password import com.android.systemui.authentication.shared.model.AuthenticationMethodModel.Pattern import com.android.systemui.authentication.shared.model.AuthenticationMethodModel.Pin import com.android.systemui.authentication.shared.model.AuthenticationMethodModel.Sim +import com.android.systemui.bouncer.domain.interactor.bouncerInteractor import com.android.systemui.coroutines.collectLastValue import com.android.systemui.flags.Flags -import com.android.systemui.scene.SceneTestUtils +import com.android.systemui.flags.fakeFeatureFlagsClassic +import com.android.systemui.kosmos.testScope +import com.android.systemui.scene.shared.flag.fakeSceneContainerFlags +import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertWithMessage import kotlin.time.Duration.Companion.seconds @@ -41,6 +47,7 @@ import kotlinx.coroutines.test.advanceTimeBy import kotlinx.coroutines.test.currentTime import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest +import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -49,18 +56,17 @@ import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class BouncerViewModelTest : SysuiTestCase() { - private val utils = SceneTestUtils(this) - private val testScope = utils.testScope - private val authenticationInteractor = utils.authenticationInteractor() - private val bouncerInteractor = - utils.bouncerInteractor( - authenticationInteractor = authenticationInteractor, - ) - private val underTest = - utils.bouncerViewModel( - bouncerInteractor = bouncerInteractor, - authenticationInteractor = authenticationInteractor, - ) + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + private val authenticationInteractor = kosmos.authenticationInteractor + private val bouncerInteractor = kosmos.bouncerInteractor + private lateinit var underTest: BouncerViewModel + + @Before + fun setUp() { + kosmos.fakeSceneContainerFlags.enabled = true + underTest = kosmos.bouncerViewModel + } @Test fun authMethod_nonNullForSecureMethods_nullForNotSecureMethods() = @@ -68,7 +74,7 @@ class BouncerViewModelTest : SysuiTestCase() { var authMethodViewModel: AuthMethodBouncerViewModel? = null authMethodsToTest().forEach { authMethod -> - utils.authenticationRepository.setAuthenticationMethod(authMethod) + kosmos.fakeAuthenticationRepository.setAuthenticationMethod(authMethod) val job = underTest.authMethodViewModel.onEach { authMethodViewModel = it }.launchIn(this) runCurrent() @@ -98,13 +104,13 @@ class BouncerViewModelTest : SysuiTestCase() { // First pass, populate our "seen" map: authMethodsToTest().forEach { authMethod -> - utils.authenticationRepository.setAuthenticationMethod(authMethod) + kosmos.fakeAuthenticationRepository.setAuthenticationMethod(authMethod) authMethodViewModel?.let { seen[authMethod] = it } } // Second pass, assert same instances are not reused: authMethodsToTest().forEach { authMethod -> - utils.authenticationRepository.setAuthenticationMethod(authMethod) + kosmos.fakeAuthenticationRepository.setAuthenticationMethod(authMethod) authMethodViewModel?.let { assertThat(it.authenticationMethod).isEqualTo(authMethod) assertThat(it).isNotSameInstanceAs(seen[authMethod]) @@ -116,11 +122,11 @@ class BouncerViewModelTest : SysuiTestCase() { fun authMethodUnchanged_reusesInstances() = testScope.runTest { authMethodsToTest().forEach { authMethod -> - utils.authenticationRepository.setAuthenticationMethod(authMethod) + kosmos.fakeAuthenticationRepository.setAuthenticationMethod(authMethod) val firstInstance: AuthMethodBouncerViewModel? = collectLastValue(underTest.authMethodViewModel).invoke() - utils.authenticationRepository.setAuthenticationMethod(authMethod) + kosmos.fakeAuthenticationRepository.setAuthenticationMethod(authMethod) val secondInstance: AuthMethodBouncerViewModel? = collectLastValue(underTest.authMethodViewModel).invoke() @@ -139,7 +145,7 @@ class BouncerViewModelTest : SysuiTestCase() { fun message() = testScope.runTest { val message by collectLastValue(underTest.message) - utils.authenticationRepository.setAuthenticationMethod(Pin) + kosmos.fakeAuthenticationRepository.setAuthenticationMethod(Pin) assertThat(message?.isUpdateAnimated).isTrue() repeat(FakeAuthenticationRepository.MAX_FAILED_AUTH_TRIES_BEFORE_LOCKOUT) { @@ -157,8 +163,8 @@ class BouncerViewModelTest : SysuiTestCase() { testScope.runTest { val authMethodViewModel by collectLastValue(underTest.authMethodViewModel) val message by collectLastValue(underTest.message) - utils.authenticationRepository.setAuthenticationMethod(Pin) - assertThat(utils.authenticationRepository.lockoutEndTimestamp).isNull() + kosmos.fakeAuthenticationRepository.setAuthenticationMethod(Pin) + assertThat(kosmos.fakeAuthenticationRepository.lockoutEndTimestamp).isNull() assertThat(authMethodViewModel?.lockoutMessageId).isNotNull() repeat(FakeAuthenticationRepository.MAX_FAILED_AUTH_TRIES_BEFORE_LOCKOUT) { times -> @@ -192,7 +198,7 @@ class BouncerViewModelTest : SysuiTestCase() { authViewModel?.isInputEnabled ?: emptyFlow() } ) - utils.authenticationRepository.setAuthenticationMethod(Pin) + kosmos.fakeAuthenticationRepository.setAuthenticationMethod(Pin) assertThat(isInputEnabled).isTrue() repeat(FakeAuthenticationRepository.MAX_FAILED_AUTH_TRIES_BEFORE_LOCKOUT) { @@ -210,7 +216,7 @@ class BouncerViewModelTest : SysuiTestCase() { testScope.runTest { val authMethodViewModel by collectLastValue(underTest.authMethodViewModel) val dialogViewModel by collectLastValue(underTest.dialogViewModel) - utils.authenticationRepository.setAuthenticationMethod(Pin) + kosmos.fakeAuthenticationRepository.setAuthenticationMethod(Pin) assertThat(authMethodViewModel?.lockoutMessageId).isNotNull() repeat(FakeAuthenticationRepository.MAX_FAILED_AUTH_TRIES_BEFORE_LOCKOUT) { @@ -228,17 +234,17 @@ class BouncerViewModelTest : SysuiTestCase() { fun isSideBySideSupported() = testScope.runTest { val isSideBySideSupported by collectLastValue(underTest.isSideBySideSupported) - utils.featureFlags.set(Flags.FULL_SCREEN_USER_SWITCHER, true) - utils.authenticationRepository.setAuthenticationMethod(Pin) + kosmos.fakeFeatureFlagsClassic.set(Flags.FULL_SCREEN_USER_SWITCHER, true) + kosmos.fakeAuthenticationRepository.setAuthenticationMethod(Pin) assertThat(isSideBySideSupported).isTrue() - utils.authenticationRepository.setAuthenticationMethod(Password) + kosmos.fakeAuthenticationRepository.setAuthenticationMethod(Password) assertThat(isSideBySideSupported).isTrue() - utils.featureFlags.set(Flags.FULL_SCREEN_USER_SWITCHER, false) - utils.authenticationRepository.setAuthenticationMethod(Pin) + kosmos.fakeFeatureFlagsClassic.set(Flags.FULL_SCREEN_USER_SWITCHER, false) + kosmos.fakeAuthenticationRepository.setAuthenticationMethod(Pin) assertThat(isSideBySideSupported).isTrue() - utils.authenticationRepository.setAuthenticationMethod(Password) + kosmos.fakeAuthenticationRepository.setAuthenticationMethod(Password) assertThat(isSideBySideSupported).isFalse() } @@ -246,12 +252,12 @@ class BouncerViewModelTest : SysuiTestCase() { fun isFoldSplitRequired() = testScope.runTest { val isFoldSplitRequired by collectLastValue(underTest.isFoldSplitRequired) - utils.authenticationRepository.setAuthenticationMethod(Pin) + kosmos.fakeAuthenticationRepository.setAuthenticationMethod(Pin) assertThat(isFoldSplitRequired).isTrue() - utils.authenticationRepository.setAuthenticationMethod(Password) + kosmos.fakeAuthenticationRepository.setAuthenticationMethod(Password) assertThat(isFoldSplitRequired).isFalse() - utils.authenticationRepository.setAuthenticationMethod(Pattern) + kosmos.fakeAuthenticationRepository.setAuthenticationMethod(Pattern) assertThat(isFoldSplitRequired).isTrue() } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt index 64e6e5707d75..b3b6457b46e7 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt @@ -19,13 +19,19 @@ package com.android.systemui.bouncer.ui.viewmodel import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.authentication.data.repository.fakeAuthenticationRepository +import com.android.systemui.authentication.domain.interactor.authenticationInteractor import com.android.systemui.authentication.shared.model.AuthenticationMethodModel +import com.android.systemui.bouncer.domain.interactor.bouncerInteractor import com.android.systemui.coroutines.collectLastValue import com.android.systemui.coroutines.collectValues +import com.android.systemui.deviceentry.data.repository.fakeDeviceEntryRepository +import com.android.systemui.kosmos.testScope import com.android.systemui.res.R -import com.android.systemui.scene.SceneTestUtils +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.testKosmos import com.google.common.truth.Truth.assertThat import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -43,20 +49,12 @@ import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class PasswordBouncerViewModelTest : SysuiTestCase() { - private val utils = SceneTestUtils(this) - private val testScope = utils.testScope - private val authenticationInteractor = utils.authenticationInteractor() - private val sceneInteractor = utils.sceneInteractor() - private val bouncerInteractor = - utils.bouncerInteractor( - authenticationInteractor = authenticationInteractor, - ) - private val bouncerViewModel = - utils.bouncerViewModel( - bouncerInteractor = bouncerInteractor, - authenticationInteractor = authenticationInteractor, - actionButtonInteractor = utils.bouncerActionButtonInteractor(), - ) + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + private val authenticationInteractor = kosmos.authenticationInteractor + private val sceneInteractor = kosmos.sceneInteractor + private val bouncerInteractor = kosmos.bouncerInteractor + private val bouncerViewModel = kosmos.bouncerViewModel private val isInputEnabled = MutableStateFlow(true) private val underTest = @@ -148,10 +146,10 @@ class PasswordBouncerViewModelTest : SysuiTestCase() { testScope.runTest { val message by collectLastValue(bouncerViewModel.message) val password by collectLastValue(underTest.password) - utils.authenticationRepository.setAuthenticationMethod( + kosmos.fakeAuthenticationRepository.setAuthenticationMethod( AuthenticationMethodModel.Password ) - utils.deviceEntryRepository.setUnlocked(false) + kosmos.fakeDeviceEntryRepository.setUnlocked(false) switchToScene(SceneKey.Bouncer) // No input entered. @@ -317,8 +315,10 @@ class PasswordBouncerViewModelTest : SysuiTestCase() { } private fun TestScope.lockDeviceAndOpenPasswordBouncer() { - utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Password) - utils.deviceEntryRepository.setUnlocked(false) + kosmos.fakeAuthenticationRepository.setAuthenticationMethod( + AuthenticationMethodModel.Password + ) + kosmos.fakeDeviceEntryRepository.setUnlocked(false) switchToScene(SceneKey.Bouncer) } @@ -328,13 +328,13 @@ class PasswordBouncerViewModelTest : SysuiTestCase() { ) { if (isLockedOut) { repeat(failedAttemptCount) { - utils.authenticationRepository.reportAuthenticationAttempt(false) + kosmos.fakeAuthenticationRepository.reportAuthenticationAttempt(false) } - utils.authenticationRepository.reportLockoutStarted( + kosmos.fakeAuthenticationRepository.reportLockoutStarted( 30.seconds.inWholeMilliseconds.toInt() ) } else { - utils.authenticationRepository.reportAuthenticationAttempt(true) + kosmos.fakeAuthenticationRepository.reportAuthenticationAttempt(true) } isInputEnabled.value = !isLockedOut diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt index 83d1938c93be..c2680bcc82a3 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt @@ -20,13 +20,20 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.authentication.data.repository.FakeAuthenticationRepository +import com.android.systemui.authentication.data.repository.authenticationRepository +import com.android.systemui.authentication.data.repository.fakeAuthenticationRepository +import com.android.systemui.authentication.domain.interactor.authenticationInteractor import com.android.systemui.authentication.shared.model.AuthenticationMethodModel import com.android.systemui.authentication.shared.model.AuthenticationPatternCoordinate as Point +import com.android.systemui.bouncer.domain.interactor.bouncerInteractor import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.deviceentry.data.repository.fakeDeviceEntryRepository +import com.android.systemui.kosmos.testScope import com.android.systemui.res.R -import com.android.systemui.scene.SceneTestUtils +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.testKosmos import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertWithMessage import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -44,20 +51,12 @@ import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class PatternBouncerViewModelTest : SysuiTestCase() { - private val utils = SceneTestUtils(this) - private val testScope = utils.testScope - private val authenticationInteractor = utils.authenticationInteractor() - private val sceneInteractor = utils.sceneInteractor() - private val bouncerInteractor = - utils.bouncerInteractor( - authenticationInteractor = authenticationInteractor, - ) - private val bouncerViewModel = - utils.bouncerViewModel( - bouncerInteractor = bouncerInteractor, - authenticationInteractor = authenticationInteractor, - actionButtonInteractor = utils.bouncerActionButtonInteractor(), - ) + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + private val authenticationInteractor = kosmos.authenticationInteractor + private val sceneInteractor = kosmos.sceneInteractor + private val bouncerInteractor = kosmos.bouncerInteractor + private val bouncerViewModel = kosmos.bouncerViewModel private val underTest = PatternBouncerViewModel( applicationContext = context, @@ -313,7 +312,7 @@ class PatternBouncerViewModelTest : SysuiTestCase() { underTest.onDragStart() CORRECT_PATTERN.subList( 0, - utils.authenticationRepository.minPatternLength - 1, + kosmos.authenticationRepository.minPatternLength - 1, ) .forEach { coordinate -> underTest.onDrag( @@ -382,8 +381,10 @@ class PatternBouncerViewModelTest : SysuiTestCase() { } private fun TestScope.lockDeviceAndOpenPatternBouncer() { - utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pattern) - utils.deviceEntryRepository.setUnlocked(false) + kosmos.fakeAuthenticationRepository.setAuthenticationMethod( + AuthenticationMethodModel.Pattern + ) + kosmos.fakeDeviceEntryRepository.setUnlocked(false) switchToScene(SceneKey.Bouncer) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt index db98d7632910..1d660d63710d 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt @@ -20,12 +20,20 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.authentication.data.repository.FakeAuthenticationRepository +import com.android.systemui.authentication.data.repository.fakeAuthenticationRepository +import com.android.systemui.authentication.domain.interactor.authenticationInteractor import com.android.systemui.authentication.shared.model.AuthenticationMethodModel +import com.android.systemui.bouncer.data.repository.fakeSimBouncerRepository +import com.android.systemui.bouncer.domain.interactor.bouncerInteractor +import com.android.systemui.bouncer.domain.interactor.simBouncerInteractor import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.deviceentry.data.repository.fakeDeviceEntryRepository +import com.android.systemui.kosmos.testScope import com.android.systemui.res.R -import com.android.systemui.scene.SceneTestUtils +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.testKosmos import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow @@ -43,27 +51,19 @@ import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class PinBouncerViewModelTest : SysuiTestCase() { - private val utils = SceneTestUtils(this) - private val testScope = utils.testScope - private val sceneInteractor = utils.sceneInteractor() - private val authenticationInteractor = utils.authenticationInteractor() - private val bouncerInteractor = - utils.bouncerInteractor( - authenticationInteractor = authenticationInteractor, - ) - private val bouncerViewModel = - utils.bouncerViewModel( - bouncerInteractor = bouncerInteractor, - authenticationInteractor = authenticationInteractor, - actionButtonInteractor = utils.bouncerActionButtonInteractor(), - ) + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + private val sceneInteractor = kosmos.sceneInteractor + private val authenticationInteractor = kosmos.authenticationInteractor + private val bouncerInteractor = kosmos.bouncerInteractor + private val bouncerViewModel = kosmos.bouncerViewModel private val underTest = PinBouncerViewModel( applicationContext = context, viewModelScope = testScope.backgroundScope, interactor = bouncerInteractor, isInputEnabled = MutableStateFlow(true).asStateFlow(), - simBouncerInteractor = utils.simBouncerInteractor, + simBouncerInteractor = kosmos.simBouncerInteractor, authenticationMethod = AuthenticationMethodModel.Pin, ) @@ -94,7 +94,7 @@ class PinBouncerViewModelTest : SysuiTestCase() { viewModelScope = testScope.backgroundScope, interactor = bouncerInteractor, isInputEnabled = MutableStateFlow(true).asStateFlow(), - simBouncerInteractor = utils.simBouncerInteractor, + simBouncerInteractor = kosmos.simBouncerInteractor, authenticationMethod = AuthenticationMethodModel.Sim, ) @@ -105,7 +105,7 @@ class PinBouncerViewModelTest : SysuiTestCase() { fun onErrorDialogDismissed_clearsDialogMessage() = testScope.runTest { val dialogMessage by collectLastValue(underTest.errorDialogMessage) - utils.simBouncerRepository.setSimVerificationErrorMessage("abc") + kosmos.fakeSimBouncerRepository.setSimVerificationErrorMessage("abc") assertThat(dialogMessage).isEqualTo("abc") underTest.onErrorDialogDismissed() @@ -122,10 +122,10 @@ class PinBouncerViewModelTest : SysuiTestCase() { viewModelScope = testScope.backgroundScope, interactor = bouncerInteractor, isInputEnabled = MutableStateFlow(true).asStateFlow(), - simBouncerInteractor = utils.simBouncerInteractor, + simBouncerInteractor = kosmos.simBouncerInteractor, authenticationMethod = AuthenticationMethodModel.Sim, ) - utils.authenticationRepository.setAutoConfirmFeatureEnabled(true) + kosmos.fakeAuthenticationRepository.setAutoConfirmFeatureEnabled(true) val hintedPinLength by collectLastValue(underTest.hintedPinLength) assertThat(hintedPinLength).isNull() @@ -262,7 +262,7 @@ class PinBouncerViewModelTest : SysuiTestCase() { @Test fun onAutoConfirm_whenCorrect() = testScope.runTest { - utils.authenticationRepository.setAutoConfirmFeatureEnabled(true) + kosmos.fakeAuthenticationRepository.setAutoConfirmFeatureEnabled(true) val authResult by collectLastValue(authenticationInteractor.onAuthenticationResult) lockDeviceAndOpenPinBouncer() @@ -277,7 +277,7 @@ class PinBouncerViewModelTest : SysuiTestCase() { val currentScene by collectLastValue(sceneInteractor.desiredScene) val message by collectLastValue(bouncerViewModel.message) val pin by collectLastValue(underTest.pinInput.map { it.getPin() }) - utils.authenticationRepository.setAutoConfirmFeatureEnabled(true) + kosmos.fakeAuthenticationRepository.setAutoConfirmFeatureEnabled(true) lockDeviceAndOpenPinBouncer() FakeAuthenticationRepository.DEFAULT_PIN.dropLast(1).forEach { digit -> @@ -317,7 +317,9 @@ class PinBouncerViewModelTest : SysuiTestCase() { testScope.runTest { val backspaceButtonAppearance by collectLastValue(underTest.backspaceButtonAppearance) - utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin) + kosmos.fakeAuthenticationRepository.setAuthenticationMethod( + AuthenticationMethodModel.Pin + ) assertThat(backspaceButtonAppearance).isEqualTo(ActionButtonAppearance.Shown) } @@ -326,8 +328,10 @@ class PinBouncerViewModelTest : SysuiTestCase() { fun backspaceButtonAppearance_withAutoConfirmButNoInput_isHidden() = testScope.runTest { val backspaceButtonAppearance by collectLastValue(underTest.backspaceButtonAppearance) - utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin) - utils.authenticationRepository.setAutoConfirmFeatureEnabled(true) + kosmos.fakeAuthenticationRepository.setAuthenticationMethod( + AuthenticationMethodModel.Pin + ) + kosmos.fakeAuthenticationRepository.setAutoConfirmFeatureEnabled(true) assertThat(backspaceButtonAppearance).isEqualTo(ActionButtonAppearance.Hidden) } @@ -336,8 +340,10 @@ class PinBouncerViewModelTest : SysuiTestCase() { fun backspaceButtonAppearance_withAutoConfirmAndInput_isShownQuiet() = testScope.runTest { val backspaceButtonAppearance by collectLastValue(underTest.backspaceButtonAppearance) - utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin) - utils.authenticationRepository.setAutoConfirmFeatureEnabled(true) + kosmos.fakeAuthenticationRepository.setAuthenticationMethod( + AuthenticationMethodModel.Pin + ) + kosmos.fakeAuthenticationRepository.setAutoConfirmFeatureEnabled(true) underTest.onPinButtonClicked(1) @@ -349,7 +355,9 @@ class PinBouncerViewModelTest : SysuiTestCase() { testScope.runTest { val confirmButtonAppearance by collectLastValue(underTest.confirmButtonAppearance) - utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin) + kosmos.fakeAuthenticationRepository.setAuthenticationMethod( + AuthenticationMethodModel.Pin + ) assertThat(confirmButtonAppearance).isEqualTo(ActionButtonAppearance.Shown) } @@ -358,8 +366,10 @@ class PinBouncerViewModelTest : SysuiTestCase() { fun confirmButtonAppearance_withAutoConfirm_isHidden() = testScope.runTest { val confirmButtonAppearance by collectLastValue(underTest.confirmButtonAppearance) - utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin) - utils.authenticationRepository.setAutoConfirmFeatureEnabled(true) + kosmos.fakeAuthenticationRepository.setAuthenticationMethod( + AuthenticationMethodModel.Pin + ) + kosmos.fakeAuthenticationRepository.setAutoConfirmFeatureEnabled(true) assertThat(confirmButtonAppearance).isEqualTo(ActionButtonAppearance.Hidden) } @@ -369,10 +379,10 @@ class PinBouncerViewModelTest : SysuiTestCase() { testScope.runTest { val isAnimationEnabled by collectLastValue(underTest.isDigitButtonAnimationEnabled) - utils.authenticationRepository.setPinEnhancedPrivacyEnabled(true) + kosmos.fakeAuthenticationRepository.setPinEnhancedPrivacyEnabled(true) assertThat(isAnimationEnabled).isFalse() - utils.authenticationRepository.setPinEnhancedPrivacyEnabled(false) + kosmos.fakeAuthenticationRepository.setPinEnhancedPrivacyEnabled(false) assertThat(isAnimationEnabled).isTrue() } @@ -390,8 +400,8 @@ class PinBouncerViewModelTest : SysuiTestCase() { } private fun TestScope.lockDeviceAndOpenPinBouncer() { - utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin) - utils.deviceEntryRepository.setUnlocked(false) + kosmos.fakeAuthenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin) + kosmos.fakeDeviceEntryRepository.setUnlocked(false) switchToScene(SceneKey.Bouncer) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalPrefsRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalPrefsRepositoryImplTest.kt new file mode 100644 index 000000000000..820bfbfdf0a2 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalPrefsRepositoryImplTest.kt @@ -0,0 +1,135 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.communal.data.repository + +import android.content.SharedPreferences +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.data.repository.CommunalPrefsRepositoryImpl.Companion.FILE_NAME +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.kosmos.testScope +import com.android.systemui.settings.UserFileManager +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.FakeSharedPreferences +import com.google.common.truth.Truth.assertThat +import java.io.File +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +class CommunalPrefsRepositoryImplTest : SysuiTestCase() { + private lateinit var underTest: CommunalPrefsRepositoryImpl + + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + + private lateinit var userRepository: FakeUserRepository + private lateinit var userFileManager: UserFileManager + + @Before + fun setUp() { + userRepository = kosmos.fakeUserRepository + userRepository.setUserInfos(USER_INFOS) + + userFileManager = + FakeUserFileManager( + mapOf( + USER_INFOS[0].id to FakeSharedPreferences(), + USER_INFOS[1].id to FakeSharedPreferences() + ) + ) + underTest = + CommunalPrefsRepositoryImpl( + testScope.backgroundScope, + kosmos.testDispatcher, + userRepository, + userFileManager, + ) + } + + @Test + fun isCtaDismissedValue_byDefault_isFalse() = + testScope.runTest { + val isCtaDismissed by collectLastValue(underTest.isCtaDismissed) + assertThat(isCtaDismissed).isFalse() + } + + @Test + fun isCtaDismissedValue_onSet_isTrue() = + testScope.runTest { + val isCtaDismissed by collectLastValue(underTest.isCtaDismissed) + + underTest.setCtaDismissedForCurrentUser() + assertThat(isCtaDismissed).isTrue() + } + + @Test + fun isCtaDismissedValue_whenSwitchUser() = + testScope.runTest { + val isCtaDismissed by collectLastValue(underTest.isCtaDismissed) + underTest.setCtaDismissedForCurrentUser() + + // dismissed true for primary user + assertThat(isCtaDismissed).isTrue() + + // switch to secondary user + userRepository.setSelectedUserInfo(USER_INFOS[1]) + + // dismissed is false for secondary user + assertThat(isCtaDismissed).isFalse() + + // switch back to primary user + userRepository.setSelectedUserInfo(USER_INFOS[0]) + + // dismissed is true for primary user + assertThat(isCtaDismissed).isTrue() + } + + private class FakeUserFileManager(private val sharedPrefs: Map<Int, SharedPreferences>) : + UserFileManager { + override fun getFile(fileName: String, userId: Int): File { + throw UnsupportedOperationException() + } + + override fun getSharedPreferences( + fileName: String, + mode: Int, + userId: Int + ): SharedPreferences { + if (fileName != FILE_NAME) { + throw IllegalArgumentException("Preference files must be $FILE_NAME") + } + return sharedPrefs.getValue(userId) + } + } + + companion object { + val USER_INFOS = + listOf( + UserInfo(/* id= */ 0, "zero", /* flags= */ 0), + UserInfo(/* id= */ 1, "secondary", /* flags= */ 0), + ) + } +} 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 65176e1c5c0d..81d5344ed264 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 @@ -24,11 +24,12 @@ import com.android.systemui.communal.shared.model.ObservableCommunalTransitionSt import com.android.systemui.coroutines.collectLastValue import com.android.systemui.flags.FakeFeatureFlagsClassic import com.android.systemui.flags.Flags -import com.android.systemui.scene.SceneTestUtils import com.android.systemui.scene.data.repository.SceneContainerRepository +import com.android.systemui.scene.data.repository.sceneContainerRepository 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.google.common.truth.Truth.assertThat import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.StandardTestDispatcher @@ -51,8 +52,8 @@ class CommunalRepositoryImplTest : SysuiTestCase() { @Before fun setUp() { - val sceneTestUtils = SceneTestUtils(this) - sceneContainerRepository = sceneTestUtils.fakeSceneContainerRepository() + val kosmos = testKosmos() + sceneContainerRepository = kosmos.sceneContainerRepository featureFlagsClassic = FakeFeatureFlagsClassic() featureFlagsClassic.set(Flags.COMMUNAL_SERVICE_ENABLED, true) diff --git a/packages/SystemUI/tests/src/com/android/systemui/communal/data/repository/CommunalTutorialRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalTutorialRepositoryImplTest.kt index 30a5497d0a14..30a5497d0a14 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/communal/data/repository/CommunalTutorialRepositoryImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalTutorialRepositoryImplTest.kt 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 4079f1241f31..1c6cecd6c838 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 @@ -16,7 +16,6 @@ package com.android.systemui.communal.data.repository -import android.appwidget.AppWidgetHost import android.appwidget.AppWidgetManager import android.appwidget.AppWidgetProviderInfo import android.content.ComponentName @@ -32,6 +31,7 @@ import com.android.systemui.communal.data.db.CommunalWidgetDao import com.android.systemui.communal.data.db.CommunalWidgetItem 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.coroutines.collectLastValue import com.android.systemui.log.LogBuffer import com.android.systemui.log.core.FakeLogBuffer @@ -65,7 +65,7 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { @Mock private lateinit var appWidgetManager: AppWidgetManager - @Mock private lateinit var appWidgetHost: AppWidgetHost + @Mock private lateinit var appWidgetHost: CommunalAppWidgetHost @Mock private lateinit var userManager: UserManager 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 cd83c07a3b38..178eb6a42e10 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 @@ -24,6 +24,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.communal.data.repository.FakeCommunalMediaRepository +import com.android.systemui.communal.data.repository.FakeCommunalPrefsRepository import com.android.systemui.communal.data.repository.FakeCommunalRepository import com.android.systemui.communal.data.repository.FakeCommunalTutorialRepository import com.android.systemui.communal.data.repository.FakeCommunalWidgetRepository @@ -65,6 +66,7 @@ class CommunalInteractorTest : SysuiTestCase() { private lateinit var widgetRepository: FakeCommunalWidgetRepository private lateinit var smartspaceRepository: FakeSmartspaceRepository private lateinit var keyguardRepository: FakeKeyguardRepository + private lateinit var communalPrefsRepository: FakeCommunalPrefsRepository private lateinit var editWidgetsActivityStarter: EditWidgetsActivityStarter private lateinit var underTest: CommunalInteractor @@ -84,6 +86,7 @@ class CommunalInteractorTest : SysuiTestCase() { smartspaceRepository = withDeps.smartspaceRepository keyguardRepository = withDeps.keyguardRepository editWidgetsActivityStarter = withDeps.editWidgetsActivityStarter + communalPrefsRepository = withDeps.communalPrefsRepository underTest = withDeps.communalInteractor } @@ -331,10 +334,9 @@ class CommunalInteractorTest : SysuiTestCase() { } @Test - fun cta_visibilityTrue_shows() = + fun ctaTile_showsByDefault() = testScope.runTest { tutorialRepository.setTutorialSettingState(HUB_MODE_TUTORIAL_COMPLETED) - communalRepository.setCtaTileInViewModeVisibility(true) val ctaTileContent by collectLastValue(underTest.ctaTileContent) @@ -346,10 +348,10 @@ class CommunalInteractorTest : SysuiTestCase() { } @Test - fun ctaTile_visibilityFalse_doesNotShow() = + fun ctaTile_afterDismiss_doesNotShow() = testScope.runTest { tutorialRepository.setTutorialSettingState(HUB_MODE_TUTORIAL_COMPLETED) - communalRepository.setCtaTileInViewModeVisibility(false) + communalPrefsRepository.setCtaDismissedForCurrentUser() val ctaTileContent by collectLastValue(underTest.ctaTileContent) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/widgets/CommunalAppWidgetHostTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/widgets/CommunalAppWidgetHostTest.kt new file mode 100644 index 000000000000..e904236f8c3e --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/widgets/CommunalAppWidgetHostTest.kt @@ -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 com.android.systemui.communal.ui.widgets + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.communal.widgets.CommunalAppWidgetHost +import com.android.systemui.communal.widgets.CommunalAppWidgetHostView +import com.android.systemui.kosmos.testScope +import com.android.systemui.testKosmos +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 + +@SmallTest +@RunWith(AndroidJUnit4::class) +class CommunalAppWidgetHostTest : SysuiTestCase() { + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + + private lateinit var underTest: CommunalAppWidgetHost + + @Before + fun setUp() { + underTest = CommunalAppWidgetHost(context = context, hostId = 116) + } + + @Test + fun createViewForCommunal_returnCommunalAppWidgetView() = + testScope.runTest { + val appWidgetId = 789 + val view = + underTest.createViewForCommunal( + context = context, + appWidgetId = appWidgetId, + appWidget = null + ) + assertThat(view).isInstanceOf(CommunalAppWidgetHostView::class.java) + assertThat(view).isNotNull() + assertThat(view.appWidgetId).isEqualTo(appWidgetId) + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt index 54510a82201a..09243e5282da 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt @@ -19,7 +19,6 @@ package com.android.systemui.communal.view.viewmodel import android.app.Activity.RESULT_CANCELED import android.app.Activity.RESULT_OK import android.app.smartspace.SmartspaceTarget -import android.appwidget.AppWidgetHost import android.content.ComponentName import android.provider.Settings import android.widget.RemoteViews @@ -36,6 +35,7 @@ import com.android.systemui.communal.domain.model.CommunalContentModel import com.android.systemui.communal.shared.log.CommunalUiEvent import com.android.systemui.communal.shared.model.CommunalWidgetContentModel import com.android.systemui.communal.ui.viewmodel.CommunalEditModeViewModel +import com.android.systemui.communal.widgets.CommunalAppWidgetHost import com.android.systemui.coroutines.collectLastValue import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository import com.android.systemui.kosmos.testScope @@ -61,7 +61,7 @@ import org.mockito.MockitoAnnotations @RunWith(AndroidJUnit4::class) class CommunalEditModeViewModelTest : SysuiTestCase() { @Mock private lateinit var mediaHost: MediaHost - @Mock private lateinit var appWidgetHost: AppWidgetHost + @Mock private lateinit var appWidgetHost: CommunalAppWidgetHost @Mock private lateinit var uiEventLogger: UiEventLogger private val kosmos = testKosmos() 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 a7760621e97c..f9cfc3732a01 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 @@ -23,6 +23,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.communal.data.repository.FakeCommunalMediaRepository +import com.android.systemui.communal.data.repository.FakeCommunalPrefsRepository import com.android.systemui.communal.data.repository.FakeCommunalRepository import com.android.systemui.communal.data.repository.FakeCommunalTutorialRepository import com.android.systemui.communal.data.repository.FakeCommunalWidgetRepository @@ -34,6 +35,7 @@ import com.android.systemui.communal.ui.viewmodel.CommunalViewModel.Companion.PO import com.android.systemui.communal.widgets.WidgetInteractionHandler import com.android.systemui.coroutines.collectLastValue import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository +import com.android.systemui.media.controls.ui.MediaHierarchyManager import com.android.systemui.media.controls.ui.MediaHost import com.android.systemui.smartspace.data.repository.FakeSmartspaceRepository import com.android.systemui.util.mockito.mock @@ -48,6 +50,7 @@ import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock import org.mockito.Mockito +import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations @OptIn(ExperimentalCoroutinesApi::class) @@ -64,6 +67,7 @@ class CommunalViewModelTest : SysuiTestCase() { private lateinit var widgetRepository: FakeCommunalWidgetRepository private lateinit var smartspaceRepository: FakeSmartspaceRepository private lateinit var mediaRepository: FakeCommunalMediaRepository + private lateinit var communalPrefsRepository: FakeCommunalPrefsRepository private lateinit var underTest: CommunalViewModel @@ -80,6 +84,7 @@ class CommunalViewModelTest : SysuiTestCase() { widgetRepository = withDeps.widgetRepository smartspaceRepository = withDeps.smartspaceRepository mediaRepository = withDeps.mediaRepository + communalPrefsRepository = withDeps.communalPrefsRepository underTest = CommunalViewModel( @@ -92,6 +97,13 @@ class CommunalViewModelTest : SysuiTestCase() { } @Test + fun init_initsMediaHost() = + testScope.runTest { + // MediaHost is initialized as soon as the class is created. + verify(mediaHost).init(MediaHierarchyManager.LOCATION_COMMUNAL_HUB) + } + + @Test fun tutorial_tutorialNotCompletedAndKeyguardVisible_showTutorialContent() = testScope.runTest { // Keyguard showing, and tutorial not started. @@ -140,9 +152,6 @@ class CommunalViewModelTest : SysuiTestCase() { // Media playing. mediaRepository.mediaActive() - // CTA Tile not dismissed. - communalRepository.setCtaTileInViewModeVisibility(true) - val communalContent by collectLastValue(underTest.communalContent) // Order is smart space, then UMO, widget content and cta tile. @@ -162,7 +171,6 @@ class CommunalViewModelTest : SysuiTestCase() { fun dismissCta_hidesCtaTileAndShowsPopup_thenHidesPopupAfterTimeout() = testScope.runTest { tutorialRepository.setTutorialSettingState(Settings.Secure.HUB_MODE_TUTORIAL_COMPLETED) - communalRepository.setCtaTileInViewModeVisibility(true) val communalContent by collectLastValue(underTest.communalContent) val isPopupOnDismissCtaShowing by collectLastValue(underTest.isPopupOnDismissCtaShowing) @@ -186,7 +194,6 @@ class CommunalViewModelTest : SysuiTestCase() { fun popup_onDismiss_hidesImmediately() = testScope.runTest { tutorialRepository.setTutorialSettingState(Settings.Secure.HUB_MODE_TUTORIAL_COMPLETED) - communalRepository.setCtaTileInViewModeVisibility(true) val isPopupOnDismissCtaShowing by collectLastValue(underTest.isPopupOnDismissCtaShowing) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/data/repository/DeviceEntryRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/data/repository/DeviceEntryRepositoryTest.kt index 565049baf6f4..b54c5bdae004 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/data/repository/DeviceEntryRepositoryTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/data/repository/DeviceEntryRepositoryTest.kt @@ -7,9 +7,11 @@ import com.android.internal.widget.LockPatternUtils import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository -import com.android.systemui.scene.SceneTestUtils +import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.kosmos.testScope import com.android.systemui.statusbar.phone.KeyguardBypassController import com.android.systemui.statusbar.policy.KeyguardStateController +import com.android.systemui.testKosmos import com.android.systemui.user.data.repository.FakeUserRepository import com.android.systemui.util.mockito.argumentCaptor import com.android.systemui.util.mockito.whenever @@ -36,8 +38,8 @@ class DeviceEntryRepositoryTest : SysuiTestCase() { @Mock private lateinit var keyguardBypassController: KeyguardBypassController @Mock private lateinit var keyguardStateController: KeyguardStateController - private val testUtils = SceneTestUtils(this) - private val testScope = testUtils.testScope + private val kosmos = testKosmos() + private val testScope = kosmos.testScope private val userRepository = FakeUserRepository() private val keyguardRepository = FakeKeyguardRepository() @@ -52,7 +54,7 @@ class DeviceEntryRepositoryTest : SysuiTestCase() { underTest = DeviceEntryRepositoryImpl( applicationScope = testScope.backgroundScope, - backgroundDispatcher = testUtils.testDispatcher, + backgroundDispatcher = kosmos.testDispatcher, userRepository = userRepository, lockPatternUtils = lockPatternUtils, keyguardBypassController = keyguardBypassController, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractorTest.kt index ea19cb799b10..62d23152b77a 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractorTest.kt @@ -20,19 +20,25 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.authentication.data.repository.FakeAuthenticationRepository +import com.android.systemui.authentication.data.repository.fakeAuthenticationRepository +import com.android.systemui.authentication.domain.interactor.authenticationInteractor import com.android.systemui.authentication.shared.model.AuthenticationMethodModel import com.android.systemui.coroutines.collectLastValue import com.android.systemui.coroutines.collectValues -import com.android.systemui.deviceentry.data.repository.FakeDeviceEntryRepository -import com.android.systemui.keyguard.data.repository.FakeDeviceEntryFaceAuthRepository -import com.android.systemui.keyguard.data.repository.FakeTrustRepository -import com.android.systemui.scene.SceneTestUtils +import com.android.systemui.deviceentry.data.repository.fakeDeviceEntryRepository +import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFaceAuthRepository +import com.android.systemui.keyguard.data.repository.fakeTrustRepository +import com.android.systemui.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.SceneKey import com.android.systemui.scene.shared.model.SceneModel +import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest +import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -41,21 +47,19 @@ import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class DeviceEntryInteractorTest : SysuiTestCase() { - private val utils = SceneTestUtils(this) - private val testScope = utils.testScope - private val repository: FakeDeviceEntryRepository = utils.deviceEntryRepository - private val faceAuthRepository = FakeDeviceEntryFaceAuthRepository() - private val trustRepository = FakeTrustRepository() - private val sceneInteractor = utils.sceneInteractor() - private val authenticationInteractor = utils.authenticationInteractor() - private val underTest = - utils.deviceEntryInteractor( - repository = repository, - authenticationInteractor = authenticationInteractor, - sceneInteractor = sceneInteractor, - faceAuthRepository = faceAuthRepository, - trustRepository = trustRepository, - ) + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + private val faceAuthRepository = kosmos.fakeDeviceEntryFaceAuthRepository + private val trustRepository = kosmos.fakeTrustRepository + private val sceneInteractor = kosmos.sceneInteractor + private val authenticationInteractor = kosmos.authenticationInteractor + private lateinit var underTest: DeviceEntryInteractor + + @Before + fun setUp() { + kosmos.fakeSceneContainerFlags.enabled = true + underTest = kosmos.deviceEntryInteractor + } @Test fun canSwipeToEnter_startsNull() = @@ -67,8 +71,10 @@ class DeviceEntryInteractorTest : SysuiTestCase() { @Test fun isUnlocked_whenAuthMethodIsNoneAndLockscreenDisabled_isTrue() = testScope.runTest { - utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.None) - utils.deviceEntryRepository.apply { + kosmos.fakeAuthenticationRepository.setAuthenticationMethod( + AuthenticationMethodModel.None + ) + kosmos.fakeDeviceEntryRepository.apply { setLockscreenEnabled(false) // Toggle isUnlocked, twice. @@ -101,8 +107,10 @@ class DeviceEntryInteractorTest : SysuiTestCase() { @Test fun isUnlocked_whenAuthMethodIsSimAndUnlocked_isFalse() = testScope.runTest { - utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Sim) - utils.deviceEntryRepository.setUnlocked(true) + kosmos.fakeAuthenticationRepository.setAuthenticationMethod( + AuthenticationMethodModel.Sim + ) + kosmos.fakeDeviceEntryRepository.setUnlocked(true) val isUnlocked by collectLastValue(underTest.isUnlocked) assertThat(isUnlocked).isFalse() @@ -159,10 +167,10 @@ class DeviceEntryInteractorTest : SysuiTestCase() { @Test fun isDeviceEntered_onBouncer_isFalse() = testScope.runTest { - utils.authenticationRepository.setAuthenticationMethod( + kosmos.fakeAuthenticationRepository.setAuthenticationMethod( AuthenticationMethodModel.Pattern ) - utils.deviceEntryRepository.setLockscreenEnabled(true) + kosmos.fakeDeviceEntryRepository.setLockscreenEnabled(true) switchToScene(SceneKey.Lockscreen) runCurrent() switchToScene(SceneKey.Bouncer) @@ -184,8 +192,10 @@ class DeviceEntryInteractorTest : SysuiTestCase() { @Test fun canSwipeToEnter_onLockscreenWithPin_isFalse() = testScope.runTest { - utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin) - utils.deviceEntryRepository.setLockscreenEnabled(true) + kosmos.fakeAuthenticationRepository.setAuthenticationMethod( + AuthenticationMethodModel.Pin + ) + kosmos.fakeDeviceEntryRepository.setLockscreenEnabled(true) switchToScene(SceneKey.Lockscreen) val canSwipeToEnter by collectLastValue(underTest.canSwipeToEnter) @@ -205,15 +215,15 @@ class DeviceEntryInteractorTest : SysuiTestCase() { } private fun setupSwipeDeviceEntryMethod() { - utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.None) - utils.deviceEntryRepository.setLockscreenEnabled(true) + kosmos.fakeAuthenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.None) + kosmos.fakeDeviceEntryRepository.setLockscreenEnabled(true) } @Test fun canSwipeToEnter_whenTrustedByTrustManager_isTrue() = testScope.runTest { val canSwipeToEnter by collectLastValue(underTest.canSwipeToEnter) - utils.authenticationRepository.setAuthenticationMethod( + kosmos.fakeAuthenticationRepository.setAuthenticationMethod( AuthenticationMethodModel.Password ) switchToScene(SceneKey.Lockscreen) @@ -230,7 +240,7 @@ class DeviceEntryInteractorTest : SysuiTestCase() { fun canSwipeToEnter_whenAuthenticatedByFace_isTrue() = testScope.runTest { val canSwipeToEnter by collectLastValue(underTest.canSwipeToEnter) - utils.authenticationRepository.setAuthenticationMethod( + kosmos.fakeAuthenticationRepository.setAuthenticationMethod( AuthenticationMethodModel.Password ) switchToScene(SceneKey.Lockscreen) @@ -246,9 +256,9 @@ class DeviceEntryInteractorTest : SysuiTestCase() { @Test fun isAuthenticationRequired_lockedAndSecured_true() = testScope.runTest { - utils.deviceEntryRepository.setUnlocked(false) + kosmos.fakeDeviceEntryRepository.setUnlocked(false) runCurrent() - utils.authenticationRepository.setAuthenticationMethod( + kosmos.fakeAuthenticationRepository.setAuthenticationMethod( AuthenticationMethodModel.Password ) @@ -258,9 +268,11 @@ class DeviceEntryInteractorTest : SysuiTestCase() { @Test fun isAuthenticationRequired_lockedAndNotSecured_false() = testScope.runTest { - utils.deviceEntryRepository.setUnlocked(false) + kosmos.fakeDeviceEntryRepository.setUnlocked(false) runCurrent() - utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.None) + kosmos.fakeAuthenticationRepository.setAuthenticationMethod( + AuthenticationMethodModel.None + ) assertThat(underTest.isAuthenticationRequired()).isFalse() } @@ -268,9 +280,9 @@ class DeviceEntryInteractorTest : SysuiTestCase() { @Test fun isAuthenticationRequired_unlockedAndSecured_false() = testScope.runTest { - utils.deviceEntryRepository.setUnlocked(true) + kosmos.fakeDeviceEntryRepository.setUnlocked(true) runCurrent() - utils.authenticationRepository.setAuthenticationMethod( + kosmos.fakeAuthenticationRepository.setAuthenticationMethod( AuthenticationMethodModel.Password ) @@ -280,9 +292,11 @@ class DeviceEntryInteractorTest : SysuiTestCase() { @Test fun isAuthenticationRequired_unlockedAndNotSecured_false() = testScope.runTest { - utils.deviceEntryRepository.setUnlocked(true) + kosmos.fakeDeviceEntryRepository.setUnlocked(true) runCurrent() - utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.None) + kosmos.fakeAuthenticationRepository.setAuthenticationMethod( + AuthenticationMethodModel.None + ) assertThat(underTest.isAuthenticationRequired()).isFalse() } @@ -290,7 +304,7 @@ class DeviceEntryInteractorTest : SysuiTestCase() { @Test fun isBypassEnabled_enabledInRepository_true() = testScope.runTest { - utils.deviceEntryRepository.setBypassEnabled(true) + kosmos.fakeDeviceEntryRepository.setBypassEnabled(true) assertThat(underTest.isBypassEnabled.value).isTrue() } @@ -301,8 +315,10 @@ class DeviceEntryInteractorTest : SysuiTestCase() { switchToScene(SceneKey.Lockscreen) assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Lockscreen)) - utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin) - utils.deviceEntryRepository.setUnlocked(true) + kosmos.fakeAuthenticationRepository.setAuthenticationMethod( + AuthenticationMethodModel.Pin + ) + kosmos.fakeDeviceEntryRepository.setUnlocked(true) runCurrent() underTest.attemptDeviceEntry() @@ -317,7 +333,9 @@ class DeviceEntryInteractorTest : SysuiTestCase() { switchToScene(SceneKey.Lockscreen) assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Lockscreen)) - utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.None) + kosmos.fakeAuthenticationRepository.setAuthenticationMethod( + AuthenticationMethodModel.None + ) underTest.attemptDeviceEntry() @@ -331,8 +349,10 @@ class DeviceEntryInteractorTest : SysuiTestCase() { switchToScene(SceneKey.Lockscreen) assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Lockscreen)) - utils.deviceEntryRepository.setLockscreenEnabled(true) - utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.None) + kosmos.fakeDeviceEntryRepository.setLockscreenEnabled(true) + kosmos.fakeAuthenticationRepository.setAuthenticationMethod( + AuthenticationMethodModel.None + ) underTest.attemptDeviceEntry() @@ -342,7 +362,7 @@ class DeviceEntryInteractorTest : SysuiTestCase() { @Test fun isBypassEnabled_disabledInRepository_false() = testScope.runTest { - utils.deviceEntryRepository.setBypassEnabled(false) + kosmos.fakeDeviceEntryRepository.setBypassEnabled(false) assertThat(underTest.isBypassEnabled.value).isFalse() } @@ -350,8 +370,10 @@ class DeviceEntryInteractorTest : SysuiTestCase() { fun successfulAuthenticationChallengeAttempt_updatesIsUnlockedState() = testScope.runTest { val isUnlocked by collectLastValue(underTest.isUnlocked) - utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin) - utils.deviceEntryRepository.setLockscreenEnabled(true) + kosmos.fakeAuthenticationRepository.setAuthenticationMethod( + AuthenticationMethodModel.Pin + ) + kosmos.fakeDeviceEntryRepository.setLockscreenEnabled(true) assertThat(isUnlocked).isFalse() authenticationInteractor.authenticate(FakeAuthenticationRepository.DEFAULT_PIN) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorTest.kt index 11939c1120d9..2c3afb1b40a9 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorTest.kt @@ -26,12 +26,16 @@ import com.android.systemui.common.ui.data.repository.FakeConfigurationRepositor import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor import com.android.systemui.coroutines.collectLastValue import com.android.systemui.keyguard.data.repository.FakeCommandQueue +import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository import com.android.systemui.keyguard.shared.model.CameraLaunchSourceModel +import com.android.systemui.kosmos.testScope import com.android.systemui.power.domain.interactor.PowerInteractorFactory -import com.android.systemui.scene.SceneTestUtils +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.shade.data.repository.FakeShadeRepository +import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow @@ -48,10 +52,10 @@ import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class KeyguardInteractorTest : SysuiTestCase() { - private val testUtils = SceneTestUtils(this) - private val testScope = testUtils.testScope - private val repository = testUtils.keyguardRepository - private val sceneInteractor = testUtils.sceneInteractor() + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + private val repository = kosmos.fakeKeyguardRepository + private val sceneInteractor = kosmos.sceneInteractor private val commandQueue = FakeCommandQueue() private val bouncerRepository = FakeKeyguardBouncerRepository() private val shadeRepository = FakeShadeRepository() @@ -63,7 +67,7 @@ class KeyguardInteractorTest : SysuiTestCase() { repository = repository, commandQueue = commandQueue, powerInteractor = PowerInteractorFactory.create().powerInteractor, - sceneContainerFlags = testUtils.sceneContainerFlags, + sceneContainerFlags = kosmos.fakeSceneContainerFlags, bouncerRepository = bouncerRepository, configurationInteractor = ConfigurationInteractor(FakeConfigurationRepository()), shadeRepository = shadeRepository, @@ -183,6 +187,7 @@ class KeyguardInteractorTest : SysuiTestCase() { @Test fun animationDozingTransitions() = testScope.runTest { + kosmos.fakeSceneContainerFlags.enabled = true val isAnimate by collectLastValue(underTest.animateDozingTransitions) underTest.setAnimateDozingTransitions(true) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTest.kt index 4f7d9444020c..6828041eff5a 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTest.kt @@ -21,23 +21,24 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectValues -import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository +import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository import com.android.systemui.keyguard.shared.model.KeyguardState.AOD import com.android.systemui.keyguard.shared.model.KeyguardState.DOZING import com.android.systemui.keyguard.shared.model.KeyguardState.GONE import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN +import com.android.systemui.keyguard.shared.model.KeyguardState.OFF import com.android.systemui.keyguard.shared.model.KeyguardState.PRIMARY_BOUNCER import com.android.systemui.keyguard.shared.model.TransitionState.CANCELED import com.android.systemui.keyguard.shared.model.TransitionState.FINISHED import com.android.systemui.keyguard.shared.model.TransitionState.RUNNING import com.android.systemui.keyguard.shared.model.TransitionState.STARTED import com.android.systemui.keyguard.shared.model.TransitionStep +import com.android.systemui.kosmos.testScope +import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat import junit.framework.Assert.assertEquals -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 @@ -46,18 +47,11 @@ import org.junit.runner.RunWith @kotlinx.coroutines.ExperimentalCoroutinesApi class KeyguardTransitionInteractorTest : SysuiTestCase() { - private lateinit var underTest: KeyguardTransitionInteractor - private lateinit var repository: FakeKeyguardTransitionRepository - private val testScope = TestScope() - - @Before - fun setUp() { - repository = FakeKeyguardTransitionRepository() - underTest = KeyguardTransitionInteractorFactory.create( - scope = testScope.backgroundScope, - repository = repository, - ).keyguardTransitionInteractor - } + val kosmos = testKosmos() + + val underTest = kosmos.keyguardTransitionInteractor + val repository = kosmos.fakeKeyguardTransitionRepository + val testScope = kosmos.testScope @Test fun transitionCollectorsReceivesOnlyAppropriateEvents() = runTest { @@ -114,49 +108,51 @@ class KeyguardTransitionInteractorTest : SysuiTestCase() { } @Test - fun finishedKeyguardStateTests() = testScope.runTest { - val finishedSteps by collectValues(underTest.finishedKeyguardState) - runCurrent() - val steps = mutableListOf<TransitionStep>() - - steps.add(TransitionStep(AOD, PRIMARY_BOUNCER, 0f, STARTED)) - steps.add(TransitionStep(AOD, PRIMARY_BOUNCER, 0.5f, RUNNING)) - steps.add(TransitionStep(AOD, PRIMARY_BOUNCER, 1f, FINISHED)) - steps.add(TransitionStep(PRIMARY_BOUNCER, AOD, 0f, STARTED)) - steps.add(TransitionStep(PRIMARY_BOUNCER, AOD, 0.9f, RUNNING)) - steps.add(TransitionStep(PRIMARY_BOUNCER, AOD, 1f, FINISHED)) - steps.add(TransitionStep(AOD, GONE, 1f, STARTED)) - - steps.forEach { - repository.sendTransitionStep(it) + fun finishedKeyguardStateTests() = + testScope.runTest { + val finishedSteps by collectValues(underTest.finishedKeyguardState) runCurrent() + val steps = mutableListOf<TransitionStep>() + + steps.add(TransitionStep(AOD, PRIMARY_BOUNCER, 0f, STARTED)) + steps.add(TransitionStep(AOD, PRIMARY_BOUNCER, 0.5f, RUNNING)) + steps.add(TransitionStep(AOD, PRIMARY_BOUNCER, 1f, FINISHED)) + steps.add(TransitionStep(PRIMARY_BOUNCER, AOD, 0f, STARTED)) + steps.add(TransitionStep(PRIMARY_BOUNCER, AOD, 0.9f, RUNNING)) + steps.add(TransitionStep(PRIMARY_BOUNCER, AOD, 1f, FINISHED)) + steps.add(TransitionStep(AOD, GONE, 1f, STARTED)) + + steps.forEach { + repository.sendTransitionStep(it) + runCurrent() + } + + assertThat(finishedSteps).isEqualTo(listOf(LOCKSCREEN, PRIMARY_BOUNCER, AOD)) } - assertThat(finishedSteps).isEqualTo(listOf(LOCKSCREEN, PRIMARY_BOUNCER, AOD)) - } - @Test - fun startedKeyguardStateTests() = testScope.runTest { - val startedStates by collectValues(underTest.startedKeyguardState) - runCurrent() - val steps = mutableListOf<TransitionStep>() - - steps.add(TransitionStep(AOD, PRIMARY_BOUNCER, 0f, STARTED)) - steps.add(TransitionStep(AOD, PRIMARY_BOUNCER, 0.5f, RUNNING)) - steps.add(TransitionStep(AOD, PRIMARY_BOUNCER, 1f, FINISHED)) - steps.add(TransitionStep(PRIMARY_BOUNCER, AOD, 0f, STARTED)) - steps.add(TransitionStep(PRIMARY_BOUNCER, AOD, 0.9f, RUNNING)) - steps.add(TransitionStep(PRIMARY_BOUNCER, AOD, 1f, FINISHED)) - steps.add(TransitionStep(AOD, GONE, 1f, STARTED)) - - steps.forEach { - repository.sendTransitionStep(it) + fun startedKeyguardStateTests() = + testScope.runTest { + val startedStates by collectValues(underTest.startedKeyguardState) runCurrent() + val steps = mutableListOf<TransitionStep>() + + steps.add(TransitionStep(AOD, PRIMARY_BOUNCER, 0f, STARTED)) + steps.add(TransitionStep(AOD, PRIMARY_BOUNCER, 0.5f, RUNNING)) + steps.add(TransitionStep(AOD, PRIMARY_BOUNCER, 1f, FINISHED)) + steps.add(TransitionStep(PRIMARY_BOUNCER, AOD, 0f, STARTED)) + steps.add(TransitionStep(PRIMARY_BOUNCER, AOD, 0.9f, RUNNING)) + steps.add(TransitionStep(PRIMARY_BOUNCER, AOD, 1f, FINISHED)) + steps.add(TransitionStep(AOD, GONE, 1f, STARTED)) + + steps.forEach { + repository.sendTransitionStep(it) + runCurrent() + } + + assertThat(startedStates).isEqualTo(listOf(LOCKSCREEN, PRIMARY_BOUNCER, AOD, GONE)) } - assertThat(startedStates).isEqualTo(listOf(LOCKSCREEN, PRIMARY_BOUNCER, AOD, GONE)) - } - @Test fun finishedKeyguardTransitionStepTests() = runTest { val finishedSteps by collectValues(underTest.finishedKeyguardTransitionStep) @@ -178,7 +174,7 @@ class KeyguardTransitionInteractorTest : SysuiTestCase() { // Ignore the default state. assertThat(finishedSteps.subList(1, finishedSteps.size)) - .isEqualTo(listOf(steps[2], steps[5])) + .isEqualTo(listOf(steps[2], steps[5])) } @Test @@ -233,500 +229,1067 @@ class KeyguardTransitionInteractorTest : SysuiTestCase() { } @Test - fun isInTransitionToState() = testScope.runTest { - val results by collectValues(underTest.isInTransitionToState(GONE)) + fun isInTransitionToAnyState() = + testScope.runTest { + val inTransition by collectValues(underTest.isInTransitionToAnyState) + + assertEquals( + listOf( + true, // The repo is seeded with a transition from OFF to LOCKSCREEN. + false, + ), + inTransition + ) + + sendSteps( + TransitionStep(LOCKSCREEN, GONE, 0f, STARTED), + ) + + assertEquals( + listOf( + true, + false, + true, + ), + inTransition + ) + + sendSteps( + TransitionStep(LOCKSCREEN, GONE, 0.5f, RUNNING), + ) + + assertEquals( + listOf( + true, + false, + true, + ), + inTransition + ) + + sendSteps( + TransitionStep(LOCKSCREEN, GONE, 1f, FINISHED), + ) + + assertEquals( + listOf( + true, + false, + true, + false, + ), + inTransition + ) + } + + @Test + fun isInTransitionToAnyState_finishedStateIsStartedStateAfterCancels() = + testScope.runTest { + val inTransition by collectValues(underTest.isInTransitionToAnyState) + + assertEquals( + listOf( + true, + false, + ), + inTransition + ) + + // Start FINISHED in GONE. + sendSteps( + TransitionStep(LOCKSCREEN, GONE, 0f, STARTED), + TransitionStep(LOCKSCREEN, GONE, 0.5f, RUNNING), + TransitionStep(LOCKSCREEN, GONE, 1f, FINISHED), + ) + + assertEquals( + listOf( + true, + false, + true, + false, + ), + inTransition + ) + + sendSteps( + TransitionStep(GONE, DOZING, 0f, STARTED), + ) + + assertEquals( + listOf( + true, + false, + true, + false, + true, + ), + inTransition + ) + + sendSteps( + TransitionStep(GONE, DOZING, 0.5f, RUNNING), + TransitionStep(GONE, DOZING, 0.6f, CANCELED), + TransitionStep(DOZING, LOCKSCREEN, 0f, STARTED), + TransitionStep(DOZING, LOCKSCREEN, 0.5f, RUNNING), + TransitionStep(DOZING, LOCKSCREEN, 0.6f, CANCELED), + TransitionStep(LOCKSCREEN, GONE, 0f, STARTED), + ) + + assertEquals( + listOf( + true, + false, + true, + false, + // We should have been in transition throughout the entire transition, including + // both cancellations, and we should still be in transition despite now + // transitioning to GONE, the state we're also FINISHED in. + true, + ), + inTransition + ) + + sendSteps( + TransitionStep(LOCKSCREEN, GONE, 0.5f, RUNNING), + TransitionStep(LOCKSCREEN, GONE, 1f, FINISHED), + ) + + assertEquals( + listOf( + true, + false, + true, + false, + true, + false, + ), + inTransition + ) + } + + @Test + fun isInTransitionToState() = + testScope.runTest { + val results by collectValues(underTest.isInTransitionToState(GONE)) - sendSteps( + sendSteps( TransitionStep(AOD, DOZING, 0f, STARTED), TransitionStep(AOD, DOZING, 0.5f, RUNNING), TransitionStep(AOD, DOZING, 1f, FINISHED), - ) - + ) - assertThat(results).isEqualTo(listOf( - false, - )) + assertThat(results) + .isEqualTo( + listOf( + false, + ) + ) - sendSteps( + sendSteps( TransitionStep(DOZING, GONE, 0f, STARTED), - ) + ) - assertThat(results).isEqualTo(listOf( - false, - true, - )) + assertThat(results) + .isEqualTo( + listOf( + false, + true, + ) + ) - sendSteps( + sendSteps( TransitionStep(DOZING, GONE, 0f, RUNNING), - ) + ) - assertThat(results).isEqualTo(listOf( - false, - true, - )) + assertThat(results) + .isEqualTo( + listOf( + false, + true, + ) + ) - sendSteps( + sendSteps( TransitionStep(DOZING, GONE, 0f, FINISHED), - ) + ) - assertThat(results).isEqualTo(listOf( - false, - true, - false, - )) + assertThat(results) + .isEqualTo( + listOf( + false, + true, + false, + ) + ) - sendSteps( + sendSteps( TransitionStep(GONE, DOZING, 0f, STARTED), TransitionStep(GONE, DOZING, 0f, RUNNING), TransitionStep(GONE, DOZING, 1f, FINISHED), - ) + ) - assertThat(results).isEqualTo(listOf( - false, - true, - false, - )) + assertThat(results) + .isEqualTo( + listOf( + false, + true, + false, + ) + ) - sendSteps( + sendSteps( TransitionStep(DOZING, GONE, 0f, STARTED), TransitionStep(DOZING, GONE, 0f, RUNNING), - ) - - assertThat(results).isEqualTo(listOf( - false, - true, - false, - true, - )) - } + ) + + assertThat(results) + .isEqualTo( + listOf( + false, + true, + false, + true, + ) + ) + } @Test - fun isInTransitionFromState() = testScope.runTest { - val results by collectValues(underTest.isInTransitionFromState(DOZING)) + fun isInTransitionFromState() = + testScope.runTest { + val results by collectValues(underTest.isInTransitionFromState(DOZING)) - sendSteps( + sendSteps( TransitionStep(AOD, DOZING, 0f, STARTED), TransitionStep(AOD, DOZING, 0.5f, RUNNING), TransitionStep(AOD, DOZING, 1f, FINISHED), - ) - + ) - assertThat(results).isEqualTo(listOf( - false, - )) + assertThat(results) + .isEqualTo( + listOf( + false, + ) + ) - sendSteps( + sendSteps( TransitionStep(DOZING, GONE, 0f, STARTED), - ) + ) - assertThat(results).isEqualTo(listOf( - false, - true, - )) + assertThat(results) + .isEqualTo( + listOf( + false, + true, + ) + ) - sendSteps( + sendSteps( TransitionStep(DOZING, GONE, 0f, RUNNING), - ) + ) - assertThat(results).isEqualTo(listOf( - false, - true, - )) + assertThat(results) + .isEqualTo( + listOf( + false, + true, + ) + ) - sendSteps( + sendSteps( TransitionStep(DOZING, GONE, 0f, FINISHED), - ) + ) - assertThat(results).isEqualTo(listOf( - false, - true, - false, - )) + assertThat(results) + .isEqualTo( + listOf( + false, + true, + false, + ) + ) - sendSteps( + sendSteps( TransitionStep(GONE, DOZING, 0f, STARTED), TransitionStep(GONE, DOZING, 0f, RUNNING), TransitionStep(GONE, DOZING, 1f, FINISHED), - ) + ) - assertThat(results).isEqualTo(listOf( - false, - true, - false, - )) + assertThat(results) + .isEqualTo( + listOf( + false, + true, + false, + ) + ) - sendSteps( + sendSteps( TransitionStep(DOZING, GONE, 0f, STARTED), TransitionStep(DOZING, GONE, 0f, RUNNING), - ) - - assertThat(results).isEqualTo(listOf( - false, - true, - false, - true, - )) - } + ) + + assertThat(results) + .isEqualTo( + listOf( + false, + true, + false, + true, + ) + ) + } @Test - fun isInTransitionFromStateWhere() = testScope.runTest { - val results by collectValues(underTest.isInTransitionFromStateWhere { - it == DOZING - }) + fun isInTransitionFromStateWhere() = + testScope.runTest { + val results by collectValues(underTest.isInTransitionFromStateWhere { it == DOZING }) - sendSteps( + sendSteps( TransitionStep(AOD, DOZING, 0f, STARTED), TransitionStep(AOD, DOZING, 0.5f, RUNNING), TransitionStep(AOD, DOZING, 1f, FINISHED), - ) - + ) - assertThat(results).isEqualTo(listOf( - false, - )) + assertThat(results) + .isEqualTo( + listOf( + false, + ) + ) - sendSteps( + sendSteps( TransitionStep(DOZING, GONE, 0f, STARTED), - ) + ) - assertThat(results).isEqualTo(listOf( - false, - true, - )) + assertThat(results) + .isEqualTo( + listOf( + false, + true, + ) + ) - sendSteps( + sendSteps( TransitionStep(DOZING, GONE, 0f, RUNNING), - ) + ) - assertThat(results).isEqualTo(listOf( - false, - true, - )) + assertThat(results) + .isEqualTo( + listOf( + false, + true, + ) + ) - sendSteps( + sendSteps( TransitionStep(DOZING, GONE, 0f, FINISHED), - ) + ) - assertThat(results).isEqualTo(listOf( - false, - true, - false, - )) + assertThat(results) + .isEqualTo( + listOf( + false, + true, + false, + ) + ) - sendSteps( + sendSteps( TransitionStep(GONE, DOZING, 0f, STARTED), TransitionStep(GONE, DOZING, 0f, RUNNING), TransitionStep(GONE, DOZING, 1f, FINISHED), - ) + ) - assertThat(results).isEqualTo(listOf( - false, - true, - false, - )) + assertThat(results) + .isEqualTo( + listOf( + false, + true, + false, + ) + ) - sendSteps( + sendSteps( TransitionStep(DOZING, GONE, 0f, STARTED), TransitionStep(DOZING, GONE, 0f, RUNNING), - ) - - assertThat(results).isEqualTo(listOf( - false, - true, - false, - true, - )) - } + ) + + assertThat(results) + .isEqualTo( + listOf( + false, + true, + false, + true, + ) + ) + } @Test - fun isInTransitionWhere() = testScope.runTest { - val results by collectValues(underTest.isInTransitionWhere( - fromStatePredicate = { it == DOZING }, - toStatePredicate = { it == GONE }, - )) + fun isInTransitionWhere() = + testScope.runTest { + val results by + collectValues( + underTest.isInTransitionWhere( + fromStatePredicate = { it == DOZING }, + toStatePredicate = { it == GONE }, + ) + ) - sendSteps( + sendSteps( TransitionStep(AOD, DOZING, 0f, STARTED), TransitionStep(AOD, DOZING, 0.5f, RUNNING), TransitionStep(AOD, DOZING, 1f, FINISHED), - ) - + ) - assertThat(results).isEqualTo(listOf( - false, - )) + assertThat(results) + .isEqualTo( + listOf( + false, + ) + ) - sendSteps( + sendSteps( TransitionStep(DOZING, GONE, 0f, STARTED), - ) + ) - assertThat(results).isEqualTo(listOf( - false, - true, - )) + assertThat(results) + .isEqualTo( + listOf( + false, + true, + ) + ) - sendSteps( + sendSteps( TransitionStep(DOZING, GONE, 0f, RUNNING), - ) + ) - assertThat(results).isEqualTo(listOf( - false, - true, - )) + assertThat(results) + .isEqualTo( + listOf( + false, + true, + ) + ) - sendSteps( + sendSteps( TransitionStep(DOZING, GONE, 0f, FINISHED), - ) + ) - assertThat(results).isEqualTo(listOf( - false, - true, - false, - )) + assertThat(results) + .isEqualTo( + listOf( + false, + true, + false, + ) + ) - sendSteps( + sendSteps( TransitionStep(GONE, DOZING, 0f, STARTED), TransitionStep(GONE, DOZING, 0f, RUNNING), TransitionStep(GONE, DOZING, 1f, FINISHED), - ) + ) - assertThat(results).isEqualTo(listOf( - false, - true, - false, - )) + assertThat(results) + .isEqualTo( + listOf( + false, + true, + false, + ) + ) - sendSteps( + sendSteps( TransitionStep(DOZING, GONE, 0f, STARTED), TransitionStep(DOZING, GONE, 0f, RUNNING), - ) - - assertThat(results).isEqualTo(listOf( - false, - true, - false, - true, - )) - } + ) + + assertThat(results) + .isEqualTo( + listOf( + false, + true, + false, + true, + ) + ) + } @Test - fun isFinishedInStateWhere() = testScope.runTest { - val results by collectValues(underTest.isFinishedInStateWhere { it == GONE } ) + fun isInTransitionWhere_withCanceledStep() = + testScope.runTest { + val results by + collectValues( + underTest.isInTransitionWhere( + fromStatePredicate = { it == DOZING }, + toStatePredicate = { it == GONE }, + ) + ) - sendSteps( + sendSteps( TransitionStep(AOD, DOZING, 0f, STARTED), TransitionStep(AOD, DOZING, 0.5f, RUNNING), TransitionStep(AOD, DOZING, 1f, FINISHED), - ) + ) - assertThat(results).isEqualTo(listOf( - false, // Finished in DOZING, not GONE. - )) + assertThat(results) + .isEqualTo( + listOf( + false, + ) + ) - sendSteps(TransitionStep(DOZING, GONE, 0f, STARTED)) + sendSteps( + TransitionStep(DOZING, GONE, 0f, STARTED), + ) - assertThat(results).isEqualTo(listOf( - false, - )) + assertThat(results) + .isEqualTo( + listOf( + false, + true, + ) + ) - sendSteps(TransitionStep(DOZING, GONE, 0f, RUNNING)) + sendSteps( + TransitionStep(DOZING, GONE, 0f, RUNNING), + ) - assertThat(results).isEqualTo(listOf( - false, - )) + assertThat(results) + .isEqualTo( + listOf( + false, + true, + ) + ) - sendSteps(TransitionStep(DOZING, GONE, 1f, FINISHED)) + sendSteps( + TransitionStep(DOZING, GONE, 0f, CANCELED), + ) - assertThat(results).isEqualTo(listOf( - false, - true, - )) + assertThat(results) + .isEqualTo( + listOf( + false, + true, + ) + ) - sendSteps( + sendSteps( TransitionStep(GONE, DOZING, 0f, STARTED), TransitionStep(GONE, DOZING, 0f, RUNNING), - ) + TransitionStep(GONE, DOZING, 1f, FINISHED), + ) - assertThat(results).isEqualTo(listOf( - false, - true, - )) + assertThat(results) + .isEqualTo( + listOf( + false, + true, + false, + ) + ) + + sendSteps( + TransitionStep(DOZING, GONE, 0f, STARTED), + TransitionStep(DOZING, GONE, 0f, RUNNING), + ) + + assertThat(results) + .isEqualTo( + listOf( + false, + true, + false, + true, + ) + ) + } + + @Test + fun isFinishedInStateWhere() = + testScope.runTest { + val results by collectValues(underTest.isFinishedInStateWhere { it == GONE }) + + sendSteps( + TransitionStep(AOD, DOZING, 0f, STARTED), + TransitionStep(AOD, DOZING, 0.5f, RUNNING), + TransitionStep(AOD, DOZING, 1f, FINISHED), + ) + + assertThat(results) + .isEqualTo( + listOf( + false, // Finished in DOZING, not GONE. + ) + ) + + sendSteps(TransitionStep(DOZING, GONE, 0f, STARTED)) + + assertThat(results) + .isEqualTo( + listOf( + false, + ) + ) + + sendSteps(TransitionStep(DOZING, GONE, 0f, RUNNING)) + + assertThat(results) + .isEqualTo( + listOf( + false, + ) + ) + + sendSteps(TransitionStep(DOZING, GONE, 1f, FINISHED)) - sendSteps(TransitionStep(GONE, DOZING, 1f, FINISHED)) + assertThat(results) + .isEqualTo( + listOf( + false, + true, + ) + ) - assertThat(results).isEqualTo(listOf( - false, - true, - false, - )) + sendSteps( + TransitionStep(GONE, DOZING, 0f, STARTED), + TransitionStep(GONE, DOZING, 0f, RUNNING), + ) - sendSteps( + assertThat(results) + .isEqualTo( + listOf( + false, + true, + ) + ) + + sendSteps(TransitionStep(GONE, DOZING, 1f, FINISHED)) + + assertThat(results) + .isEqualTo( + listOf( + false, + true, + false, + ) + ) + + sendSteps( TransitionStep(DOZING, GONE, 0f, STARTED), TransitionStep(DOZING, GONE, 0f, RUNNING), - ) - - assertThat(results).isEqualTo(listOf( - false, - true, - false, - )) - - sendSteps(TransitionStep(DOZING, GONE, 1f, FINISHED)) - - assertThat(results).isEqualTo(listOf( - false, - true, - false, - true, - )) - } + ) + + assertThat(results) + .isEqualTo( + listOf( + false, + true, + false, + ) + ) + + sendSteps(TransitionStep(DOZING, GONE, 1f, FINISHED)) + + assertThat(results) + .isEqualTo( + listOf( + false, + true, + false, + true, + ) + ) + } @Test - fun isFinishedInState() = testScope.runTest { - val results by collectValues(underTest.isFinishedInState(GONE)) + fun isFinishedInState() = + testScope.runTest { + val results by collectValues(underTest.isFinishedInState(GONE)) - sendSteps( + sendSteps( TransitionStep(AOD, DOZING, 0f, STARTED), TransitionStep(AOD, DOZING, 0.5f, RUNNING), TransitionStep(AOD, DOZING, 1f, FINISHED), - ) + ) - assertThat(results).isEqualTo(listOf( - false, // Finished in DOZING, not GONE. - )) + assertThat(results) + .isEqualTo( + listOf( + false, // Finished in DOZING, not GONE. + ) + ) - sendSteps(TransitionStep(DOZING, GONE, 0f, STARTED)) + sendSteps(TransitionStep(DOZING, GONE, 0f, STARTED)) - assertThat(results).isEqualTo(listOf( - false, - )) + assertThat(results) + .isEqualTo( + listOf( + false, + ) + ) - sendSteps(TransitionStep(DOZING, GONE, 0f, RUNNING)) + sendSteps(TransitionStep(DOZING, GONE, 0f, RUNNING)) - assertThat(results).isEqualTo(listOf( - false, - )) + assertThat(results) + .isEqualTo( + listOf( + false, + ) + ) - sendSteps(TransitionStep(DOZING, GONE, 1f, FINISHED)) + sendSteps(TransitionStep(DOZING, GONE, 1f, FINISHED)) - assertThat(results).isEqualTo(listOf( - false, - true, - )) + assertThat(results) + .isEqualTo( + listOf( + false, + true, + ) + ) - sendSteps( + sendSteps( TransitionStep(GONE, DOZING, 0f, STARTED), TransitionStep(GONE, DOZING, 0f, RUNNING), - ) + ) - assertThat(results).isEqualTo(listOf( - false, - true, - )) + assertThat(results) + .isEqualTo( + listOf( + false, + true, + ) + ) - sendSteps(TransitionStep(GONE, DOZING, 1f, FINISHED)) + sendSteps(TransitionStep(GONE, DOZING, 1f, FINISHED)) - assertThat(results).isEqualTo(listOf( - false, - true, - false, - )) + assertThat(results) + .isEqualTo( + listOf( + false, + true, + false, + ) + ) - sendSteps( + sendSteps( TransitionStep(DOZING, GONE, 0f, STARTED), TransitionStep(DOZING, GONE, 0f, RUNNING), - ) - - assertThat(results).isEqualTo(listOf( - false, - true, - false, - )) - - sendSteps(TransitionStep(DOZING, GONE, 1f, FINISHED)) - - assertThat(results).isEqualTo(listOf( - false, - true, - false, - true, - )) - } + ) + + assertThat(results) + .isEqualTo( + listOf( + false, + true, + false, + ) + ) + + sendSteps(TransitionStep(DOZING, GONE, 1f, FINISHED)) + + assertThat(results) + .isEqualTo( + listOf( + false, + true, + false, + true, + ) + ) + } @Test - fun finishedKeyguardState_emitsAgainIfCancelledAndReversed() = testScope.runTest { - val finishedStates by collectValues(underTest.finishedKeyguardState) + fun finishedKeyguardState_emitsAgainIfCancelledAndReversed() = + testScope.runTest { + val finishedStates by collectValues(underTest.finishedKeyguardState) - // We default FINISHED in LOCKSCREEN. - assertEquals(listOf( - LOCKSCREEN - ), finishedStates) + // We default FINISHED in LOCKSCREEN. + assertEquals(listOf(LOCKSCREEN), finishedStates) - sendSteps( + sendSteps( TransitionStep(LOCKSCREEN, AOD, 0f, STARTED), TransitionStep(LOCKSCREEN, AOD, 0.5f, RUNNING), TransitionStep(LOCKSCREEN, AOD, 1f, FINISHED), - ) + ) - // We're FINISHED in AOD. - assertEquals(listOf( - LOCKSCREEN, - AOD, - ), finishedStates) + // We're FINISHED in AOD. + assertEquals( + listOf( + LOCKSCREEN, + AOD, + ), + finishedStates + ) - // Transition back to LOCKSCREEN. - sendSteps( + // Transition back to LOCKSCREEN. + sendSteps( TransitionStep(AOD, LOCKSCREEN, 0f, STARTED), TransitionStep(AOD, LOCKSCREEN, 0.5f, RUNNING), TransitionStep(AOD, LOCKSCREEN, 1f, FINISHED), - ) + ) - // We're FINISHED in LOCKSCREEN. - assertEquals(listOf( - LOCKSCREEN, - AOD, - LOCKSCREEN, - ), finishedStates) + // We're FINISHED in LOCKSCREEN. + assertEquals( + listOf( + LOCKSCREEN, + AOD, + LOCKSCREEN, + ), + finishedStates + ) - sendSteps( + sendSteps( TransitionStep(LOCKSCREEN, GONE, 0f, STARTED), TransitionStep(LOCKSCREEN, GONE, 0.5f, RUNNING), - ) + ) - // We've STARTED a transition to GONE but not yet finished it so we're still FINISHED in - // LOCKSCREEN. - assertEquals(listOf( - LOCKSCREEN, - AOD, - LOCKSCREEN, - ), finishedStates) + // We've STARTED a transition to GONE but not yet finished it so we're still FINISHED in + // LOCKSCREEN. + assertEquals( + listOf( + LOCKSCREEN, + AOD, + LOCKSCREEN, + ), + finishedStates + ) - sendSteps( + sendSteps( TransitionStep(LOCKSCREEN, GONE, 0.6f, CANCELED), - ) + ) - // We've CANCELED a transition to GONE, we're still FINISHED in LOCKSCREEN. - assertEquals(listOf( - LOCKSCREEN, - AOD, - LOCKSCREEN, - ), finishedStates) + // We've CANCELED a transition to GONE, we're still FINISHED in LOCKSCREEN. + assertEquals( + listOf( + LOCKSCREEN, + AOD, + LOCKSCREEN, + ), + finishedStates + ) - sendSteps( + sendSteps( TransitionStep(GONE, LOCKSCREEN, 0.6f, STARTED), TransitionStep(GONE, LOCKSCREEN, 0.9f, RUNNING), TransitionStep(GONE, LOCKSCREEN, 1f, FINISHED), - ) - - // Expect another emission of LOCKSCREEN, as we have FINISHED a second transition to - // LOCKSCREEN after the cancellation. - assertEquals(listOf( - LOCKSCREEN, - AOD, - LOCKSCREEN, - LOCKSCREEN, - ), finishedStates) - } + ) + + // Expect another emission of LOCKSCREEN, as we have FINISHED a second transition to + // LOCKSCREEN after the cancellation. + assertEquals( + listOf( + LOCKSCREEN, + AOD, + LOCKSCREEN, + LOCKSCREEN, + ), + finishedStates + ) + } + + @Test + fun testCurrentState() = + testScope.runTest { + val currentStates by collectValues(underTest.currentKeyguardState) + + // We init the repo with a transition from OFF -> LOCKSCREEN. + assertEquals( + listOf( + OFF, + LOCKSCREEN, + ), + currentStates + ) + + sendSteps( + TransitionStep(LOCKSCREEN, AOD, 0f, STARTED), + ) + + // The current state should continue to be LOCKSCREEN as we transition to AOD. + assertEquals( + listOf( + OFF, + LOCKSCREEN, + ), + currentStates + ) + + sendSteps( + TransitionStep(LOCKSCREEN, AOD, 0.5f, RUNNING), + ) + + // The current state should continue to be LOCKSCREEN as we transition to AOD. + assertEquals( + listOf( + OFF, + LOCKSCREEN, + ), + currentStates + ) + + sendSteps( + TransitionStep(LOCKSCREEN, AOD, 0.6f, CANCELED), + ) + + // Once CANCELED, we're still currently in LOCKSCREEN... + assertEquals( + listOf( + OFF, + LOCKSCREEN, + ), + currentStates + ) + + sendSteps( + TransitionStep(AOD, LOCKSCREEN, 0.6f, STARTED), + ) + + // ...until STARTING back to LOCKSCREEN, at which point the "current" state should be + // the + // one we're transitioning from, despite never FINISHING in that state. + assertEquals( + listOf( + OFF, + LOCKSCREEN, + AOD, + ), + currentStates + ) + + sendSteps( + TransitionStep(AOD, LOCKSCREEN, 0.8f, RUNNING), + TransitionStep(AOD, LOCKSCREEN, 0.8f, FINISHED), + ) + + // FINSHING in LOCKSCREEN should update the current state to LOCKSCREEN. + assertEquals( + listOf( + OFF, + LOCKSCREEN, + AOD, + LOCKSCREEN, + ), + currentStates + ) + } + + @Test + fun testCurrentState_multipleCancellations_backToLastFinishedState() = + testScope.runTest { + val currentStates by collectValues(underTest.currentKeyguardState) + + // We init the repo with a transition from OFF -> LOCKSCREEN. + assertEquals( + listOf( + OFF, + LOCKSCREEN, + ), + currentStates + ) + + sendSteps( + TransitionStep(LOCKSCREEN, GONE, 0f, STARTED), + TransitionStep(LOCKSCREEN, GONE, 0.5f, RUNNING), + TransitionStep(LOCKSCREEN, GONE, 1f, FINISHED), + ) + + assertEquals( + listOf( + // Default transition from OFF -> LOCKSCREEN + OFF, + LOCKSCREEN, + // Transitioned to GONE + GONE, + ), + currentStates + ) + + sendSteps( + TransitionStep(GONE, DOZING, 0f, STARTED), + TransitionStep(GONE, DOZING, 0.5f, RUNNING), + TransitionStep(GONE, DOZING, 0.6f, CANCELED), + ) + + assertEquals( + listOf( + OFF, + LOCKSCREEN, + GONE, + // Current state should not be DOZING until the post-cancelation transition is + // STARTED + ), + currentStates + ) + + sendSteps( + TransitionStep(DOZING, LOCKSCREEN, 0f, STARTED), + ) + + assertEquals( + listOf( + OFF, + LOCKSCREEN, + GONE, + // DOZING -> LS STARTED, DOZING is now the current state. + DOZING, + ), + currentStates + ) + + sendSteps( + TransitionStep(DOZING, LOCKSCREEN, 0.5f, RUNNING), + TransitionStep(DOZING, LOCKSCREEN, 0.6f, CANCELED), + ) + + assertEquals( + listOf( + OFF, + LOCKSCREEN, + GONE, + DOZING, + ), + currentStates + ) + + sendSteps( + TransitionStep(LOCKSCREEN, GONE, 0f, STARTED), + ) + + assertEquals( + listOf( + OFF, + LOCKSCREEN, + GONE, + DOZING, + // LS -> GONE STARTED, LS is now the current state. + LOCKSCREEN, + ), + currentStates + ) + + sendSteps( + TransitionStep(LOCKSCREEN, GONE, 0.5f, RUNNING), + TransitionStep(LOCKSCREEN, GONE, 1f, FINISHED), + ) + + assertEquals( + listOf( + OFF, + LOCKSCREEN, + GONE, + DOZING, + LOCKSCREEN, + // FINISHED in GONE, GONE is now the current state. + GONE, + ), + currentStates + ) + } private suspend fun sendSteps(vararg steps: TransitionStep) { steps.forEach { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelTest.kt new file mode 100644 index 000000000000..d4dd2ac78e2a --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelTest.kt @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyguard.ui.viewmodel + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.keyguard.KeyguardClockSwitch +import com.android.systemui.SysuiTestCase +import com.android.systemui.biometrics.authController +import com.android.systemui.flags.Flags +import com.android.systemui.flags.fakeFeatureFlagsClassic +import com.android.systemui.keyguard.data.repository.fakeKeyguardClockRepository +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.testScope +import com.android.systemui.testKosmos +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 + +@SmallTest +@RunWith(AndroidJUnit4::class) +class LockscreenContentViewModelTest : SysuiTestCase() { + + private val kosmos: Kosmos = testKosmos() + + lateinit var underTest: LockscreenContentViewModel + + @Before + fun setup() { + with(kosmos) { + fakeFeatureFlagsClassic.set(Flags.LOCK_SCREEN_LONG_PRESS_ENABLED, true) + underTest = lockscreenContentViewModel + } + } + + @Test + fun isUdfpsVisible_withUdfps_true() = + with(kosmos) { + testScope.runTest { + whenever(kosmos.authController.isUdfpsSupported).thenReturn(true) + assertThat(underTest.isUdfpsVisible).isTrue() + } + } + + @Test + fun isUdfpsVisible_withoutUdfps_false() = + with(kosmos) { + testScope.runTest { + whenever(kosmos.authController.isUdfpsSupported).thenReturn(false) + assertThat(underTest.isUdfpsVisible).isFalse() + } + } + + @Test + fun isLargeClockVisible_withLargeClock_true() = + with(kosmos) { + testScope.runTest { + kosmos.fakeKeyguardClockRepository.setClockSize(KeyguardClockSwitch.LARGE) + assertThat(underTest.isLargeClockVisible).isTrue() + } + } + + @Test + fun isLargeClockVisible_withSmallClock_false() = + with(kosmos) { + testScope.runTest { + kosmos.fakeKeyguardClockRepository.setClockSize(KeyguardClockSwitch.SMALL) + assertThat(underTest.isLargeClockVisible).isFalse() + } + } + + @Test + fun areNotificationsVisible_withSmallClock_true() = + with(kosmos) { + testScope.runTest { + kosmos.fakeKeyguardClockRepository.setClockSize(KeyguardClockSwitch.SMALL) + assertThat(underTest.areNotificationsVisible).isTrue() + } + } + + @Test + fun areNotificationsVisible_withLargeClock_false() = + with(kosmos) { + testScope.runTest { + kosmos.fakeKeyguardClockRepository.setClockSize(KeyguardClockSwitch.LARGE) + assertThat(underTest.areNotificationsVisible).isFalse() + } + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModelTest.kt index 74d309c1d359..aa15d0befa80 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModelTest.kt @@ -21,11 +21,19 @@ package com.android.systemui.keyguard.ui.viewmodel import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.authentication.data.repository.fakeAuthenticationRepository import com.android.systemui.authentication.shared.model.AuthenticationMethodModel +import com.android.systemui.communal.data.repository.fakeCommunalRepository +import com.android.systemui.communal.domain.interactor.communalInteractor import com.android.systemui.coroutines.collectLastValue -import com.android.systemui.scene.SceneTestUtils +import com.android.systemui.deviceentry.data.repository.fakeDeviceEntryRepository +import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor +import com.android.systemui.kosmos.testScope +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.statusbar.notification.stack.ui.viewmodel.notificationsPlaceholderViewModel +import com.android.systemui.testKosmos import com.android.systemui.util.mockito.mock import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -37,9 +45,9 @@ import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class LockscreenSceneViewModelTest : SysuiTestCase() { - private val utils = SceneTestUtils(this) - private val testScope = utils.testScope - private val sceneInteractor = utils.sceneInteractor() + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + private val sceneInteractor = kosmos.sceneInteractor private val underTest = createLockscreenSceneViewModel() @@ -47,9 +55,11 @@ class LockscreenSceneViewModelTest : SysuiTestCase() { fun upTransitionSceneKey_canSwipeToUnlock_gone() = testScope.runTest { val upTransitionSceneKey by collectLastValue(underTest.upDestinationSceneKey) - utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.None) - utils.deviceEntryRepository.setLockscreenEnabled(true) - utils.deviceEntryRepository.setUnlocked(true) + kosmos.fakeAuthenticationRepository.setAuthenticationMethod( + AuthenticationMethodModel.None + ) + kosmos.fakeDeviceEntryRepository.setLockscreenEnabled(true) + kosmos.fakeDeviceEntryRepository.setUnlocked(true) sceneInteractor.changeScene(SceneModel(SceneKey.Lockscreen), "reason") assertThat(upTransitionSceneKey).isEqualTo(SceneKey.Gone) @@ -59,8 +69,10 @@ class LockscreenSceneViewModelTest : SysuiTestCase() { fun upTransitionSceneKey_cannotSwipeToUnlock_bouncer() = testScope.runTest { val upTransitionSceneKey by collectLastValue(underTest.upDestinationSceneKey) - utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin) - utils.deviceEntryRepository.setUnlocked(false) + kosmos.fakeAuthenticationRepository.setAuthenticationMethod( + AuthenticationMethodModel.Pin + ) + kosmos.fakeDeviceEntryRepository.setUnlocked(false) sceneInteractor.changeScene(SceneModel(SceneKey.Lockscreen), "reason") assertThat(upTransitionSceneKey).isEqualTo(SceneKey.Bouncer) @@ -69,7 +81,7 @@ class LockscreenSceneViewModelTest : SysuiTestCase() { @Test fun leftTransitionSceneKey_communalIsEnabled_communal() = testScope.runTest { - utils.communalRepository.setIsCommunalEnabled(true) + kosmos.fakeCommunalRepository.setIsCommunalEnabled(true) val underTest = createLockscreenSceneViewModel() assertThat(underTest.leftDestinationSceneKey).isEqualTo(SceneKey.Communal) @@ -78,7 +90,7 @@ class LockscreenSceneViewModelTest : SysuiTestCase() { @Test fun leftTransitionSceneKey_communalIsDisabled_null() = testScope.runTest { - utils.communalRepository.setIsCommunalEnabled(false) + kosmos.fakeCommunalRepository.setIsCommunalEnabled(false) val underTest = createLockscreenSceneViewModel() assertThat(underTest.leftDestinationSceneKey).isNull() @@ -87,17 +99,13 @@ class LockscreenSceneViewModelTest : SysuiTestCase() { private fun createLockscreenSceneViewModel(): LockscreenSceneViewModel { return LockscreenSceneViewModel( applicationScope = testScope.backgroundScope, - deviceEntryInteractor = - utils.deviceEntryInteractor( - authenticationInteractor = utils.authenticationInteractor(), - sceneInteractor = utils.sceneInteractor(), - ), - communalInteractor = utils.communalInteractor(), + deviceEntryInteractor = kosmos.deviceEntryInteractor, + communalInteractor = kosmos.communalInteractor, longPress = KeyguardLongPressViewModel( interactor = mock(), ), - notifications = utils.notificationsPlaceholderViewModel(), + notifications = kosmos.notificationsPlaceholderViewModel, ) } } 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 070e07a75d23..eb845b2b423c 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 @@ -17,11 +17,9 @@ package com.android.systemui.qs.pipeline.data.repository import android.Manifest.permission.BIND_QUICK_SETTINGS_TILE -import android.content.BroadcastReceiver import android.content.ComponentName import android.content.Context import android.content.Intent -import android.content.IntentFilter import android.content.pm.ApplicationInfo import android.content.pm.PackageManager import android.content.pm.PackageManager.ResolveInfoFlags @@ -33,44 +31,36 @@ import android.testing.TestableLooper import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.common.data.repository.fakePackageChangeRepository +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 import com.android.systemui.util.mockito.argThat -import com.android.systemui.util.mockito.argumentCaptor -import com.android.systemui.util.mockito.capture import com.android.systemui.util.mockito.eq import com.android.systemui.util.mockito.mock -import com.android.systemui.util.mockito.nullable import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.launch -import kotlinx.coroutines.test.StandardTestDispatcher -import kotlinx.coroutines.test.TestScope -import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.mockito.ArgumentCaptor import org.mockito.ArgumentMatchers.anyInt -import org.mockito.Captor import org.mockito.Mock -import org.mockito.Mockito.never -import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations @SmallTest @RunWith(AndroidJUnit4::class) @TestableLooper.RunWithLooper -@OptIn(ExperimentalCoroutinesApi::class) class InstalledTilesComponentRepositoryImplTest : SysuiTestCase() { - private val testDispatcher = StandardTestDispatcher() - private val testScope = TestScope(testDispatcher) + private val kosmos = testKosmos() + private val testScope = kosmos.testScope @Mock private lateinit var context: Context @Mock private lateinit var packageManager: PackageManager - @Captor private lateinit var receiverCaptor: ArgumentCaptor<BroadcastReceiver> private lateinit var underTest: InstalledTilesComponentRepositoryImpl @@ -92,63 +82,12 @@ class InstalledTilesComponentRepositoryImplTest : SysuiTestCase() { underTest = InstalledTilesComponentRepositoryImpl( context, - testDispatcher, + kosmos.testDispatcher, + kosmos.packageChangeRepository ) } @Test - fun registersAndUnregistersBroadcastReceiver() = - testScope.runTest { - val user = 10 - val job = launch { underTest.getInstalledTilesComponents(user).collect {} } - runCurrent() - - verify(context) - .registerReceiverAsUser( - capture(receiverCaptor), - eq(UserHandle.of(user)), - any(), - nullable(), - nullable(), - ) - - verify(context, never()).unregisterReceiver(receiverCaptor.value) - - job.cancel() - runCurrent() - verify(context).unregisterReceiver(receiverCaptor.value) - } - - @Test - fun intentFilterForCorrectActionsAndScheme() = - testScope.runTest { - val filterCaptor = argumentCaptor<IntentFilter>() - - backgroundScope.launch { underTest.getInstalledTilesComponents(0).collect {} } - runCurrent() - - verify(context) - .registerReceiverAsUser( - any(), - any(), - capture(filterCaptor), - nullable(), - nullable(), - ) - - with(filterCaptor.value) { - assertThat(matchAction(Intent.ACTION_PACKAGE_CHANGED)).isTrue() - assertThat(matchAction(Intent.ACTION_PACKAGE_ADDED)).isTrue() - assertThat(matchAction(Intent.ACTION_PACKAGE_REMOVED)).isTrue() - assertThat(matchAction(Intent.ACTION_PACKAGE_REPLACED)).isTrue() - assertThat(countActions()).isEqualTo(4) - - assertThat(hasDataScheme("package")).isTrue() - assertThat(countDataSchemes()).isEqualTo(1) - } - } - - @Test fun componentsLoadedOnStart() = testScope.runTest { val userId = 0 @@ -169,7 +108,7 @@ class InstalledTilesComponentRepositoryImplTest : SysuiTestCase() { } @Test - fun componentAdded_foundAfterBroadcast() = + fun componentAdded_foundAfterPackageChange() = testScope.runTest { val userId = 0 val resolveInfo = @@ -186,7 +125,7 @@ class InstalledTilesComponentRepositoryImplTest : SysuiTestCase() { ) ) .thenReturn(listOf(resolveInfo)) - getRegisteredReceiver().onReceive(context, Intent(Intent.ACTION_PACKAGE_ADDED)) + kosmos.fakePackageChangeRepository.notifyChange(PackageChangeModel.Empty) assertThat(componentNames).containsExactly(TEST_COMPONENT) } @@ -275,19 +214,6 @@ class InstalledTilesComponentRepositoryImplTest : SysuiTestCase() { assertThat(componentNames).containsExactly(TEST_COMPONENT) } - private fun getRegisteredReceiver(): BroadcastReceiver { - verify(context) - .registerReceiverAsUser( - capture(receiverCaptor), - any(), - any(), - nullable(), - nullable(), - ) - - return receiverCaptor.value - } - companion object { private val INTENT = Intent(TileService.ACTION_QS_TILE) private val FLAGS = 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 6403ed19a9b2..be523b813494 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 @@ -22,13 +22,15 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue import com.android.systemui.flags.FakeFeatureFlagsClassic import com.android.systemui.flags.Flags +import com.android.systemui.kosmos.testScope import com.android.systemui.qs.ui.adapter.FakeQSSceneAdapter -import com.android.systemui.scene.SceneTestUtils +import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.scene.shared.model.Direction import com.android.systemui.scene.shared.model.SceneKey import com.android.systemui.scene.shared.model.SceneModel 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 com.android.systemui.statusbar.pipeline.airplane.data.repository.FakeAirplaneModeRepository import com.android.systemui.statusbar.pipeline.airplane.domain.interactor.AirplaneModeInteractor import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionsRepository @@ -36,6 +38,7 @@ import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.FakeMobi import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconsViewModel 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.mock import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.test.runTest @@ -47,9 +50,9 @@ import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class QuickSettingsSceneViewModelTest : SysuiTestCase() { - private val utils = SceneTestUtils(this) - private val testScope = utils.testScope - private val sceneInteractor = utils.sceneInteractor() + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + private val sceneInteractor = kosmos.sceneInteractor private val mobileIconsInteractor = FakeMobileIconsInteractor(FakeMobileMappingsProxy(), mock()) private val flags = FakeFeatureFlagsClassic().also { it.set(Flags.NEW_NETWORK_SLICE_UI, false) } private val qsFlexiglassAdapter = FakeQSSceneAdapter { mock() } @@ -90,7 +93,7 @@ class QuickSettingsSceneViewModelTest : SysuiTestCase() { QuickSettingsSceneViewModel( shadeHeaderViewModel = shadeHeaderViewModel, qsSceneAdapter = qsFlexiglassAdapter, - notifications = utils.notificationsPlaceholderViewModel(), + notifications = kosmos.notificationsPlaceholderViewModel, ) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt index 530d127d17a9..1cd764e01bad 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt @@ -24,26 +24,41 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.internal.R import com.android.internal.util.EmergencyAffordanceManager +import com.android.internal.util.emergencyAffordanceManager import com.android.systemui.SysuiTestCase import com.android.systemui.authentication.data.repository.FakeAuthenticationRepository +import com.android.systemui.authentication.data.repository.fakeAuthenticationRepository +import com.android.systemui.authentication.domain.interactor.authenticationInteractor import com.android.systemui.authentication.shared.model.AuthenticationMethodModel import com.android.systemui.bouncer.domain.interactor.BouncerActionButtonInteractor +import com.android.systemui.bouncer.domain.interactor.bouncerActionButtonInteractor +import com.android.systemui.bouncer.domain.interactor.bouncerInteractor +import com.android.systemui.bouncer.domain.interactor.simBouncerInteractor import com.android.systemui.bouncer.ui.viewmodel.BouncerViewModel import com.android.systemui.bouncer.ui.viewmodel.PasswordBouncerViewModel import com.android.systemui.bouncer.ui.viewmodel.PinBouncerViewModel +import com.android.systemui.bouncer.ui.viewmodel.bouncerViewModel +import com.android.systemui.classifier.domain.interactor.falsingInteractor +import com.android.systemui.classifier.falsingCollector +import com.android.systemui.communal.domain.interactor.communalInteractor import com.android.systemui.coroutines.collectLastValue +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.keyguard.domain.interactor.keyguardInteractor import com.android.systemui.keyguard.ui.viewmodel.KeyguardLongPressViewModel import com.android.systemui.keyguard.ui.viewmodel.LockscreenSceneViewModel -import com.android.systemui.log.table.TableLogBuffer +import com.android.systemui.kosmos.testScope import com.android.systemui.media.controls.pipeline.MediaDataManager -import com.android.systemui.media.controls.ui.MediaHost import com.android.systemui.model.SysUiState 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.power.domain.interactor.powerInteractor import com.android.systemui.qs.ui.adapter.FakeQSSceneAdapter +import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.scene.domain.startable.SceneContainerStartable +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 @@ -51,16 +66,21 @@ import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModel import com.android.systemui.settings.FakeDisplayTracker import com.android.systemui.shade.ui.viewmodel.ShadeHeaderViewModel import com.android.systemui.shade.ui.viewmodel.ShadeSceneViewModel +import com.android.systemui.statusbar.notification.stack.ui.viewmodel.notificationsPlaceholderViewModel import com.android.systemui.statusbar.pipeline.airplane.data.repository.FakeAirplaneModeRepository import com.android.systemui.statusbar.pipeline.airplane.domain.interactor.AirplaneModeInteractor import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionsRepository +import com.android.systemui.statusbar.pipeline.mobile.data.repository.fakeMobileConnectionsRepository import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.FakeMobileIconsInteractor import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconsViewModel import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy import com.android.systemui.statusbar.pipeline.shared.data.repository.FakeConnectivityRepository +import com.android.systemui.telephony.data.repository.fakeTelephonyRepository +import com.android.systemui.testKosmos import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever +import com.android.telecom.telecomManager import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertWithMessage import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -101,28 +121,13 @@ import org.mockito.MockitoAnnotations @RunWith(AndroidJUnit4::class) class SceneFrameworkIntegrationTest : SysuiTestCase() { - @Mock private lateinit var emergencyAffordanceManager: EmergencyAffordanceManager - @Mock private lateinit var tableLogger: TableLogBuffer - @Mock private lateinit var telecomManager: TelecomManager - - private val utils = SceneTestUtils(this) - private val testScope = utils.testScope - private val sceneContainerConfig = utils.fakeSceneContainerConfig() - private val sceneRepository = - utils.fakeSceneContainerRepository( - containerConfig = sceneContainerConfig, - ) - private val sceneInteractor = - utils.sceneInteractor( - repository = sceneRepository, - ) - private val authenticationInteractor = utils.authenticationInteractor() - private val deviceEntryInteractor = - utils.deviceEntryInteractor( - authenticationInteractor = authenticationInteractor, - sceneInteractor = sceneInteractor, - ) - private val communalInteractor = utils.communalInteractor() + private val kosmos = testKosmos().apply { fakeSceneContainerFlags.enabled = true } + private val testScope = kosmos.testScope + private val sceneContainerConfig = kosmos.sceneContainerConfig + private val sceneInteractor = kosmos.sceneInteractor + private val authenticationInteractor = kosmos.authenticationInteractor + private val deviceEntryInteractor = kosmos.deviceEntryInteractor + private val communalInteractor = kosmos.communalInteractor private val transitionState = MutableStateFlow<ObservableTransitionState>( @@ -131,14 +136,11 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { private val sceneContainerViewModel = SceneContainerViewModel( sceneInteractor = sceneInteractor, - falsingInteractor = utils.falsingInteractor(), + falsingInteractor = kosmos.falsingInteractor, ) .apply { setTransitionState(transitionState) } - private val bouncerInteractor = - utils.bouncerInteractor( - authenticationInteractor = authenticationInteractor, - ) + private val bouncerInteractor = kosmos.bouncerInteractor private lateinit var mobileConnectionsRepository: FakeMobileConnectionsRepository private lateinit var bouncerActionButtonInteractor: BouncerActionButtonInteractor @@ -153,7 +155,7 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { KeyguardLongPressViewModel( interactor = mock(), ), - notifications = utils.notificationsPlaceholderViewModel(), + notifications = kosmos.notificationsPlaceholderViewModel, ) private val mobileIconsInteractor = FakeMobileIconsInteractor(FakeMobileMappingsProxy(), mock()) @@ -170,61 +172,51 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { FakeMobileConnectionsRepository(), ), constants = mock(), - utils.featureFlags, + flags = kosmos.fakeFeatureFlagsClassic, scope = testScope.backgroundScope, ) private lateinit var shadeHeaderViewModel: ShadeHeaderViewModel private lateinit var shadeSceneViewModel: ShadeSceneViewModel - private val keyguardRepository = utils.keyguardRepository - private val keyguardInteractor = - utils.keyguardInteractor( - repository = keyguardRepository, - ) - private val powerInteractor = PowerInteractorFactory.create().powerInteractor + private val keyguardInteractor = kosmos.keyguardInteractor + private val powerInteractor = kosmos.powerInteractor private var bouncerSceneJob: Job? = null private val qsFlexiglassAdapter = FakeQSSceneAdapter(inflateDelegate = { mock() }) @Mock private lateinit var mediaDataManager: MediaDataManager - @Mock private lateinit var mediaHost: MediaHost + + private lateinit var emergencyAffordanceManager: EmergencyAffordanceManager + private lateinit var telecomManager: TelecomManager @Before fun setUp() { MockitoAnnotations.initMocks(this) + overrideResource(R.bool.config_enable_emergency_call_while_sim_locked, true) + telecomManager = checkNotNull(kosmos.telecomManager) whenever(telecomManager.isInCall).thenReturn(false) + emergencyAffordanceManager = kosmos.emergencyAffordanceManager whenever(emergencyAffordanceManager.needsEmergencyAffordance()).thenReturn(true) - utils.featureFlags.apply { + kosmos.fakeFeatureFlagsClassic.apply { set(Flags.NEW_NETWORK_SLICE_UI, false) set(Flags.REFACTOR_GETCURRENTUSER, true) } - mobileConnectionsRepository = - FakeMobileConnectionsRepository(FakeMobileMappingsProxy(), tableLogger) - mobileConnectionsRepository.isAnySimSecure.value = true + mobileConnectionsRepository = kosmos.fakeMobileConnectionsRepository + mobileConnectionsRepository.isAnySimSecure.value = false - utils.telephonyRepository.apply { + kosmos.fakeTelephonyRepository.apply { setHasTelephonyRadio(true) setCallState(TelephonyManager.CALL_STATE_IDLE) setIsInCall(false) } - bouncerActionButtonInteractor = - utils.bouncerActionButtonInteractor( - mobileConnectionsRepository = mobileConnectionsRepository, - telecomManager = telecomManager, - emergencyAffordanceManager = emergencyAffordanceManager, - ) - bouncerViewModel = - utils.bouncerViewModel( - bouncerInteractor = bouncerInteractor, - authenticationInteractor = authenticationInteractor, - actionButtonInteractor = bouncerActionButtonInteractor, - ) + bouncerActionButtonInteractor = kosmos.bouncerActionButtonInteractor + bouncerViewModel = kosmos.bouncerViewModel shadeHeaderViewModel = ShadeHeaderViewModel( @@ -242,12 +234,11 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { deviceEntryInteractor = deviceEntryInteractor, shadeHeaderViewModel = shadeHeaderViewModel, qsSceneAdapter = qsFlexiglassAdapter, - notifications = utils.notificationsPlaceholderViewModel(), + notifications = kosmos.notificationsPlaceholderViewModel, mediaDataManager = mediaDataManager, - mediaHost = mediaHost, ) - utils.deviceEntryRepository.setUnlocked(false) + kosmos.fakeDeviceEntryRepository.setUnlocked(false) val displayTracker = FakeDisplayTracker(context) val sysUiState = SysUiState(displayTracker) @@ -257,15 +248,15 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { sceneInteractor = sceneInteractor, deviceEntryInteractor = deviceEntryInteractor, keyguardInteractor = keyguardInteractor, - flags = utils.sceneContainerFlags, + flags = kosmos.fakeSceneContainerFlags, sysUiState = sysUiState, displayId = displayTracker.defaultDisplayId, sceneLogger = mock(), - falsingCollector = utils.falsingCollector(), + falsingCollector = kosmos.falsingCollector, powerInteractor = powerInteractor, bouncerInteractor = bouncerInteractor, - simBouncerInteractor = dagger.Lazy { utils.simBouncerInteractor }, - authenticationInteractor = dagger.Lazy { utils.authenticationInteractor() }, + simBouncerInteractor = dagger.Lazy { kosmos.simBouncerInteractor }, + authenticationInteractor = dagger.Lazy { kosmos.authenticationInteractor }, windowController = mock(), ) startable.start() @@ -561,15 +552,15 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { // Set the lockscreen enabled bit _before_ set the auth method as the code picks up on the // lockscreen enabled bit _after_ the auth method is changed and the lockscreen enabled bit // is not an observable that can trigger a new evaluation. - utils.deviceEntryRepository.setLockscreenEnabled(enableLockscreen) - utils.authenticationRepository.setAuthenticationMethod(authMethod) + kosmos.fakeDeviceEntryRepository.setLockscreenEnabled(enableLockscreen) + kosmos.fakeAuthenticationRepository.setAuthenticationMethod(authMethod) runCurrent() } /** Emulates a phone call in progress. */ private fun TestScope.startPhoneCall() { whenever(telecomManager.isInCall).thenReturn(true) - utils.telephonyRepository.apply { + kosmos.fakeTelephonyRepository.apply { setHasTelephonyRadio(true) setIsInCall(true) setCallState(TelephonyManager.CALL_STATE_OFFHOOK) @@ -678,7 +669,7 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { .that(authMethod.isSecure) .isTrue() - utils.deviceEntryRepository.setUnlocked(false) + kosmos.fakeDeviceEntryRepository.setUnlocked(false) runCurrent() } @@ -692,7 +683,7 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { enterPin() // This repository state is not changed by the AuthInteractor, it relies on // KeyguardStateController. - utils.deviceEntryRepository.setUnlocked(true) + kosmos.fakeDeviceEntryRepository.setUnlocked(true) emulateUiSceneTransition( expectedVisible = false, ) @@ -748,7 +739,7 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { } pinBouncerViewModel.onAuthenticateButtonClicked() setAuthMethod(authMethodAfterSimUnlock) - utils.mobileConnectionsRepository.isAnySimSecure.value = false + kosmos.fakeMobileConnectionsRepository.isAnySimSecure.value = false runCurrent() } @@ -795,7 +786,7 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { private fun TestScope.introduceLockedSim() { setAuthMethod(AuthenticationMethodModel.Sim) - utils.mobileConnectionsRepository.isAnySimSecure.value = true + kosmos.fakeMobileConnectionsRepository.isAnySimSecure.value = true runCurrent() } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/data/repository/SceneContainerRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/data/repository/SceneContainerRepositoryTest.kt index ddeb05b39e53..b267720971cd 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/data/repository/SceneContainerRepositoryTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/data/repository/SceneContainerRepositoryTest.kt @@ -22,11 +22,14 @@ 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.scene.SceneTestUtils +import com.android.systemui.kosmos.testScope +import com.android.systemui.scene.sceneContainerConfig import com.android.systemui.scene.sceneKeys +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.testKosmos import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow @@ -39,12 +42,12 @@ import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class SceneContainerRepositoryTest : SysuiTestCase() { - private val utils = SceneTestUtils(this) - private val testScope = utils.testScope + private val kosmos = testKosmos().apply { fakeSceneContainerFlags.enabled = true } + private val testScope = kosmos.testScope @Test fun allSceneKeys() { - val underTest = utils.fakeSceneContainerRepository() + val underTest = kosmos.sceneContainerRepository assertThat(underTest.allSceneKeys()) .isEqualTo( listOf( @@ -61,7 +64,7 @@ class SceneContainerRepositoryTest : SysuiTestCase() { @Test fun desiredScene() = testScope.runTest { - val underTest = utils.fakeSceneContainerRepository() + val underTest = kosmos.sceneContainerRepository val currentScene by collectLastValue(underTest.desiredScene) assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Lockscreen)) @@ -71,15 +74,15 @@ class SceneContainerRepositoryTest : SysuiTestCase() { @Test(expected = IllegalStateException::class) fun setDesiredScene_noSuchSceneInContainer_throws() { - utils.kosmos.sceneKeys = listOf(SceneKey.QuickSettings, SceneKey.Lockscreen) - val underTest = utils.fakeSceneContainerRepository(utils.fakeSceneContainerConfig()) + kosmos.sceneKeys = listOf(SceneKey.QuickSettings, SceneKey.Lockscreen) + val underTest = kosmos.sceneContainerRepository underTest.setDesiredScene(SceneModel(SceneKey.Shade)) } @Test fun isVisible() = testScope.runTest { - val underTest = utils.fakeSceneContainerRepository() + val underTest = kosmos.sceneContainerRepository val isVisible by collectLastValue(underTest.isVisible) assertThat(isVisible).isTrue() @@ -93,19 +96,19 @@ class SceneContainerRepositoryTest : SysuiTestCase() { @Test fun transitionState_defaultsToIdle() = testScope.runTest { - val underTest = utils.fakeSceneContainerRepository() + val underTest = kosmos.sceneContainerRepository val transitionState by collectLastValue(underTest.transitionState) assertThat(transitionState) .isEqualTo( - ObservableTransitionState.Idle(utils.fakeSceneContainerConfig().initialSceneKey) + ObservableTransitionState.Idle(kosmos.sceneContainerConfig.initialSceneKey) ) } @Test fun transitionState_reflectsUpdates() = testScope.runTest { - val underTest = utils.fakeSceneContainerRepository() + val underTest = kosmos.sceneContainerRepository val transitionState = MutableStateFlow<ObservableTransitionState>( ObservableTransitionState.Idle(SceneKey.Lockscreen) @@ -134,7 +137,7 @@ class SceneContainerRepositoryTest : SysuiTestCase() { underTest.setTransitionState(null) assertThat(reflectedTransitionState) .isEqualTo( - ObservableTransitionState.Idle(utils.fakeSceneContainerConfig().initialSceneKey) + ObservableTransitionState.Idle(kosmos.sceneContainerConfig.initialSceneKey) ) } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt index 7f4bbbe36768..d159986015a0 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt @@ -20,14 +20,21 @@ 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.scene.SceneTestUtils +import com.android.systemui.kosmos.testScope +import com.android.systemui.power.data.repository.fakePowerRepository +import com.android.systemui.scene.data.repository.sceneContainerRepository +import com.android.systemui.scene.sceneContainerConfig +import com.android.systemui.scene.sceneKeys +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.testKosmos import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runTest +import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -35,14 +42,20 @@ import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class SceneInteractorTest : SysuiTestCase() { - private val utils = SceneTestUtils(this) - private val testScope = utils.testScope - private val repository = utils.fakeSceneContainerRepository() - private val underTest = utils.sceneInteractor(repository = repository) + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + + private lateinit var underTest: SceneInteractor + + @Before + fun setUp() { + kosmos.fakeSceneContainerFlags.enabled = true + underTest = kosmos.sceneInteractor + } @Test fun allSceneKeys() { - assertThat(underTest.allSceneKeys()).isEqualTo(utils.fakeSceneKeys()) + assertThat(underTest.allSceneKeys()).isEqualTo(kosmos.sceneKeys) } @Test @@ -68,7 +81,7 @@ class SceneInteractorTest : SysuiTestCase() { @Test fun transitionState() = testScope.runTest { - val underTest = utils.fakeSceneContainerRepository() + val underTest = kosmos.sceneContainerRepository val transitionState = MutableStateFlow<ObservableTransitionState>( ObservableTransitionState.Idle(SceneKey.Lockscreen) @@ -97,7 +110,7 @@ class SceneInteractorTest : SysuiTestCase() { underTest.setTransitionState(null) assertThat(reflectedTransitionState) .isEqualTo( - ObservableTransitionState.Idle(utils.fakeSceneContainerConfig().initialSceneKey) + ObservableTransitionState.Idle(kosmos.sceneContainerConfig.initialSceneKey) ) } @@ -341,8 +354,8 @@ class SceneInteractorTest : SysuiTestCase() { @Test fun userInput() = testScope.runTest { - assertThat(utils.powerRepository.userTouchRegistered).isFalse() + assertThat(kosmos.fakePowerRepository.userTouchRegistered).isFalse() underTest.onUserInput() - assertThat(utils.powerRepository.userTouchRegistered).isTrue() + assertThat(kosmos.fakePowerRepository.userTouchRegistered).isTrue() } } 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 8be4eeb7be7a..f23716ccca54 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 @@ -16,6 +16,8 @@ package com.android.systemui.scene.domain.interactor +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.internal.statusbar.IStatusBarService @@ -28,7 +30,11 @@ import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.se import com.android.systemui.power.domain.interactor.PowerInteractorFactory import com.android.systemui.scene.data.repository.WindowRootViewVisibilityRepository import com.android.systemui.statusbar.NotificationPresenter +import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationListRepository +import com.android.systemui.statusbar.notification.data.repository.setActiveNotifs +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 com.android.systemui.util.concurrency.FakeExecutor import com.android.systemui.util.mockito.any @@ -37,6 +43,7 @@ import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever 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 @@ -50,6 +57,7 @@ import org.mockito.Mockito.verify class WindowRootViewVisibilityInteractorTest : SysuiTestCase() { private val testScope = TestScope() + private val testDispatcher = StandardTestDispatcher() private val iStatusBarService = mock<IStatusBarService>() private val executor = FakeExecutor(FakeSystemClock()) private val windowRootViewVisibilityRepository = @@ -59,6 +67,9 @@ class WindowRootViewVisibilityInteractorTest : SysuiTestCase() { private val notificationPresenter = mock<NotificationPresenter>() private val notificationsController = mock<NotificationsController>() private val powerInteractor = PowerInteractorFactory.create().powerInteractor + private val activeNotificationsRepository = ActiveNotificationListRepository() + private val activeNotificationsInteractor = + ActiveNotificationsInteractor(activeNotificationsRepository, testDispatcher) private val underTest = WindowRootViewVisibilityInteractor( @@ -67,6 +78,7 @@ class WindowRootViewVisibilityInteractorTest : SysuiTestCase() { keyguardRepository, headsUpManager, powerInteractor, + activeNotificationsInteractor, ) .apply { setUp(notificationPresenter, notificationsController) } @@ -257,7 +269,8 @@ class WindowRootViewVisibilityInteractorTest : SysuiTestCase() { } @Test - fun lockscreenShadeInteractive_hasHeadsUpAndNotifPresenterCollapsed_notifCountOne() = + @DisableFlags(NotificationsLiveDataStoreRefactor.FLAG_NAME) + fun lockscreenShadeInteractive_hasHeadsUpAndNotifPresenterCollapsed_flagOff_notifCountOne() = testScope.runTest { underTest.start() @@ -273,6 +286,23 @@ class WindowRootViewVisibilityInteractorTest : SysuiTestCase() { } @Test + @EnableFlags(NotificationsLiveDataStoreRefactor.FLAG_NAME) + fun lockscreenShadeInteractive_hasHeadsUpAndNotifPresenterCollapsed_flagOn_notifCountOne() = + testScope.runTest { + underTest.start() + + whenever(headsUpManager.hasPinnedHeadsUp()).thenReturn(true) + whenever(notificationPresenter.isPresenterFullyCollapsed).thenReturn(true) + activeNotificationsRepository.setActiveNotifs(4) + + makeLockscreenShadeVisible() + + val notifCount = argumentCaptor<Int>() + verify(iStatusBarService).onPanelRevealed(any(), notifCount.capture()) + assertThat(notifCount.value).isEqualTo(1) + } + + @Test fun lockscreenShadeInteractive_hasHeadsUpAndNullPresenter_notifCountOne() = testScope.runTest { underTest.start() @@ -288,7 +318,8 @@ class WindowRootViewVisibilityInteractorTest : SysuiTestCase() { } @Test - fun lockscreenShadeInteractive_noHeadsUp_notifCountMatchesNotifController() = + @DisableFlags(NotificationsLiveDataStoreRefactor.FLAG_NAME) + fun lockscreenShadeInteractive_noHeadsUp_flagOff_notifCountMatchesNotifController() = testScope.runTest { underTest.start() whenever(notificationPresenter.isPresenterFullyCollapsed).thenReturn(true) @@ -304,7 +335,25 @@ class WindowRootViewVisibilityInteractorTest : SysuiTestCase() { } @Test - fun lockscreenShadeInteractive_notifPresenterNotCollapsed_notifCountMatchesNotifController() = + @EnableFlags(NotificationsLiveDataStoreRefactor.FLAG_NAME) + fun lockscreenShadeInteractive_noHeadsUp_flagOn_notifCountMatchesNotifController() = + testScope.runTest { + underTest.start() + whenever(notificationPresenter.isPresenterFullyCollapsed).thenReturn(true) + + whenever(headsUpManager.hasPinnedHeadsUp()).thenReturn(false) + activeNotificationsRepository.setActiveNotifs(9) + + makeLockscreenShadeVisible() + + val notifCount = argumentCaptor<Int>() + verify(iStatusBarService).onPanelRevealed(any(), notifCount.capture()) + assertThat(notifCount.value).isEqualTo(9) + } + + @Test + @DisableFlags(NotificationsLiveDataStoreRefactor.FLAG_NAME) + fun lockscreenShadeInteractive_notifPresenterNotCollapsed_flagOff_notifCountMatchesNotifController() = testScope.runTest { underTest.start() whenever(headsUpManager.hasPinnedHeadsUp()).thenReturn(true) @@ -320,6 +369,23 @@ class WindowRootViewVisibilityInteractorTest : SysuiTestCase() { } @Test + @EnableFlags(NotificationsLiveDataStoreRefactor.FLAG_NAME) + fun lockscreenShadeInteractive_notifPresenterNotCollapsed_flagOn_notifCountMatchesNotifController() = + testScope.runTest { + underTest.start() + whenever(headsUpManager.hasPinnedHeadsUp()).thenReturn(true) + + whenever(notificationPresenter.isPresenterFullyCollapsed).thenReturn(false) + activeNotificationsRepository.setActiveNotifs(8) + + makeLockscreenShadeVisible() + + val notifCount = argumentCaptor<Int>() + verify(iStatusBarService).onPanelRevealed(any(), notifCount.capture()) + assertThat(notifCount.value).isEqualTo(8) + } + + @Test fun lockscreenShadeInteractive_noHeadsUp_noNotifController_notifCountZero() = testScope.runTest { underTest.start() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt index dd22976ee9e1..4afa5f2a44b9 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt @@ -25,19 +25,31 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.Flags as AconfigFlags import com.android.systemui.SysuiTestCase +import com.android.systemui.authentication.data.repository.fakeAuthenticationRepository +import com.android.systemui.authentication.domain.interactor.authenticationInteractor import com.android.systemui.authentication.shared.model.AuthenticationMethodModel +import com.android.systemui.bouncer.domain.interactor.bouncerInteractor +import com.android.systemui.bouncer.domain.interactor.simBouncerInteractor import com.android.systemui.classifier.FalsingCollector import com.android.systemui.coroutines.collectLastValue -import com.android.systemui.keyguard.data.repository.FakeDeviceEntryFaceAuthRepository +import com.android.systemui.deviceentry.data.repository.fakeDeviceEntryRepository +import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor +import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFaceAuthRepository +import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository +import com.android.systemui.keyguard.domain.interactor.keyguardInteractor +import com.android.systemui.kosmos.testScope import com.android.systemui.model.SysUiState 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.SceneTestUtils +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.statusbar.NotificationShadeWindowController +import com.android.systemui.statusbar.pipeline.mobile.data.repository.fakeMobileConnectionsRepository +import com.android.systemui.testKosmos import com.android.systemui.util.mockito.mock import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -65,21 +77,15 @@ class SceneContainerStartableTest : SysuiTestCase() { @Mock private lateinit var windowController: NotificationShadeWindowController - private val utils = SceneTestUtils(this) - private val testScope = utils.testScope - private val sceneInteractor = utils.sceneInteractor() - private val sceneContainerFlags = utils.sceneContainerFlags - private val authenticationInteractor = utils.authenticationInteractor() - private val bouncerInteractor = - utils.bouncerInteractor(authenticationInteractor = authenticationInteractor) - private val faceAuthRepository = FakeDeviceEntryFaceAuthRepository() - private val deviceEntryInteractor = - utils.deviceEntryInteractor( - faceAuthRepository = faceAuthRepository, - authenticationInteractor = authenticationInteractor, - sceneInteractor = sceneInteractor, - ) - private val keyguardInteractor = utils.keyguardInteractor() + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + private val sceneInteractor = kosmos.sceneInteractor + private val sceneContainerFlags = kosmos.fakeSceneContainerFlags + private val authenticationInteractor = kosmos.authenticationInteractor + private val bouncerInteractor = kosmos.bouncerInteractor + private val faceAuthRepository = kosmos.fakeDeviceEntryFaceAuthRepository + private val deviceEntryInteractor = kosmos.deviceEntryInteractor + private val keyguardInteractor = kosmos.keyguardInteractor private val sysUiState: SysUiState = mock() private val falsingCollector: FalsingCollector = mock() private val powerInteractor = PowerInteractorFactory.create().powerInteractor @@ -103,7 +109,7 @@ class SceneContainerStartableTest : SysuiTestCase() { falsingCollector = falsingCollector, powerInteractor = powerInteractor, bouncerInteractor = bouncerInteractor, - simBouncerInteractor = dagger.Lazy { utils.simBouncerInteractor }, + simBouncerInteractor = dagger.Lazy { kosmos.simBouncerInteractor }, authenticationInteractor = dagger.Lazy { authenticationInteractor }, windowController = windowController, ) @@ -178,7 +184,7 @@ class SceneContainerStartableTest : SysuiTestCase() { assertThat(currentSceneKey).isEqualTo(SceneKey.Gone) underTest.start() - utils.deviceEntryRepository.setUnlocked(false) + kosmos.fakeDeviceEntryRepository.setUnlocked(false) assertThat(currentSceneKey).isEqualTo(SceneKey.Lockscreen) } @@ -194,7 +200,7 @@ class SceneContainerStartableTest : SysuiTestCase() { assertThat(currentSceneKey).isEqualTo(SceneKey.Bouncer) underTest.start() - utils.deviceEntryRepository.setUnlocked(true) + kosmos.fakeDeviceEntryRepository.setUnlocked(true) assertThat(currentSceneKey).isEqualTo(SceneKey.Gone) } @@ -210,7 +216,7 @@ class SceneContainerStartableTest : SysuiTestCase() { assertThat(currentSceneKey).isEqualTo(SceneKey.Lockscreen) underTest.start() - utils.deviceEntryRepository.setUnlocked(true) + kosmos.fakeDeviceEntryRepository.setUnlocked(true) assertThat(currentSceneKey).isEqualTo(SceneKey.Gone) } @@ -228,7 +234,7 @@ class SceneContainerStartableTest : SysuiTestCase() { // Authenticate using a passive auth method like face auth while bypass is disabled. faceAuthRepository.isAuthenticated.value = true - utils.deviceEntryRepository.setUnlocked(true) + kosmos.fakeDeviceEntryRepository.setUnlocked(true) assertThat(currentSceneKey).isEqualTo(SceneKey.Lockscreen) } @@ -250,7 +256,7 @@ class SceneContainerStartableTest : SysuiTestCase() { transitionStateFlowValue.value = ObservableTransitionState.Idle(SceneKey.Shade) assertThat(currentSceneKey).isEqualTo(SceneKey.Shade) - utils.deviceEntryRepository.setUnlocked(true) + kosmos.fakeDeviceEntryRepository.setUnlocked(true) runCurrent() assertThat(currentSceneKey).isEqualTo(SceneKey.Shade) @@ -269,7 +275,7 @@ class SceneContainerStartableTest : SysuiTestCase() { // Authenticate using a passive auth method like face auth while bypass is disabled. faceAuthRepository.isAuthenticated.value = true - utils.deviceEntryRepository.setUnlocked(true) + kosmos.fakeDeviceEntryRepository.setUnlocked(true) assertThat(currentSceneKey).isEqualTo(SceneKey.Gone) } @@ -379,7 +385,7 @@ class SceneContainerStartableTest : SysuiTestCase() { assertThat(currentSceneKey).isEqualTo(SceneKey.Lockscreen) underTest.start() - utils.deviceEntryRepository.setUnlocked(true) + kosmos.fakeDeviceEntryRepository.setUnlocked(true) runCurrent() powerInteractor.setAwakeForTest() runCurrent() @@ -469,11 +475,11 @@ class SceneContainerStartableTest : SysuiTestCase() { runCurrent() verify(falsingCollector).setShowingAod(false) - utils.keyguardRepository.setIsDozing(true) + kosmos.fakeKeyguardRepository.setIsDozing(true) runCurrent() verify(falsingCollector).setShowingAod(true) - utils.keyguardRepository.setIsDozing(false) + kosmos.fakeKeyguardRepository.setIsDozing(false) runCurrent() verify(falsingCollector, times(2)).setShowingAod(false) } @@ -499,7 +505,7 @@ class SceneContainerStartableTest : SysuiTestCase() { @Test fun collectFalsingSignals_screenOnAndOff_aodUnavailable() = testScope.runTest { - utils.keyguardRepository.setAodAvailable(false) + kosmos.fakeKeyguardRepository.setAodAvailable(false) runCurrent() prepareState( initialSceneKey = SceneKey.Lockscreen, @@ -547,7 +553,7 @@ class SceneContainerStartableTest : SysuiTestCase() { @Test fun collectFalsingSignals_screenOnAndOff_aodAvailable() = testScope.runTest { - utils.keyguardRepository.setAodAvailable(true) + kosmos.fakeKeyguardRepository.setAodAvailable(true) runCurrent() prepareState( initialSceneKey = SceneKey.Lockscreen, @@ -625,7 +631,7 @@ class SceneContainerStartableTest : SysuiTestCase() { underTest.start() runCurrent() - utils.mobileConnectionsRepository.isAnySimSecure.value = true + kosmos.fakeMobileConnectionsRepository.isAnySimSecure.value = true runCurrent() assertThat(currentSceneKey).isEqualTo(SceneKey.Bouncer) @@ -634,7 +640,7 @@ class SceneContainerStartableTest : SysuiTestCase() { @Test fun switchesToLockscreen_whenSimBecomesUnlocked() = testScope.runTest { - utils.mobileConnectionsRepository.isAnySimSecure.value = true + kosmos.fakeMobileConnectionsRepository.isAnySimSecure.value = true val currentSceneKey by collectLastValue(sceneInteractor.desiredScene.map { it.key }) prepareState( @@ -644,7 +650,7 @@ class SceneContainerStartableTest : SysuiTestCase() { ) underTest.start() runCurrent() - utils.mobileConnectionsRepository.isAnySimSecure.value = false + kosmos.fakeMobileConnectionsRepository.isAnySimSecure.value = false runCurrent() assertThat(currentSceneKey).isEqualTo(SceneKey.Lockscreen) @@ -653,7 +659,7 @@ class SceneContainerStartableTest : SysuiTestCase() { @Test fun switchesToGone_whenSimBecomesUnlocked_ifDeviceUnlockedAndLockscreenDisabled() = testScope.runTest { - utils.mobileConnectionsRepository.isAnySimSecure.value = true + kosmos.fakeMobileConnectionsRepository.isAnySimSecure.value = true val currentSceneKey by collectLastValue(sceneInteractor.desiredScene.map { it.key }) prepareState( @@ -664,7 +670,7 @@ class SceneContainerStartableTest : SysuiTestCase() { ) underTest.start() runCurrent() - utils.mobileConnectionsRepository.isAnySimSecure.value = false + kosmos.fakeMobileConnectionsRepository.isAnySimSecure.value = false runCurrent() assertThat(currentSceneKey).isEqualTo(SceneKey.Gone) @@ -736,8 +742,8 @@ class SceneContainerStartableTest : SysuiTestCase() { } } sceneContainerFlags.enabled = true - utils.deviceEntryRepository.setUnlocked(isDeviceUnlocked) - utils.deviceEntryRepository.setBypassEnabled(isBypassEnabled) + kosmos.fakeDeviceEntryRepository.setUnlocked(isDeviceUnlocked) + kosmos.fakeDeviceEntryRepository.setBypassEnabled(isBypassEnabled) val transitionStateFlow = MutableStateFlow<ObservableTransitionState>( ObservableTransitionState.Idle(SceneKey.Lockscreen) @@ -749,8 +755,8 @@ class SceneContainerStartableTest : SysuiTestCase() { sceneInteractor.onSceneChanged(SceneModel(it), "reason") } authenticationMethod?.let { - utils.authenticationRepository.setAuthenticationMethod(authenticationMethod) - utils.deviceEntryRepository.setLockscreenEnabled( + kosmos.fakeAuthenticationRepository.setAuthenticationMethod(authenticationMethod) + kosmos.fakeDeviceEntryRepository.setLockscreenEnabled( isLockscreenEnabled = isLockscreenEnabled ) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt index c89cd9e0c1f1..ede453d85ee1 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt @@ -21,13 +21,18 @@ package com.android.systemui.scene.ui.viewmodel import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.classifier.domain.interactor.falsingInteractor import com.android.systemui.coroutines.collectLastValue -import com.android.systemui.scene.SceneTestUtils +import com.android.systemui.scene.domain.interactor.sceneInteractor +import com.android.systemui.scene.sceneKeys +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.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest +import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -35,13 +40,19 @@ import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class SceneContainerViewModelTest : SysuiTestCase() { - private val utils = SceneTestUtils(this) - private val interactor = utils.sceneInteractor() - private val underTest = - SceneContainerViewModel( - sceneInteractor = interactor, - falsingInteractor = utils.falsingInteractor(), - ) + private val kosmos = testKosmos() + private val interactor = kosmos.sceneInteractor + private lateinit var underTest: SceneContainerViewModel + + @Before + fun setUp() { + kosmos.fakeSceneContainerFlags.enabled = true + underTest = + SceneContainerViewModel( + sceneInteractor = interactor, + falsingInteractor = kosmos.falsingInteractor, + ) + } @Test fun isVisible() = runTest { @@ -57,7 +68,7 @@ class SceneContainerViewModelTest : SysuiTestCase() { @Test fun allSceneKeys() { - assertThat(underTest.allSceneKeys).isEqualTo(utils.fakeSceneKeys()) + assertThat(underTest.allSceneKeys).isEqualTo(kosmos.sceneKeys) } @Test diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelTest.kt index 77ddf15c41ad..51745023c5be 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelTest.kt @@ -7,7 +7,8 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue import com.android.systemui.flags.FakeFeatureFlagsClassic import com.android.systemui.flags.Flags -import com.android.systemui.scene.SceneTestUtils +import com.android.systemui.kosmos.testScope +import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.scene.shared.model.ObservableTransitionState import com.android.systemui.scene.shared.model.SceneKey import com.android.systemui.statusbar.pipeline.airplane.data.repository.FakeAirplaneModeRepository @@ -18,6 +19,7 @@ import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.FakeMobi import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconsViewModel 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.mock import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.flow.MutableStateFlow @@ -31,9 +33,9 @@ import org.mockito.MockitoAnnotations @SmallTest @RunWith(AndroidJUnit4::class) class ShadeHeaderViewModelTest : SysuiTestCase() { - private val utils = SceneTestUtils(this) - private val testScope = utils.testScope - private val sceneInteractor = utils.sceneInteractor() + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + private val sceneInteractor = kosmos.sceneInteractor private val mobileIconsInteractor = FakeMobileIconsInteractor(FakeMobileMappingsProxy(), mock()) private val flags = FakeFeatureFlagsClassic().also { it.set(Flags.NEW_NETWORK_SLICE_UI, false) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt index 1d2497dfd716..e9801652f060 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt @@ -19,16 +19,20 @@ package com.android.systemui.shade.ui.viewmodel import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.authentication.data.repository.fakeAuthenticationRepository import com.android.systemui.authentication.shared.model.AuthenticationMethodModel import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.deviceentry.data.repository.fakeDeviceEntryRepository +import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor import com.android.systemui.flags.FakeFeatureFlagsClassic import com.android.systemui.flags.Flags +import com.android.systemui.kosmos.testScope import com.android.systemui.media.controls.pipeline.MediaDataManager -import com.android.systemui.media.controls.ui.MediaHost import com.android.systemui.qs.ui.adapter.FakeQSSceneAdapter -import com.android.systemui.scene.SceneTestUtils +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.statusbar.notification.stack.ui.viewmodel.notificationsPlaceholderViewModel import com.android.systemui.statusbar.pipeline.airplane.data.repository.FakeAirplaneModeRepository import com.android.systemui.statusbar.pipeline.airplane.domain.interactor.AirplaneModeInteractor import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionsRepository @@ -36,6 +40,7 @@ import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.FakeMobi import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconsViewModel 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.mock import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat @@ -53,15 +58,10 @@ import org.mockito.MockitoAnnotations @RunWith(AndroidJUnit4::class) class ShadeSceneViewModelTest : SysuiTestCase() { - private val utils = SceneTestUtils(this) - private val testScope = utils.testScope - private val sceneInteractor = utils.sceneInteractor() - private val authenticationInteractor = utils.authenticationInteractor() - private val deviceEntryInteractor = - utils.deviceEntryInteractor( - authenticationInteractor = authenticationInteractor, - sceneInteractor = sceneInteractor, - ) + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + private val sceneInteractor = kosmos.sceneInteractor + private val deviceEntryInteractor = kosmos.deviceEntryInteractor private val mobileIconsInteractor = FakeMobileIconsInteractor(FakeMobileMappingsProxy(), mock()) private val flags = FakeFeatureFlagsClassic().also { it.set(Flags.NEW_NETWORK_SLICE_UI, false) } @@ -89,7 +89,6 @@ class ShadeSceneViewModelTest : SysuiTestCase() { private lateinit var underTest: ShadeSceneViewModel @Mock private lateinit var mediaDataManager: MediaDataManager - @Mock private lateinit var mediaHost: MediaHost @Before fun setUp() { @@ -110,9 +109,8 @@ class ShadeSceneViewModelTest : SysuiTestCase() { deviceEntryInteractor = deviceEntryInteractor, shadeHeaderViewModel = shadeHeaderViewModel, qsSceneAdapter = qsFlexiglassAdapter, - notifications = utils.notificationsPlaceholderViewModel(), + notifications = kosmos.notificationsPlaceholderViewModel, mediaDataManager = mediaDataManager, - mediaHost = mediaHost, ) } @@ -120,8 +118,10 @@ class ShadeSceneViewModelTest : SysuiTestCase() { fun upTransitionSceneKey_deviceLocked_lockScreen() = testScope.runTest { val upTransitionSceneKey by collectLastValue(underTest.upDestinationSceneKey) - utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin) - utils.deviceEntryRepository.setUnlocked(false) + kosmos.fakeAuthenticationRepository.setAuthenticationMethod( + AuthenticationMethodModel.Pin + ) + kosmos.fakeDeviceEntryRepository.setUnlocked(false) assertThat(upTransitionSceneKey).isEqualTo(SceneKey.Lockscreen) } @@ -130,8 +130,10 @@ class ShadeSceneViewModelTest : SysuiTestCase() { fun upTransitionSceneKey_deviceUnlocked_gone() = testScope.runTest { val upTransitionSceneKey by collectLastValue(underTest.upDestinationSceneKey) - utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin) - utils.deviceEntryRepository.setUnlocked(true) + kosmos.fakeAuthenticationRepository.setAuthenticationMethod( + AuthenticationMethodModel.Pin + ) + kosmos.fakeDeviceEntryRepository.setUnlocked(true) assertThat(upTransitionSceneKey).isEqualTo(SceneKey.Gone) } @@ -140,8 +142,10 @@ class ShadeSceneViewModelTest : SysuiTestCase() { fun upTransitionSceneKey_authMethodSwipe_lockscreenNotDismissed_goesToLockscreen() = testScope.runTest { val upTransitionSceneKey by collectLastValue(underTest.upDestinationSceneKey) - utils.deviceEntryRepository.setLockscreenEnabled(true) - utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.None) + kosmos.fakeDeviceEntryRepository.setLockscreenEnabled(true) + kosmos.fakeAuthenticationRepository.setAuthenticationMethod( + AuthenticationMethodModel.None + ) sceneInteractor.changeScene(SceneModel(SceneKey.Lockscreen), "reason") sceneInteractor.onSceneChanged(SceneModel(SceneKey.Lockscreen), "reason") @@ -152,8 +156,10 @@ class ShadeSceneViewModelTest : SysuiTestCase() { fun upTransitionSceneKey_authMethodSwipe_lockscreenDismissed_goesToGone() = testScope.runTest { val upTransitionSceneKey by collectLastValue(underTest.upDestinationSceneKey) - utils.deviceEntryRepository.setLockscreenEnabled(true) - utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.None) + kosmos.fakeDeviceEntryRepository.setLockscreenEnabled(true) + kosmos.fakeAuthenticationRepository.setAuthenticationMethod( + AuthenticationMethodModel.None + ) sceneInteractor.changeScene(SceneModel(SceneKey.Gone), "reason") sceneInteractor.onSceneChanged(SceneModel(SceneKey.Gone), "reason") @@ -164,8 +170,10 @@ class ShadeSceneViewModelTest : SysuiTestCase() { fun onContentClicked_deviceUnlocked_switchesToGone() = testScope.runTest { val currentScene by collectLastValue(sceneInteractor.desiredScene) - utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin) - utils.deviceEntryRepository.setUnlocked(true) + kosmos.fakeAuthenticationRepository.setAuthenticationMethod( + AuthenticationMethodModel.Pin + ) + kosmos.fakeDeviceEntryRepository.setUnlocked(true) runCurrent() underTest.onContentClicked() @@ -177,8 +185,10 @@ class ShadeSceneViewModelTest : SysuiTestCase() { fun onContentClicked_deviceLockedSecurely_switchesToBouncer() = testScope.runTest { val currentScene by collectLastValue(sceneInteractor.desiredScene) - utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin) - utils.deviceEntryRepository.setUnlocked(false) + kosmos.fakeAuthenticationRepository.setAuthenticationMethod( + AuthenticationMethodModel.Pin + ) + kosmos.fakeDeviceEntryRepository.setUnlocked(false) runCurrent() underTest.onContentClicked() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationStackAppearanceIntegrationTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationStackAppearanceIntegrationTest.kt index 4cdb08afb22e..607996d67d4f 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationStackAppearanceIntegrationTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationStackAppearanceIntegrationTest.kt @@ -27,8 +27,7 @@ import com.android.systemui.flags.Flags import com.android.systemui.flags.fakeFeatureFlagsClassic 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.flag.sceneContainerFlags +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 @@ -50,7 +49,7 @@ class NotificationStackAppearanceIntegrationTest : SysuiTestCase() { private val kosmos = testKosmos().apply { - sceneContainerFlags = FakeSceneContainerFlags(enabled = true) + fakeSceneContainerFlags.enabled = true fakeFeatureFlagsClassic.apply { set(Flags.FULL_SCREEN_USER_SWITCHER, false) set(Flags.NSSL_DEBUG_LINES, false) 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 index 0a10b2c85ebe..0c7ce970cf3a 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 com.google.common.truth.Truth.assertThat 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 @@ class GroupExpansionManagerTest : SysuiTestCase() { 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) - assertThat(listenerCalledCount).isEqualTo(2) - gem.setGroupExpanded(summary1, true) + underTest.setGroupExpanded(summary2, true) assertThat(listenerCalledCount).isEqualTo(2) - gem.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) + underTest.setGroupExpanded(summary1, true) assertThat(listenerCalledCount).isEqualTo(2) - gem.setGroupExpanded(summary2, true) + underTest.setGroupExpanded(summary2, false) 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/tests/src/com/android/systemui/statusbar/notification/collection/render/GroupMembershipManagerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/render/GroupMembershipManagerTest.kt index c1ffa641c6a4..2cbcc5a8d925 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/render/GroupMembershipManagerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/render/GroupMembershipManagerTest.kt @@ -16,67 +16,35 @@ 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.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 +import org.junit.runner.RunWith @SmallTest +@RunWith(AndroidJUnit4::class) class GroupMembershipManagerTest : SysuiTestCase() { - private lateinit var gmm: GroupMembershipManagerImpl - - private val featureFlags = FakeFeatureFlagsClassic() - - @Before - fun setUp() { - gmm = GroupMembershipManagerImpl(featureFlags) - } + private var underTest = GroupMembershipManagerImpl() @Test - fun testIsChildInGroup_topLevel() { - featureFlags.set(Flags.NOTIFICATION_GROUP_EXPANSION_CHANGE, false) + fun isChildInGroup_topLevel() { 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() + assertThat(underTest.isChildInGroup(topLevelEntry)).isFalse() } @Test - fun testIsChildInGroup_noParent_new() { - featureFlags.set(Flags.NOTIFICATION_GROUP_EXPANSION_CHANGE, true) + fun isChildInGroup_noParent() { 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() + assertThat(underTest.isChildInGroup(noParentEntry)).isFalse() } @Test - fun testIsChildInGroup_summary_new() { - featureFlags.set(Flags.NOTIFICATION_GROUP_EXPANSION_CHANGE, true) - + fun isChildInGroup_summary() { val groupKey = "group" val summary = NotificationEntryBuilder() @@ -85,27 +53,17 @@ class GroupMembershipManagerTest : SysuiTestCase() { .build() GroupEntryBuilder().setKey(groupKey).setSummary(summary).build() - assertThat(gmm.isChildInGroup(summary)).isFalse() + assertThat(underTest.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) + fun isGroupSummary_topLevelEntry() { val entry = NotificationEntryBuilder().setParent(GroupEntry.ROOT_ENTRY).build() - assertThat(gmm.isGroupSummary(entry)).isFalse() + assertThat(underTest.isGroupSummary(entry)).isFalse() } @Test - fun testIsGroupSummary_summary() { - featureFlags.set(Flags.NOTIFICATION_GROUP_EXPANSION_CHANGE, true) - + fun isGroupSummary_summary() { val groupKey = "group" val summary = NotificationEntryBuilder() @@ -114,13 +72,11 @@ class GroupMembershipManagerTest : SysuiTestCase() { .build() GroupEntryBuilder().setKey(groupKey).setSummary(summary).build() - assertThat(gmm.isGroupSummary(summary)).isTrue() + assertThat(underTest.isGroupSummary(summary)).isTrue() } @Test - fun testIsGroupSummary_child() { - featureFlags.set(Flags.NOTIFICATION_GROUP_EXPANSION_CHANGE, true) - + fun isGroupSummary_child() { val groupKey = "group" val summary = NotificationEntryBuilder() @@ -130,20 +86,17 @@ class GroupMembershipManagerTest : SysuiTestCase() { val entry = NotificationEntryBuilder().setGroup(mContext, groupKey).build() GroupEntryBuilder().setKey(groupKey).setSummary(summary).addChild(entry).build() - assertThat(gmm.isGroupSummary(entry)).isFalse() + assertThat(underTest.isGroupSummary(entry)).isFalse() } @Test - fun testGetGroupSummary_topLevelEntry() { - featureFlags.set(Flags.NOTIFICATION_GROUP_EXPANSION_CHANGE, true) + fun getGroupSummary_topLevelEntry() { val entry = NotificationEntryBuilder().setParent(GroupEntry.ROOT_ENTRY).build() - assertThat(gmm.getGroupSummary(entry)).isNull() + assertThat(underTest.getGroupSummary(entry)).isNull() } @Test - fun testGetGroupSummary_summary() { - featureFlags.set(Flags.NOTIFICATION_GROUP_EXPANSION_CHANGE, true) - + fun getGroupSummary_summary() { val groupKey = "group" val summary = NotificationEntryBuilder() @@ -152,13 +105,11 @@ class GroupMembershipManagerTest : SysuiTestCase() { .build() GroupEntryBuilder().setKey(groupKey).setSummary(summary).build() - assertThat(gmm.getGroupSummary(summary)).isEqualTo(summary) + assertThat(underTest.getGroupSummary(summary)).isEqualTo(summary) } @Test - fun testGetGroupSummary_child() { - featureFlags.set(Flags.NOTIFICATION_GROUP_EXPANSION_CHANGE, true) - + fun getGroupSummary_child() { val groupKey = "group" val summary = NotificationEntryBuilder() @@ -168,6 +119,6 @@ class GroupMembershipManagerTest : SysuiTestCase() { val entry = NotificationEntryBuilder().setGroup(mContext, groupKey).build() GroupEntryBuilder().setKey(groupKey).setSummary(summary).addChild(entry).build() - assertThat(gmm.getGroupSummary(entry)).isEqualTo(summary) + assertThat(underTest.getGroupSummary(entry)).isEqualTo(summary) } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/telephony/data/repository/TelephonyRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/telephony/data/repository/TelephonyRepositoryImplTest.kt index 262795fe0eb6..8e8e5106e93c 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/telephony/data/repository/TelephonyRepositoryImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/telephony/data/repository/TelephonyRepositoryImplTest.kt @@ -24,12 +24,13 @@ 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.scene.SceneTestUtils +import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.kosmos.testScope import com.android.systemui.telephony.TelephonyListenerManager +import com.android.systemui.testKosmos import com.android.systemui.util.mockito.kotlinArgumentCaptor 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 @@ -39,7 +40,6 @@ import org.mockito.Mock import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations -@OptIn(ExperimentalCoroutinesApi::class) @SmallTest @RunWith(AndroidJUnit4::class) class TelephonyRepositoryImplTest : SysuiTestCase() { @@ -47,8 +47,8 @@ class TelephonyRepositoryImplTest : SysuiTestCase() { @Mock private lateinit var manager: TelephonyListenerManager @Mock private lateinit var telecomManager: TelecomManager - private val utils = SceneTestUtils(this) - private val testScope = utils.testScope + private val kosmos = testKosmos() + private val testScope = kosmos.testScope private lateinit var underTest: TelephonyRepositoryImpl @@ -61,7 +61,7 @@ class TelephonyRepositoryImplTest : SysuiTestCase() { TelephonyRepositoryImpl( applicationScope = testScope.backgroundScope, applicationContext = context, - backgroundDispatcher = utils.testDispatcher, + backgroundDispatcher = kosmos.testDispatcher, manager = manager, telecomManager = telecomManager, ) diff --git a/packages/SystemUI/res-keyguard/drawable/bouncer_password_text_view_focused_background.xml b/packages/SystemUI/res-keyguard/drawable/bouncer_password_text_view_focused_background.xml new file mode 100644 index 000000000000..84b89ca68e65 --- /dev/null +++ b/packages/SystemUI/res-keyguard/drawable/bouncer_password_text_view_focused_background.xml @@ -0,0 +1,27 @@ +<?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. +--> +<selector xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:state_focused="true"> + <shape android:shape="rectangle"> + <corners android:radius="16dp" /> + <!--By default this outline will not show hence 0 width. + width is set programmatically when needed and is gated by the flag: + com.android.systemui.Flags.pinInputFieldStyledFocusState--> + <stroke android:width="0dp" + android:color="@color/bouncer_password_focus_color" /> + </shape> + </item> +</selector>
\ No newline at end of file diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_bouncer_message_area.xml b/packages/SystemUI/res-keyguard/layout/keyguard_bouncer_message_area.xml index 66c54f2a668e..0b35559148af 100644 --- a/packages/SystemUI/res-keyguard/layout/keyguard_bouncer_message_area.xml +++ b/packages/SystemUI/res-keyguard/layout/keyguard_bouncer_message_area.xml @@ -23,7 +23,7 @@ android:layout_marginTop="@dimen/keyguard_lock_padding" android:importantForAccessibility="no" android:ellipsize="marquee" - android:focusable="true" + android:focusable="false" android:gravity="center" android:singleLine="true" /> </merge> diff --git a/packages/SystemUI/res-keyguard/values/dimens.xml b/packages/SystemUI/res-keyguard/values/dimens.xml index 0628c3e957b1..ddad1e3f8940 100644 --- a/packages/SystemUI/res-keyguard/values/dimens.xml +++ b/packages/SystemUI/res-keyguard/values/dimens.xml @@ -25,6 +25,12 @@ <!-- Maximum width of the sliding KeyguardSecurityContainer --> <dimen name="keyguard_security_width">420dp</dimen> + <!-- Width for the keyguard pin input field --> + <dimen name="keyguard_pin_field_width">292dp</dimen> + + <!-- Width for the keyguard pin input field --> + <dimen name="keyguard_pin_field_height">48dp</dimen> + <!-- Height of the sliding KeyguardSecurityContainer (includes 2x keyguard_security_view_top_margin) --> <dimen name="keyguard_security_height">420dp</dimen> diff --git a/packages/SystemUI/res-keyguard/values/strings.xml b/packages/SystemUI/res-keyguard/values/strings.xml index 565ed1085fb9..f51e1098f333 100644 --- a/packages/SystemUI/res-keyguard/values/strings.xml +++ b/packages/SystemUI/res-keyguard/values/strings.xml @@ -62,10 +62,10 @@ <string name="keyguard_plugged_in_charging_slowly"><xliff:g id="percentage">%s</xliff:g> • Charging slowly</string> <!-- When the lock screen is showing and the phone plugged in, and the defend mode is triggered, say that charging is temporarily limited. --> - <string name="keyguard_plugged_in_charging_limited"><xliff:g id="percentage">%s</xliff:g> • Charging optimized to protect battery</string> + <string name="keyguard_plugged_in_charging_limited"><xliff:g id="percentage">%s</xliff:g> • Charging on hold to protect battery</string> <!-- When the lock screen is showing and the phone plugged in with incompatible charger. --> - <string name="keyguard_plugged_in_incompatible_charger"><xliff:g id="percentage">%s</xliff:g> • Issue with charging accessory</string> + <string name="keyguard_plugged_in_incompatible_charger"><xliff:g id="percentage">%s</xliff:g> • Check charging accessory</string> <!-- SIM messages --><skip /> <!-- When the user inserts a sim card from an unsupported network, it becomes network locked --> diff --git a/packages/SystemUI/res-keyguard/values/styles.xml b/packages/SystemUI/res-keyguard/values/styles.xml index 2cca9510417a..4789a229c4d0 100644 --- a/packages/SystemUI/res-keyguard/values/styles.xml +++ b/packages/SystemUI/res-keyguard/values/styles.xml @@ -76,6 +76,7 @@ </style> <style name="Widget.TextView.Password" parent="@android:style/Widget.TextView"> <item name="android:fontFamily">@*android:string/config_headlineFontFamily</item> + <item name="android:background">@drawable/bouncer_password_text_view_focused_background</item> <item name="android:gravity">center</item> <item name="android:textColor">?android:attr/textColorPrimary</item> </style> diff --git a/packages/SystemUI/res/layout/biometric_prompt_layout.xml b/packages/SystemUI/res/layout/biometric_prompt_layout.xml index 23fbb12f3036..10f71134c4cc 100644 --- a/packages/SystemUI/res/layout/biometric_prompt_layout.xml +++ b/packages/SystemUI/res/layout/biometric_prompt_layout.xml @@ -20,6 +20,13 @@ android:layout_height="wrap_content" android:orientation="vertical"> + <ImageView + android:id="@+id/logo" + android:layout_width="@dimen/biometric_auth_icon_size" + android:layout_height="@dimen/biometric_auth_icon_size" + android:layout_gravity="center" + android:scaleType="fitXY"/> + <TextView android:id="@+id/title" android:layout_width="match_parent" diff --git a/packages/SystemUI/res/layout/media_session_view.xml b/packages/SystemUI/res/layout/media_session_view.xml index 85b6e8dc12b3..5db9eee6a908 100644 --- a/packages/SystemUI/res/layout/media_session_view.xml +++ b/packages/SystemUI/res/layout/media_session_view.xml @@ -50,7 +50,10 @@ app:layout_constraintStart_toStartOf="@id/album_art" app:layout_constraintEnd_toEndOf="@id/album_art" app:layout_constraintTop_toTopOf="@id/album_art" - app:layout_constraintBottom_toBottomOf="@id/album_art" /> + app:layout_constraintBottom_toBottomOf="@id/album_art" + android:clipToOutline="true" + android:background="@drawable/qs_media_outline_layout_bg" + /> <com.android.systemui.surfaceeffects.turbulencenoise.TurbulenceNoiseView android:id="@+id/turbulence_noise_view" @@ -59,7 +62,10 @@ app:layout_constraintStart_toStartOf="@id/album_art" app:layout_constraintEnd_toEndOf="@id/album_art" app:layout_constraintTop_toTopOf="@id/album_art" - app:layout_constraintBottom_toBottomOf="@id/album_art" /> + app:layout_constraintBottom_toBottomOf="@id/album_art" + android:clipToOutline="true" + android:background="@drawable/qs_media_outline_layout_bg" + /> <!-- Guideline for output switcher --> <androidx.constraintlayout.widget.Guideline diff --git a/packages/SystemUI/res/values-land/dimens.xml b/packages/SystemUI/res/values-land/dimens.xml index cfb40171cfc1..55606aa0bc82 100644 --- a/packages/SystemUI/res/values-land/dimens.xml +++ b/packages/SystemUI/res/values-land/dimens.xml @@ -86,8 +86,8 @@ <!-- Power Menu Lite --> <!-- These values are for small screen landscape. For larger landscape screens, they are overlaid --> - <dimen name="global_actions_button_size">72dp</dimen> - <dimen name="global_actions_button_padding">26dp</dimen> + <dimen name="global_actions_button_size">64dp</dimen> + <dimen name="global_actions_button_padding">20dp</dimen> <!-- scroll view the size of 2 channel rows --> <dimen name="notification_blocker_channel_list_height">128dp</dimen> diff --git a/packages/SystemUI/res/values-night/colors.xml b/packages/SystemUI/res/values-night/colors.xml index bcc3c83b4560..61a323d44dfc 100644 --- a/packages/SystemUI/res/values-night/colors.xml +++ b/packages/SystemUI/res/values-night/colors.xml @@ -93,6 +93,8 @@ <color name="qs_user_switcher_selected_avatar_icon_color">#202124</color> <!-- Color of background circle of user avatars in quick settings user switcher --> <color name="qs_user_switcher_avatar_background">#3C4043</color> + <!-- Color of border for keyguard password input when focused --> + <color name="bouncer_password_focus_color">@*android:color/system_secondary_dark</color> <!-- Accessibility floating menu --> <color name="accessibility_floating_menu_background">#B3000000</color> <!-- 70% --> diff --git a/packages/SystemUI/res/values/colors.xml b/packages/SystemUI/res/values/colors.xml index 8be1cc7282e3..3839dd98cdd3 100644 --- a/packages/SystemUI/res/values/colors.xml +++ b/packages/SystemUI/res/values/colors.xml @@ -56,6 +56,8 @@ <color name="kg_user_switcher_restricted_avatar_icon_color">@color/GM2_grey_600</color> <!-- Color of background circle of user avatars in keyguard user switcher --> <color name="user_avatar_color_bg">?android:attr/colorBackgroundFloating</color> + <!-- Color of border for keyguard password input when focused --> + <color name="bouncer_password_focus_color">@*android:color/system_secondary_light</color> <!-- Icon color for user avatars in user switcher quick settings --> <color name="qs_user_switcher_avatar_icon_color">#3C4043</color> diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index 798fc06b44f7..ee2a1ceab2b7 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -897,6 +897,10 @@ <dimen name="communal_tutorial_indicator_padding">24dp</dimen> <dimen name="communal_tutorial_indicator_horizontal_offset">32dp</dimen> + <!-- Size of the maximum radius for the enforced rounded rectangles on communal hub. + Keep it the same as in Launcher--> + <dimen name="communal_enforced_rounded_corner_max_radius">16dp</dimen> + <!-- The width/height of the unlock icon view on keyguard. --> <dimen name="keyguard_lock_height">42dp</dimen> <dimen name="keyguard_lock_padding">20dp</dimen> diff --git a/packages/SystemUI/res/values/ids.xml b/packages/SystemUI/res/values/ids.xml index 6e035e8c8c36..ec4c7d5bf67e 100644 --- a/packages/SystemUI/res/values/ids.xml +++ b/packages/SystemUI/res/values/ids.xml @@ -249,6 +249,9 @@ --> <item type="id" name="tag_smartspace_view" /> + <!-- ID of the Scene Container root Composable view --> + <item type='id' name="scene_container_root_composable" /> + <!-- Tag set on the Compose implementation of the QS footer actions. --> <item type="id" name="tag_compose_qs_footer_actions" /> diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java index a5a545af641a..033f93b260ab 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java @@ -3,6 +3,7 @@ package com.android.keyguard; import static com.android.keyguard.KeyguardStatusAreaView.TRANSLATE_X_CLOCK_DESIGN; import static com.android.keyguard.KeyguardStatusAreaView.TRANSLATE_Y_CLOCK_DESIGN; import static com.android.keyguard.KeyguardStatusAreaView.TRANSLATE_Y_CLOCK_SIZE; +import static com.android.systemui.Flags.migrateClocksToBlueprint; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; @@ -191,11 +192,11 @@ public class KeyguardClockSwitch extends RelativeLayout { @Override protected void onFinishInflate() { super.onFinishInflate(); - - mSmallClockFrame = findViewById(R.id.lockscreen_clock_view); - mLargeClockFrame = findViewById(R.id.lockscreen_clock_view_large); - mStatusArea = findViewById(R.id.keyguard_status_area); - + if (!migrateClocksToBlueprint()) { + mSmallClockFrame = findViewById(R.id.lockscreen_clock_view); + mLargeClockFrame = findViewById(R.id.lockscreen_clock_view_large); + mStatusArea = findViewById(R.id.keyguard_status_area); + } onConfigChanged(); } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java index 66f965aea76f..efd8f7f97ca3 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardInputViewController.java @@ -37,6 +37,7 @@ import com.android.systemui.bouncer.ui.binder.BouncerMessageViewBinder; import com.android.systemui.classifier.FalsingCollector; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.flags.FeatureFlags; +import com.android.systemui.keyboard.data.repository.KeyboardRepository; import com.android.systemui.log.BouncerLogger; import com.android.systemui.res.R; import com.android.systemui.statusbar.policy.DevicePostureController; @@ -210,6 +211,7 @@ public abstract class KeyguardInputViewController<T extends KeyguardInputView> private final FeatureFlags mFeatureFlags; private final SelectedUserInteractor mSelectedUserInteractor; private final UiEventLogger mUiEventLogger; + private final KeyboardRepository mKeyboardRepository; @Inject public Factory(KeyguardUpdateMonitor keyguardUpdateMonitor, @@ -223,7 +225,8 @@ public abstract class KeyguardInputViewController<T extends KeyguardInputView> DevicePostureController devicePostureController, KeyguardViewController keyguardViewController, FeatureFlags featureFlags, SelectedUserInteractor selectedUserInteractor, - UiEventLogger uiEventLogger) { + UiEventLogger uiEventLogger, + KeyboardRepository keyboardRepository) { mKeyguardUpdateMonitor = keyguardUpdateMonitor; mLockPatternUtils = lockPatternUtils; mLatencyTracker = latencyTracker; @@ -240,6 +243,7 @@ public abstract class KeyguardInputViewController<T extends KeyguardInputView> mFeatureFlags = featureFlags; mSelectedUserInteractor = selectedUserInteractor; mUiEventLogger = uiEventLogger; + mKeyboardRepository = keyboardRepository; } /** Create a new {@link KeyguardInputViewController}. */ @@ -268,19 +272,22 @@ public abstract class KeyguardInputViewController<T extends KeyguardInputView> keyguardSecurityCallback, mMessageAreaControllerFactory, mLatencyTracker, mLiftToActivateListener, emergencyButtonController, mFalsingCollector, mDevicePostureController, mFeatureFlags, mSelectedUserInteractor, - mUiEventLogger); + mUiEventLogger, mKeyboardRepository + ); } else if (keyguardInputView instanceof KeyguardSimPinView) { return new KeyguardSimPinViewController((KeyguardSimPinView) keyguardInputView, mKeyguardUpdateMonitor, securityMode, mLockPatternUtils, keyguardSecurityCallback, mMessageAreaControllerFactory, mLatencyTracker, mLiftToActivateListener, mTelephonyManager, mFalsingCollector, - emergencyButtonController, mFeatureFlags, mSelectedUserInteractor); + emergencyButtonController, mFeatureFlags, mSelectedUserInteractor, + mKeyboardRepository); } else if (keyguardInputView instanceof KeyguardSimPukView) { return new KeyguardSimPukViewController((KeyguardSimPukView) keyguardInputView, mKeyguardUpdateMonitor, securityMode, mLockPatternUtils, keyguardSecurityCallback, mMessageAreaControllerFactory, mLatencyTracker, mLiftToActivateListener, mTelephonyManager, mFalsingCollector, - emergencyButtonController, mFeatureFlags, mSelectedUserInteractor); + emergencyButtonController, mFeatureFlags, mSelectedUserInteractor, + mKeyboardRepository); } throw new RuntimeException("Unable to find controller for " + keyguardInputView); diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputView.java index 36fe75f69a45..fcff0dbc0878 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputView.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputView.java @@ -25,6 +25,7 @@ import static com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_RESTART_FO import static com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_TIMEOUT; import static com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_TRUSTAGENT_EXPIRED; import static com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_USER_REQUEST; +import static com.android.systemui.Flags.pinInputFieldStyledFocusState; import android.animation.Animator; import android.animation.AnimatorSet; @@ -168,7 +169,9 @@ public abstract class KeyguardPinBasedInputView extends KeyguardAbsKeyInputView // Set selected property on so the view can send accessibility events. mPasswordEntry.setSelected(true); - mPasswordEntry.setDefaultFocusHighlightEnabled(false); + if (!pinInputFieldStyledFocusState()) { + mPasswordEntry.setDefaultFocusHighlightEnabled(false); + } mOkButton = findViewById(R.id.key_enter); diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputViewController.java index 376933dc5519..60dd5686c315 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputViewController.java @@ -16,17 +16,25 @@ package com.android.keyguard; +import static com.android.systemui.util.kotlin.JavaAdapterKt.collectFlow; +import static com.android.systemui.Flags.pinInputFieldStyledFocusState; + +import android.graphics.drawable.GradientDrawable; +import android.graphics.drawable.StateListDrawable; +import android.util.TypedValue; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.View; import android.view.View.OnKeyListener; import android.view.View.OnTouchListener; +import android.view.ViewGroup; import com.android.internal.util.LatencyTracker; import com.android.internal.widget.LockPatternUtils; import com.android.keyguard.KeyguardSecurityModel.SecurityMode; import com.android.systemui.classifier.FalsingCollector; import com.android.systemui.flags.FeatureFlags; +import com.android.systemui.keyboard.data.repository.KeyboardRepository; import com.android.systemui.res.R; import com.android.systemui.user.domain.interactor.SelectedUserInteractor; @@ -35,6 +43,7 @@ public abstract class KeyguardPinBasedInputViewController<T extends KeyguardPinB private final LiftToActivateListener mLiftToActivateListener; private final FalsingCollector mFalsingCollector; + private final KeyboardRepository mKeyboardRepository; protected PasswordTextView mPasswordEntry; private final OnKeyListener mOnKeyListener = (v, keyCode, event) -> { @@ -65,12 +74,14 @@ public abstract class KeyguardPinBasedInputViewController<T extends KeyguardPinB EmergencyButtonController emergencyButtonController, FalsingCollector falsingCollector, FeatureFlags featureFlags, - SelectedUserInteractor selectedUserInteractor) { + SelectedUserInteractor selectedUserInteractor, + KeyboardRepository keyboardRepository) { super(view, keyguardUpdateMonitor, securityMode, lockPatternUtils, keyguardSecurityCallback, messageAreaControllerFactory, latencyTracker, falsingCollector, emergencyButtonController, featureFlags, selectedUserInteractor); mLiftToActivateListener = liftToActivateListener; mFalsingCollector = falsingCollector; + mKeyboardRepository = keyboardRepository; mPasswordEntry = mView.findViewById(mView.getPasswordTextViewId()); } @@ -120,6 +131,35 @@ public abstract class KeyguardPinBasedInputViewController<T extends KeyguardPinB }); okButton.setOnHoverListener(mLiftToActivateListener); } + if (pinInputFieldStyledFocusState()) { + collectFlow(mPasswordEntry, mKeyboardRepository.isAnyKeyboardConnected(), + this::setKeyboardBasedFocusOutline); + + /** + * new UI Specs for PIN Input field have new dimensions go/pin-focus-states. + * However we want these changes behind a flag, and resource files cannot be flagged + * hence the dimension change in code. When the flags are removed these dimensions + * should be set in resources permanently and the code below removed. + */ + ViewGroup.LayoutParams layoutParams = mPasswordEntry.getLayoutParams(); + layoutParams.width = (int) getResources().getDimension( + R.dimen.keyguard_pin_field_width); + layoutParams.height = (int) getResources().getDimension( + R.dimen.keyguard_pin_field_height); + } + } + + private void setKeyboardBasedFocusOutline(boolean isAnyKeyboardConnected) { + StateListDrawable background = (StateListDrawable) mPasswordEntry.getBackground(); + GradientDrawable stateDrawable = (GradientDrawable) background.getStateDrawable(0); + int color = getResources().getColor(R.color.bouncer_password_focus_color); + if (!isAnyKeyboardConnected) { + stateDrawable.setStroke(0, color); + } else { + int strokeWidthInDP = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 3, + getResources().getDisplayMetrics()); + stateDrawable.setStroke(strokeWidthInDP, color); + } } @Override diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPinViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPinViewController.java index 2aab1f189263..b958f55bdf79 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardPinViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPinViewController.java @@ -28,6 +28,7 @@ import com.android.keyguard.KeyguardSecurityModel.SecurityMode; import com.android.systemui.classifier.FalsingCollector; import com.android.systemui.flags.FeatureFlags; import com.android.systemui.flags.Flags; +import com.android.systemui.keyboard.data.repository.KeyboardRepository; import com.android.systemui.res.R; import com.android.systemui.statusbar.policy.DevicePostureController; import com.android.systemui.user.domain.interactor.SelectedUserInteractor; @@ -58,12 +59,13 @@ public class KeyguardPinViewController LatencyTracker latencyTracker, LiftToActivateListener liftToActivateListener, EmergencyButtonController emergencyButtonController, FalsingCollector falsingCollector, - DevicePostureController postureController, - FeatureFlags featureFlags, SelectedUserInteractor selectedUserInteractor, - UiEventLogger uiEventLogger) { + DevicePostureController postureController, FeatureFlags featureFlags, + SelectedUserInteractor selectedUserInteractor, UiEventLogger uiEventLogger, + KeyboardRepository keyboardRepository) { super(view, keyguardUpdateMonitor, securityMode, lockPatternUtils, keyguardSecurityCallback, messageAreaControllerFactory, latencyTracker, liftToActivateListener, - emergencyButtonController, falsingCollector, featureFlags, selectedUserInteractor); + emergencyButtonController, falsingCollector, featureFlags, selectedUserInteractor, + keyboardRepository); mKeyguardUpdateMonitor = keyguardUpdateMonitor; mPostureController = postureController; mLockPatternUtils = lockPatternUtils; diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSimPinViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSimPinViewController.java index 5729119a582a..1cdcbd06815f 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardSimPinViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSimPinViewController.java @@ -44,6 +44,7 @@ import com.android.internal.widget.LockPatternUtils; import com.android.keyguard.KeyguardSecurityModel.SecurityMode; import com.android.systemui.classifier.FalsingCollector; import com.android.systemui.flags.FeatureFlags; +import com.android.systemui.keyboard.data.repository.KeyboardRepository; import com.android.systemui.res.R; import com.android.systemui.user.domain.interactor.SelectedUserInteractor; @@ -93,10 +94,11 @@ public class KeyguardSimPinViewController LatencyTracker latencyTracker, LiftToActivateListener liftToActivateListener, TelephonyManager telephonyManager, FalsingCollector falsingCollector, EmergencyButtonController emergencyButtonController, FeatureFlags featureFlags, - SelectedUserInteractor selectedUserInteractor) { + SelectedUserInteractor selectedUserInteractor, KeyboardRepository keyboardRepository) { super(view, keyguardUpdateMonitor, securityMode, lockPatternUtils, keyguardSecurityCallback, messageAreaControllerFactory, latencyTracker, liftToActivateListener, - emergencyButtonController, falsingCollector, featureFlags, selectedUserInteractor); + emergencyButtonController, falsingCollector, featureFlags, selectedUserInteractor, + keyboardRepository); mKeyguardUpdateMonitor = keyguardUpdateMonitor; mTelephonyManager = telephonyManager; mSimImageView = mView.findViewById(R.id.keyguard_sim); diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSimPukViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSimPukViewController.java index 05fb5fa75e9e..f019d61a3ccc 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardSimPukViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSimPukViewController.java @@ -39,6 +39,7 @@ import com.android.internal.widget.LockPatternUtils; import com.android.keyguard.KeyguardSecurityModel.SecurityMode; import com.android.systemui.classifier.FalsingCollector; import com.android.systemui.flags.FeatureFlags; +import com.android.systemui.keyboard.data.repository.KeyboardRepository; import com.android.systemui.res.R; import com.android.systemui.user.domain.interactor.SelectedUserInteractor; @@ -90,10 +91,11 @@ public class KeyguardSimPukViewController LatencyTracker latencyTracker, LiftToActivateListener liftToActivateListener, TelephonyManager telephonyManager, FalsingCollector falsingCollector, EmergencyButtonController emergencyButtonController, FeatureFlags featureFlags, - SelectedUserInteractor selectedUserInteractor) { + SelectedUserInteractor selectedUserInteractor, KeyboardRepository keyboardRepository) { super(view, keyguardUpdateMonitor, securityMode, lockPatternUtils, keyguardSecurityCallback, messageAreaControllerFactory, latencyTracker, liftToActivateListener, - emergencyButtonController, falsingCollector, featureFlags, selectedUserInteractor); + emergencyButtonController, falsingCollector, featureFlags, selectedUserInteractor, + keyboardRepository); mKeyguardUpdateMonitor = keyguardUpdateMonitor; mTelephonyManager = telephonyManager; mSimImageView = mView.findViewById(R.id.keyguard_sim); diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java index d372f5a616b1..1758831203d6 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java @@ -494,7 +494,7 @@ public class KeyguardStatusViewController extends ViewController<KeyguardStatusV boolean shouldBeCentered, boolean animate) { if (migrateClocksToBlueprint()) { - mKeyguardInteractor.setClockShouldBeCentered(mSplitShadeEnabled && shouldBeCentered); + mKeyguardInteractor.setClockShouldBeCentered(shouldBeCentered); } else { mKeyguardClockSwitchController.setSplitShadeCentered( splitShadeEnabled && shouldBeCentered); diff --git a/packages/SystemUI/src/com/android/systemui/NoOpCoreStartable.kt b/packages/SystemUI/src/com/android/systemui/NoOpCoreStartable.kt new file mode 100644 index 000000000000..9f54c26b5151 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/NoOpCoreStartable.kt @@ -0,0 +1,22 @@ +/* + * 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 + +/** A [CoreStartable] that does nothing. */ +class NoOpCoreStartable : CoreStartable { + override fun start() {} +} diff --git a/packages/SystemUI/src/com/android/systemui/SystemUIAppComponentFactoryBase.kt b/packages/SystemUI/src/com/android/systemui/SystemUIAppComponentFactoryBase.kt index b15aaaf9a00e..e88aaf015f87 100644 --- a/packages/SystemUI/src/com/android/systemui/SystemUIAppComponentFactoryBase.kt +++ b/packages/SystemUI/src/com/android/systemui/SystemUIAppComponentFactoryBase.kt @@ -91,7 +91,7 @@ abstract class SystemUIAppComponentFactoryBase : AppComponentFactory() { return app } - @UsesReflection(KeepTarget(extendsClassConstant = SysUIComponent::class, methodName = "inject")) + @UsesReflection(KeepTarget(instanceOfClassConstant = SysUIComponent::class, methodName = "inject")) override fun instantiateProviderCompat(cl: ClassLoader, className: String): ContentProvider { val contentProvider = super.instantiateProviderCompat(cl, className) if (contentProvider is ContextInitializer) { diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/data/repository/ColorCorrectionRepository.kt b/packages/SystemUI/src/com/android/systemui/accessibility/data/repository/ColorCorrectionRepository.kt index 6483ae44d5ec..c7e5b645db5f 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/data/repository/ColorCorrectionRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/accessibility/data/repository/ColorCorrectionRepository.kt @@ -19,16 +19,20 @@ package com.android.systemui.accessibility.data.repository import android.os.UserHandle import android.provider.Settings.Secure import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.util.settings.SecureSettings import com.android.systemui.util.settings.SettingsProxyExt.observerFlow import javax.inject.Inject import kotlin.coroutines.CoroutineContext +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 import kotlinx.coroutines.withContext /** Provides data related to color correction. */ @@ -45,22 +49,24 @@ class ColorCorrectionRepositoryImpl @Inject constructor( @Background private val bgCoroutineContext: CoroutineContext, + @Application private val scope: CoroutineScope, private val secureSettings: SecureSettings, ) : ColorCorrectionRepository { - companion object { - const val SETTING_NAME = Secure.ACCESSIBILITY_DISPLAY_DALTONIZER_ENABLED - const val DISABLED = 0 - const val ENABLED = 1 - } + private val userMap = mutableMapOf<Int, Flow<Boolean>>() override fun isEnabled(userHandle: UserHandle): Flow<Boolean> = - secureSettings - .observerFlow(userHandle.identifier, SETTING_NAME) - .onStart { emit(Unit) } - .map { secureSettings.getIntForUser(SETTING_NAME, userHandle.identifier) == ENABLED } - .distinctUntilChanged() - .flowOn(bgCoroutineContext) + userMap.getOrPut(userHandle.identifier) { + secureSettings + .observerFlow(userHandle.identifier, SETTING_NAME) + .onStart { emit(Unit) } + .map { + secureSettings.getIntForUser(SETTING_NAME, userHandle.identifier) == ENABLED + } + .distinctUntilChanged() + .flowOn(bgCoroutineContext) + .shareIn(scope, SharingStarted.WhileSubscribed(), replay = 1) + } override suspend fun setIsEnabled(isEnabled: Boolean, userHandle: UserHandle): Boolean = withContext(bgCoroutineContext) { @@ -70,4 +76,10 @@ constructor( userHandle.identifier ) } + + companion object { + private const val SETTING_NAME = Secure.ACCESSIBILITY_DISPLAY_DALTONIZER_ENABLED + private const val DISABLED = 0 + private const val ENABLED = 1 + } } diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/data/repository/ColorInversionRepository.kt b/packages/SystemUI/src/com/android/systemui/accessibility/data/repository/ColorInversionRepository.kt index bbf10c509e50..419eada91f87 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/data/repository/ColorInversionRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/accessibility/data/repository/ColorInversionRepository.kt @@ -19,16 +19,20 @@ package com.android.systemui.accessibility.data.repository import android.os.UserHandle import android.provider.Settings.Secure import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.util.settings.SecureSettings import com.android.systemui.util.settings.SettingsProxyExt.observerFlow import javax.inject.Inject import kotlin.coroutines.CoroutineContext +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 import kotlinx.coroutines.withContext /** Provides data related to color inversion. */ @@ -45,16 +49,24 @@ class ColorInversionRepositoryImpl @Inject constructor( @Background private val bgCoroutineContext: CoroutineContext, + @Application private val scope: CoroutineScope, private val secureSettings: SecureSettings, ) : ColorInversionRepository { + private val userMap = mutableMapOf<Int, Flow<Boolean>>() + override fun isEnabled(userHandle: UserHandle): Flow<Boolean> = - secureSettings - .observerFlow(userHandle.identifier, SETTING_NAME) - .onStart { emit(Unit) } - .map { secureSettings.getIntForUser(SETTING_NAME, userHandle.identifier) == ENABLED } - .distinctUntilChanged() - .flowOn(bgCoroutineContext) + userMap.getOrPut(userHandle.identifier) { + secureSettings + .observerFlow(userHandle.identifier, SETTING_NAME) + .onStart { emit(Unit) } + .map { + secureSettings.getIntForUser(SETTING_NAME, userHandle.identifier) == ENABLED + } + .distinctUntilChanged() + .flowOn(bgCoroutineContext) + .shareIn(scope, SharingStarted.WhileSubscribed(), replay = 1) + } override suspend fun setIsEnabled(isEnabled: Boolean, userHandle: UserHandle): Boolean = withContext(bgCoroutineContext) { diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java index ab23564a1df4..57e308ff16e8 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java @@ -399,7 +399,8 @@ public class AuthContainerView extends LinearLayout config.mPromptInfo, config.mUserId, config.mOperationId, - new BiometricModalities(fpProps, faceProps)); + new BiometricModalities(fpProps, faceProps), + config.mOpPackageName); final BiometricPromptLayout view = (BiometricPromptLayout) layoutInflater.inflate( R.layout.biometric_prompt_layout, null, false); @@ -470,7 +471,8 @@ public class AuthContainerView extends LinearLayout mBackgroundView.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO); mPromptSelectorInteractorProvider.get().useCredentialsForAuthentication( - mConfig.mPromptInfo, credentialType, mConfig.mUserId, mConfig.mOperationId); + mConfig.mPromptInfo, credentialType, mConfig.mUserId, mConfig.mOperationId, + mConfig.mOpPackageName); final CredentialViewModel vm = mCredentialViewModelProvider.get(); vm.setAnimateContents(animateContents); ((CredentialView) mCredentialView).init(vm, this, mPanelController, animatePanel); diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsAnimationViewController.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsAnimationViewController.kt index 7d9ec08bcb3b..f5603ed732a5 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsAnimationViewController.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsAnimationViewController.kt @@ -23,6 +23,7 @@ import androidx.lifecycle.Lifecycle import androidx.lifecycle.repeatOnLifecycle import com.android.app.animation.Interpolators import com.android.systemui.Dumpable +import com.android.systemui.biometrics.domain.interactor.UdfpsOverlayInteractor import com.android.systemui.dump.DumpManager import com.android.systemui.lifecycle.repeatWhenAttached import com.android.systemui.plugins.statusbar.StatusBarStateController @@ -49,7 +50,8 @@ abstract class UdfpsAnimationViewController<T : UdfpsAnimationView>( protected val statusBarStateController: StatusBarStateController, protected val shadeInteractor: ShadeInteractor, protected val dialogManager: SystemUIDialogManager, - private val dumpManager: DumpManager + private val dumpManager: DumpManager, + private val udfpsOverlayInteractor: UdfpsOverlayInteractor, ) : ViewController<T>(view), Dumpable { protected abstract val tag: String @@ -130,11 +132,13 @@ abstract class UdfpsAnimationViewController<T : UdfpsAnimationView>( override fun onViewAttached() { dialogManager.registerListener(dialogListener) dumpManager.registerDumpable(dumpTag, this) + udfpsOverlayInteractor.setHandleTouches(shouldHandle = true) } override fun onViewDetached() { dialogManager.unregisterListener(dialogListener) dumpManager.unregisterDumpable(dumpTag) + udfpsOverlayInteractor.setHandleTouches(shouldHandle = true) } /** @@ -165,6 +169,7 @@ abstract class UdfpsAnimationViewController<T : UdfpsAnimationView>( fun updatePauseAuth() { if (view.setPauseAuth(shouldPauseAuth())) { view.postInvalidate() + udfpsOverlayInteractor.setHandleTouches(shouldHandle = !shouldPauseAuth()) } } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsBpViewController.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsBpViewController.kt index e7b0d9fd9d85..e0455b58b919 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsBpViewController.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsBpViewController.kt @@ -15,6 +15,7 @@ */ package com.android.systemui.biometrics +import com.android.systemui.biometrics.domain.interactor.UdfpsOverlayInteractor import com.android.systemui.dump.DumpManager import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.shade.domain.interactor.ShadeInteractor @@ -28,13 +29,15 @@ class UdfpsBpViewController( statusBarStateController: StatusBarStateController, shadeInteractor: ShadeInteractor, systemUIDialogManager: SystemUIDialogManager, - dumpManager: DumpManager + dumpManager: DumpManager, + udfpsOverlayInteractor: UdfpsOverlayInteractor, ) : UdfpsAnimationViewController<UdfpsBpView>( view, statusBarStateController, shadeInteractor, systemUIDialogManager, - dumpManager + dumpManager, + udfpsOverlayInteractor, ) { override val tag = "UdfpsBpViewController" diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java index 81de0a283e88..66fe4b36567d 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java @@ -68,6 +68,7 @@ import com.android.keyguard.KeyguardUpdateMonitor; import com.android.systemui.Dumpable; import com.android.systemui.animation.ActivityLaunchAnimator; import com.android.systemui.biometrics.dagger.BiometricsBackground; +import com.android.systemui.biometrics.domain.interactor.UdfpsOverlayInteractor; import com.android.systemui.biometrics.shared.model.UdfpsOverlayParams; import com.android.systemui.biometrics.udfps.InteractionEvent; import com.android.systemui.biometrics.udfps.NormalizedTouchData; @@ -171,6 +172,7 @@ public class UdfpsController implements DozeReceiver, Dumpable { @NonNull private final Lazy<DefaultUdfpsTouchOverlayViewModel> mDefaultUdfpsTouchOverlayViewModel; @NonNull private final AlternateBouncerInteractor mAlternateBouncerInteractor; + @NonNull private final UdfpsOverlayInteractor mUdfpsOverlayInteractor; @NonNull private final InputManager mInputManager; @NonNull private final UdfpsKeyguardAccessibilityDelegate mUdfpsKeyguardAccessibilityDelegate; @NonNull private final SelectedUserInteractor mSelectedUserInteractor; @@ -293,7 +295,8 @@ public class UdfpsController implements DozeReceiver, Dumpable { mSelectedUserInteractor, mDeviceEntryUdfpsTouchOverlayViewModel, mDefaultUdfpsTouchOverlayViewModel, - mShadeInteractor + mShadeInteractor, + mUdfpsOverlayInteractor ))); } @@ -674,7 +677,8 @@ public class UdfpsController implements DozeReceiver, Dumpable { @NonNull FpsUnlockTracker fpsUnlockTracker, @NonNull KeyguardTransitionInteractor keyguardTransitionInteractor, Lazy<DeviceEntryUdfpsTouchOverlayViewModel> deviceEntryUdfpsTouchOverlayViewModel, - Lazy<DefaultUdfpsTouchOverlayViewModel> defaultUdfpsTouchOverlayViewModel) { + Lazy<DefaultUdfpsTouchOverlayViewModel> defaultUdfpsTouchOverlayViewModel, + @NonNull UdfpsOverlayInteractor udfpsOverlayInteractor) { mContext = context; mExecution = execution; mVibrator = vibrator; @@ -715,6 +719,7 @@ public class UdfpsController implements DozeReceiver, Dumpable { mPrimaryBouncerInteractor = primaryBouncerInteractor; mShadeInteractor = shadeInteractor; mAlternateBouncerInteractor = alternateBouncerInteractor; + mUdfpsOverlayInteractor = udfpsOverlayInteractor; mInputManager = inputManager; mUdfpsKeyguardAccessibilityDelegate = udfpsKeyguardAccessibilityDelegate; mSelectedUserInteractor = selectedUserInteractor; diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt index b94a1779e10b..417608336974 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt @@ -45,6 +45,7 @@ import androidx.annotation.LayoutRes import androidx.annotation.VisibleForTesting import com.android.keyguard.KeyguardUpdateMonitor import com.android.systemui.animation.ActivityLaunchAnimator +import com.android.systemui.biometrics.domain.interactor.UdfpsOverlayInteractor import com.android.systemui.biometrics.shared.model.UdfpsOverlayParams import com.android.systemui.biometrics.ui.binder.UdfpsTouchOverlayBinder import com.android.systemui.biometrics.ui.view.UdfpsTouchOverlay @@ -109,6 +110,7 @@ class UdfpsControllerOverlay @JvmOverloads constructor( private val deviceEntryUdfpsTouchOverlayViewModel: Lazy<DeviceEntryUdfpsTouchOverlayViewModel>, private val defaultUdfpsTouchOverlayViewModel: Lazy<DefaultUdfpsTouchOverlayViewModel>, private val shadeInteractor: ShadeInteractor, + private val udfpsOverlayInteractor: UdfpsOverlayInteractor, ) { private var overlayViewLegacy: UdfpsView? = null private set @@ -281,7 +283,8 @@ class UdfpsControllerOverlay @JvmOverloads constructor( statusBarStateController, shadeInteractor, dialogManager, - dumpManager + dumpManager, + udfpsOverlayInteractor, ) } REASON_AUTH_KEYGUARD -> { @@ -306,6 +309,7 @@ class UdfpsControllerOverlay @JvmOverloads constructor( selectedUserInteractor, transitionInteractor, shadeInteractor, + udfpsOverlayInteractor, ) } REASON_AUTH_BP -> { @@ -315,7 +319,8 @@ class UdfpsControllerOverlay @JvmOverloads constructor( statusBarStateController, shadeInteractor, dialogManager, - dumpManager + dumpManager, + udfpsOverlayInteractor, ) } REASON_AUTH_OTHER, @@ -325,7 +330,8 @@ class UdfpsControllerOverlay @JvmOverloads constructor( statusBarStateController, shadeInteractor, dialogManager, - dumpManager + dumpManager, + udfpsOverlayInteractor, ) } else -> { diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsDialogMeasureAdapter.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsDialogMeasureAdapter.java index 86802a5b58b0..02eae9cedf74 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsDialogMeasureAdapter.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsDialogMeasureAdapter.java @@ -148,6 +148,12 @@ public class UdfpsDialogMeasureAdapter { || child.getId() == R.id.customized_view_container) { //skip description view and compute later continue; + } else if (child.getId() == R.id.logo) { + child.measure( + MeasureSpec.makeMeasureSpec(child.getLayoutParams().width, + MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(child.getLayoutParams().height, + MeasureSpec.EXACTLY)); } else { child.measure( MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsFpmEmptyViewController.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsFpmEmptyViewController.kt index ab3fbb191527..cfbbc26800d2 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsFpmEmptyViewController.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsFpmEmptyViewController.kt @@ -15,6 +15,7 @@ */ package com.android.systemui.biometrics +import com.android.systemui.biometrics.domain.interactor.UdfpsOverlayInteractor import com.android.systemui.dump.DumpManager import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.shade.domain.interactor.ShadeInteractor @@ -30,13 +31,15 @@ class UdfpsFpmEmptyViewController( statusBarStateController: StatusBarStateController, shadeInteractor: ShadeInteractor, systemUIDialogManager: SystemUIDialogManager, - dumpManager: DumpManager + dumpManager: DumpManager, + udfpsOverlayInteractor: UdfpsOverlayInteractor, ) : UdfpsAnimationViewController<UdfpsFpmEmptyView>( view, statusBarStateController, shadeInteractor, systemUIDialogManager, - dumpManager + dumpManager, + udfpsOverlayInteractor, ) { override val tag = "UdfpsFpmOtherViewController" } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerLegacy.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerLegacy.kt index 9f170241269d..7020d05350da 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerLegacy.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerLegacy.kt @@ -27,6 +27,7 @@ import com.android.keyguard.BouncerPanelExpansionCalculator.aboutToShowBouncerPr import com.android.keyguard.KeyguardUpdateMonitor import com.android.systemui.animation.ActivityLaunchAnimator import com.android.systemui.biometrics.UdfpsKeyguardViewLegacy.ANIMATE_APPEAR_ON_SCREEN_OFF +import com.android.systemui.biometrics.domain.interactor.UdfpsOverlayInteractor import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor import com.android.systemui.dump.DumpManager @@ -75,6 +76,7 @@ open class UdfpsKeyguardViewControllerLegacy( private val selectedUserInteractor: SelectedUserInteractor, private val transitionInteractor: KeyguardTransitionInteractor, shadeInteractor: ShadeInteractor, + udfpsOverlayInteractor: UdfpsOverlayInteractor, ) : UdfpsAnimationViewController<UdfpsKeyguardViewLegacy>( view, @@ -82,6 +84,7 @@ open class UdfpsKeyguardViewControllerLegacy( shadeInteractor, systemUIDialogManager, dumpManager, + udfpsOverlayInteractor, ) { private val uniqueIdentifier = this.toString() private var showingUdfpsBouncer = false diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/PromptRepository.kt b/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/PromptRepository.kt index b35fbbc7bb32..ad7bb0e61178 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/PromptRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/PromptRepository.kt @@ -55,6 +55,9 @@ interface PromptRepository { /** The kind of credential to use (biometric, pin, pattern, etc.). */ val kind: StateFlow<PromptKind> + /** The package name that the prompt is called from. */ + val opPackageName: StateFlow<String?> + /** * If explicit confirmation is required. * @@ -68,6 +71,7 @@ interface PromptRepository { userId: Int, gatekeeperChallenge: Long?, kind: PromptKind, + opPackageName: String, ) /** Unset the prompt info. */ @@ -108,6 +112,9 @@ constructor( private val _kind: MutableStateFlow<PromptKind> = MutableStateFlow(PromptKind.Biometric()) override val kind = _kind.asStateFlow() + private val _opPackageName: MutableStateFlow<String?> = MutableStateFlow(null) + override val opPackageName = _opPackageName.asStateFlow() + private val _faceSettings = _userId.map { id -> faceSettings.forUser(id) }.distinctUntilChanged() private val _faceSettingAlwaysRequireConfirmation = @@ -127,11 +134,13 @@ constructor( userId: Int, gatekeeperChallenge: Long?, kind: PromptKind, + opPackageName: String, ) { _kind.value = kind _userId.value = userId _challenge.value = gatekeeperChallenge _promptInfo.value = promptInfo + _opPackageName.value = opPackageName } override fun unsetPrompt() { @@ -139,6 +148,7 @@ constructor( _userId.value = null _challenge.value = null _kind.value = PromptKind.Biometric() + _opPackageName.value = null } companion object { diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/LogContextInteractor.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/LogContextInteractor.kt index 863ba8d15b47..70be0f249b33 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/LogContextInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/LogContextInteractor.kt @@ -63,6 +63,9 @@ interface LogContextInteractor { /** Current display state, defined as [AuthenticateOptions.DisplayState] */ val displayState: Flow<Int> + /** If touches on the fingerprint sensor should be ignored by the HAL. */ + val isHardwareIgnoringTouches: Flow<Boolean> + /** * Add a permanent context listener. * @@ -79,12 +82,11 @@ constructor( @Application private val applicationScope: CoroutineScope, private val foldProvider: FoldStateProvider, keyguardTransitionInteractor: KeyguardTransitionInteractor, + udfpsOverlayInteractor: UdfpsOverlayInteractor, ) : LogContextInteractor { init { - applicationScope.launch { - foldProvider.start() - } + applicationScope.launch { foldProvider.start() } } override val displayState = @@ -102,6 +104,9 @@ constructor( } } + override val isHardwareIgnoringTouches: Flow<Boolean> = + udfpsOverlayInteractor.shouldHandleTouches.map { shouldHandle -> !shouldHandle } + override val isAod = displayState.map { it == AuthenticateOptions.DISPLAY_STATE_AOD }.distinctUntilChanged() @@ -159,6 +164,12 @@ constructor( .catch { t -> Log.w(TAG, "failed to notify new display state", t) } .launchIn(this) + isHardwareIgnoringTouches + .distinctUntilChanged() + .onEach { state -> listener.onHardwareIgnoreTouchesChanged(state) } + .catch { t -> Log.w(TAG, "failed to notify new set ignore state", t) } + .launchIn(this) + listener.asBinder().linkToDeath({ cancel() }, 0) } } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/PromptCredentialInteractor.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/PromptCredentialInteractor.kt index ac4b717a23ec..359e2e703ee6 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/PromptCredentialInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/PromptCredentialInteractor.kt @@ -115,12 +115,14 @@ constructor( @Utils.CredentialType kind: Int, userId: Int, challenge: Long, + opPackageName: String, ) { biometricPromptRepository.setPrompt( promptInfo, userId, challenge, - kind.asBiometricPromptCredential() + kind.asBiometricPromptCredential(), + opPackageName, ) } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractor.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractor.kt index 65a2c0a2490f..b3f95748ccdc 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractor.kt @@ -76,6 +76,7 @@ interface PromptSelectorInteractor { userId: Int, challenge: Long, modalities: BiometricModalities, + opPackageName: String, ) /** Use credential-based authentication instead of biometrics. */ @@ -84,6 +85,7 @@ interface PromptSelectorInteractor { @Utils.CredentialType kind: Int, userId: Int, challenge: Long, + opPackageName: String, ) /** Unset the current authentication request. */ @@ -104,9 +106,12 @@ constructor( promptRepository.promptInfo, promptRepository.challenge, promptRepository.userId, - promptRepository.kind - ) { promptInfo, challenge, userId, kind -> - if (promptInfo == null || userId == null || challenge == null) { + promptRepository.kind, + promptRepository.opPackageName, + ) { promptInfo, challenge, userId, kind, opPackageName -> + if ( + promptInfo == null || userId == null || challenge == null || opPackageName == null + ) { return@combine null } @@ -117,6 +122,7 @@ constructor( userInfo = BiometricUserInfo(userId = userId), operationInfo = BiometricOperationInfo(gatekeeperChallenge = challenge), modalities = kind.activeModalities, + opPackageName = opPackageName, ) else -> null } @@ -152,13 +158,15 @@ constructor( promptInfo: PromptInfo, userId: Int, challenge: Long, - modalities: BiometricModalities + modalities: BiometricModalities, + opPackageName: String, ) { promptRepository.setPrompt( promptInfo = promptInfo, userId = userId, gatekeeperChallenge = challenge, kind = PromptKind.Biometric(modalities), + opPackageName = opPackageName, ) } @@ -167,12 +175,14 @@ constructor( @Utils.CredentialType kind: Int, userId: Int, challenge: Long, + opPackageName: String, ) { promptRepository.setPrompt( promptInfo = promptInfo, userId = userId, gatekeeperChallenge = challenge, kind = kind.asBiometricPromptCredential(), + opPackageName = opPackageName, ) } 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 f4a2811a1b5a..4fc1b5841047 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 @@ -30,8 +30,10 @@ import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.channels.awaitClose 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.map import kotlinx.coroutines.flow.stateIn @@ -56,6 +58,16 @@ constructor( return isUdfpsEnrolled && isWithinOverlayBounds } + /** Sets whether Udfps overlay should handle touches */ + fun setHandleTouches(shouldHandle: Boolean = true) { + _shouldHandleTouches.value = shouldHandle + } + + private var _shouldHandleTouches = MutableStateFlow(true) + + /** Whether Udfps overlay should handle touches */ + val shouldHandleTouches: StateFlow<Boolean> = _shouldHandleTouches.asStateFlow() + /** Returns the current udfpsOverlayParams */ val udfpsOverlayParams: StateFlow<UdfpsOverlayParams> = ConflatedCallbackFlow.conflatedCallbackFlow { diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/model/BiometricPromptRequest.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/model/BiometricPromptRequest.kt index 437793798567..c17c8dced668 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/domain/model/BiometricPromptRequest.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/domain/model/BiometricPromptRequest.kt @@ -1,5 +1,6 @@ package com.android.systemui.biometrics.domain.model +import android.graphics.Bitmap import android.hardware.biometrics.PromptContentView import android.hardware.biometrics.PromptInfo import com.android.systemui.biometrics.shared.model.BiometricModalities @@ -26,6 +27,7 @@ sealed class BiometricPromptRequest( userInfo: BiometricUserInfo, operationInfo: BiometricOperationInfo, val modalities: BiometricModalities, + val opPackageName: String, ) : BiometricPromptRequest( title = info.title?.toString() ?: "", @@ -36,6 +38,8 @@ sealed class BiometricPromptRequest( showEmergencyCallButton = info.isShowEmergencyCallButton ) { val contentView: PromptContentView? = info.contentView + val logoRes: Int = info.logoRes + val logoBitmap: Bitmap? = info.logoBitmap val negativeButtonText: String = info.negativeButtonText?.toString() ?: "" } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/BiometricPromptLayout.java b/packages/SystemUI/src/com/android/systemui/biometrics/ui/BiometricPromptLayout.java index 60b454e9670e..b450896729b7 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/BiometricPromptLayout.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/BiometricPromptLayout.java @@ -115,6 +115,12 @@ public class BiometricPromptLayout extends LinearLayout { MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(iconView.getLayoutParams().height, MeasureSpec.EXACTLY)); + } else if (child.getId() == R.id.logo) { + child.measure( + MeasureSpec.makeMeasureSpec(child.getLayoutParams().width, + MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(child.getLayoutParams().height, + MeasureSpec.EXACTLY)); } else if (child.getId() == R.id.biometric_icon) { child.measure( MeasureSpec.makeMeasureSpec(width, MeasureSpec.AT_MOST), 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 22b02da5a7d5..16e7f05fae37 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 @@ -20,9 +20,9 @@ import android.content.Context import android.content.res.Resources import android.content.res.Resources.Theme import android.graphics.Paint -import android.hardware.biometrics.PromptContentListItem -import android.hardware.biometrics.PromptContentListItemBulletedText -import android.hardware.biometrics.PromptContentListItemPlainText +import android.hardware.biometrics.PromptContentItem +import android.hardware.biometrics.PromptContentItemBulletedText +import android.hardware.biometrics.PromptContentItemPlainText import android.hardware.biometrics.PromptContentView import android.hardware.biometrics.PromptVerticalListContentView import android.text.SpannableString @@ -111,21 +111,21 @@ private fun createNewRowLayout(inflater: LayoutInflater): LinearLayout { return inflater.inflate(R.layout.biometric_prompt_content_row_layout, null) as LinearLayout } -private fun PromptContentListItem.doesExceedMaxLinesIfTwoColumn( +private fun PromptContentItem.doesExceedMaxLinesIfTwoColumn( resources: Resources, ): Boolean { val passedInText: CharSequence = when (this) { - is PromptContentListItemPlainText -> text - is PromptContentListItemBulletedText -> text + is PromptContentItemPlainText -> text + is PromptContentItemBulletedText -> text else -> { - throw IllegalStateException("No such ListItem: $this") + throw IllegalStateException("No such PromptContentItem: $this") } } when (this) { - is PromptContentListItemPlainText, - is PromptContentListItemBulletedText -> { + is PromptContentItemPlainText, + is PromptContentItemBulletedText -> { val dialogMargin = resources.getDimensionPixelSize(R.dimen.biometric_dialog_border_padding) val halfDialogWidth = @@ -155,12 +155,12 @@ private fun PromptContentListItem.doesExceedMaxLinesIfTwoColumn( return numLines > maxLines } else -> { - throw IllegalStateException("No such ListItem: $this") + throw IllegalStateException("No such PromptContentItem: $this") } } } -private fun PromptContentListItem.toView( +private fun PromptContentItem.toView( resources: Resources, inflater: LayoutInflater, theme: Theme, @@ -171,10 +171,10 @@ private fun PromptContentListItem.toView( textView.layoutParams = lp when (this) { - is PromptContentListItemPlainText -> { + is PromptContentItemPlainText -> { textView.text = text } - is PromptContentListItemBulletedText -> { + is PromptContentItemBulletedText -> { val bulletedText = SpannableString(text) val span = BulletSpan( @@ -186,25 +186,25 @@ private fun PromptContentListItem.toView( textView.text = bulletedText } else -> { - throw IllegalStateException("No such ListItem: $this") + throw IllegalStateException("No such PromptContentItem: $this") } } return textView } -private fun PromptContentListItem.getListItemPadding(resources: Resources): Int { +private fun PromptContentItem.getListItemPadding(resources: Resources): Int { var listItemPadding = resources.getDimensionPixelSize( R.dimen.biometric_prompt_content_list_item_padding_horizontal ) * 2 when (this) { - is PromptContentListItemPlainText -> {} - is PromptContentListItemBulletedText -> { + is PromptContentItemPlainText -> {} + is PromptContentItemBulletedText -> { listItemPadding += getListItemBulletRadius(resources) * 2 + getListItemBulletGapWidth(resources) } else -> { - throw IllegalStateException("No such ListItem: $this") + throw IllegalStateException("No such PromptContentItem: $this") } } return listItemPadding diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt index 04dc7a8da9f3..285ab4a800b6 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt @@ -31,6 +31,7 @@ import android.view.View import android.view.View.IMPORTANT_FOR_ACCESSIBILITY_NO import android.view.accessibility.AccessibilityManager import android.widget.Button +import android.widget.ImageView import android.widget.ScrollView import android.widget.TextView import androidx.lifecycle.DefaultLifecycleObserver @@ -92,6 +93,7 @@ object BiometricViewBinder { val textColorHint = view.resources.getColor(R.color.biometric_dialog_gray, view.context.theme) + val logoView = view.requireViewById<ImageView>(R.id.logo) val titleView = view.requireViewById<TextView>(R.id.title) val subtitleView = view.requireViewById<TextView>(R.id.subtitle) val descriptionView = view.requireViewById<TextView>(R.id.description) @@ -99,6 +101,8 @@ object BiometricViewBinder { view.requireViewById<ScrollView>(R.id.customized_view_container) // set selected to enable marquee unless a screen reader is enabled + logoView.isSelected = + !accessibilityManager.isEnabled || !accessibilityManager.isTouchExplorationEnabled titleView.isSelected = !accessibilityManager.isEnabled || !accessibilityManager.isTouchExplorationEnabled subtitleView.isSelected = @@ -152,6 +156,7 @@ object BiometricViewBinder { } } + logoView.setImageDrawable(viewModel.logo.first()) titleView.text = viewModel.title.first() subtitleView.text = viewModel.subtitle.first() descriptionView.text = viewModel.description.first() @@ -183,6 +188,7 @@ object BiometricViewBinder { viewModel = viewModel, viewsToHideWhenSmall = listOf( + logoView, titleView, subtitleView, descriptionView, @@ -190,6 +196,7 @@ object BiometricViewBinder { ), viewsToFadeInOnSizeChange = listOf( + logoView, titleView, subtitleView, descriptionView, diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewSizeBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewSizeBinder.kt index c3bbaedb2670..d5695f31f121 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewSizeBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewSizeBinder.kt @@ -25,6 +25,7 @@ import android.view.ViewGroup import android.view.WindowInsets import android.view.WindowManager import android.view.accessibility.AccessibilityManager +import android.widget.ImageView import android.widget.TextView import androidx.core.animation.addListener import androidx.core.view.doOnLayout @@ -234,7 +235,13 @@ private fun View.isLandscape(): Boolean { private fun View.showContentOrHide(forceHide: Boolean = false) { val isTextViewWithBlankText = this is TextView && this.text.isBlank() - visibility = if (forceHide || isTextViewWithBlankText) View.GONE else View.VISIBLE + val isImageViewWithoutImage = this is ImageView && this.drawable == null + visibility = + if (forceHide || isTextViewWithBlankText || isImageViewWithoutImage) { + View.GONE + } else { + View.VISIBLE + } } private fun View.asVerticalAnimator( 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 1c789283ec70..dca0338dc8e7 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 @@ -18,6 +18,8 @@ package com.android.systemui.biometrics.ui.viewmodel import android.content.Context import android.graphics.Rect +import android.graphics.drawable.BitmapDrawable +import android.graphics.drawable.Drawable import android.hardware.biometrics.BiometricPrompt import android.hardware.biometrics.PromptContentView import android.util.Log @@ -233,6 +235,19 @@ constructor( } } + /** Logo for the prompt. */ + val logo: Flow<Drawable?> = + promptSelectorInteractor.prompt + .map { + when { + 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) + } + } + .distinctUntilChanged() + /** Title for the prompt. */ val title: Flow<String> = promptSelectorInteractor.prompt.map { it?.title ?: "" }.distinctUntilChanged() diff --git a/packages/SystemUI/src/com/android/systemui/common/data/repository/PackageUpdateLogger.kt b/packages/SystemUI/src/com/android/systemui/common/data/repository/PackageUpdateLogger.kt index adbb37cc3a42..4184ef7c7922 100644 --- a/packages/SystemUI/src/com/android/systemui/common/data/repository/PackageUpdateLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/common/data/repository/PackageUpdateLogger.kt @@ -31,6 +31,7 @@ private fun getChangeString(model: PackageChangeModel) = is PackageChangeModel.UpdateStarted -> "started updating" is PackageChangeModel.UpdateFinished -> "finished updating" is PackageChangeModel.Changed -> "changed" + is PackageChangeModel.Empty -> throw IllegalStateException("Unexpected empty value: $model") } /** A debug logger for [PackageChangeRepository]. */ diff --git a/packages/SystemUI/src/com/android/systemui/common/data/shared/model/PackageChangeModel.kt b/packages/SystemUI/src/com/android/systemui/common/data/shared/model/PackageChangeModel.kt index 853eff77b66c..3ae87c327e50 100644 --- a/packages/SystemUI/src/com/android/systemui/common/data/shared/model/PackageChangeModel.kt +++ b/packages/SystemUI/src/com/android/systemui/common/data/shared/model/PackageChangeModel.kt @@ -23,6 +23,14 @@ sealed interface PackageChangeModel { val packageName: String val packageUid: Int + /** Empty change, provided for convenience when a sensible default value is needed. */ + data object Empty : PackageChangeModel { + override val packageName: String + get() = "" + override val packageUid: Int + get() = 0 + } + /** * An existing application package was uninstalled. * diff --git a/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.kt b/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.kt index 10768ea6122a..dc07c1b25678 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.kt @@ -18,6 +18,7 @@ package com.android.systemui.communal.dagger import com.android.systemui.communal.data.db.CommunalDatabaseModule import com.android.systemui.communal.data.repository.CommunalMediaRepositoryModule +import com.android.systemui.communal.data.repository.CommunalPrefsRepositoryModule import com.android.systemui.communal.data.repository.CommunalRepositoryModule import com.android.systemui.communal.data.repository.CommunalTutorialRepositoryModule import com.android.systemui.communal.data.repository.CommunalWidgetRepositoryModule @@ -34,6 +35,7 @@ import dagger.Module CommunalTutorialRepositoryModule::class, CommunalWidgetRepositoryModule::class, CommunalDatabaseModule::class, + CommunalPrefsRepositoryModule::class, ] ) interface CommunalModule { diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalPrefsRepository.kt b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalPrefsRepository.kt new file mode 100644 index 000000000000..c2ea2e93ce58 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalPrefsRepository.kt @@ -0,0 +1,108 @@ +/* + * 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.data.repository + +import android.content.Context +import android.content.SharedPreferences +import android.content.pm.UserInfo +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.settings.UserFileManager +import com.android.systemui.settings.UserFileManagerExt.observeSharedPreferences +import com.android.systemui.user.data.repository.UserRepository +import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.withContext + +/** + * Stores simple preferences for the current user in communal hub. For use cases like "has the CTA + * tile been dismissed?" + */ +interface CommunalPrefsRepository { + + /** Whether the CTA tile has been dismissed. */ + val isCtaDismissed: Flow<Boolean> + + /** Save the CTA tile dismissed state for the current user. */ + suspend fun setCtaDismissedForCurrentUser() +} + +@OptIn(ExperimentalCoroutinesApi::class) +@SysUISingleton +class CommunalPrefsRepositoryImpl +@Inject +constructor( + @Background private val backgroundScope: CoroutineScope, + @Background private val bgDispatcher: CoroutineDispatcher, + private val userRepository: UserRepository, + private val userFileManager: UserFileManager, +) : CommunalPrefsRepository { + + override val isCtaDismissed: Flow<Boolean> = + userRepository.selectedUserInfo + .flatMapLatest(::observeCtaDismissState) + .stateIn( + scope = backgroundScope, + started = SharingStarted.WhileSubscribed(), + initialValue = false, + ) + + override suspend fun setCtaDismissedForCurrentUser() = + withContext(bgDispatcher) { + getSharedPrefsForUser(userRepository.getSelectedUserInfo()) + .edit() + .putBoolean(CTA_DISMISSED_STATE, true) + .apply() + } + + private fun observeCtaDismissState(user: UserInfo): Flow<Boolean> = + userFileManager + .observeSharedPreferences(FILE_NAME, Context.MODE_PRIVATE, user.id) + // Emit at the start of collection to ensure we get an initial value + .onStart { emit(Unit) } + .map { getCtaDismissedState() } + .flowOn(bgDispatcher) + + private suspend fun getCtaDismissedState(): Boolean = + withContext(bgDispatcher) { + getSharedPrefsForUser(userRepository.getSelectedUserInfo()) + .getBoolean(CTA_DISMISSED_STATE, false) + } + + private fun getSharedPrefsForUser(user: UserInfo): SharedPreferences { + return userFileManager.getSharedPreferences( + FILE_NAME, + Context.MODE_PRIVATE, + user.id, + ) + } + + companion object { + const val TAG = "CommunalRepository" + const val FILE_NAME = "communal_hub_prefs" + const val CTA_DISMISSED_STATE = "cta_dismissed" + } +} diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalPrefsRepositoryModule.kt b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalPrefsRepositoryModule.kt new file mode 100644 index 000000000000..a4ff6d3dfbef --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalPrefsRepositoryModule.kt @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.systemui.communal.data.repository + +import dagger.Binds +import dagger.Module + +@Module +interface CommunalPrefsRepositoryModule { + @Binds fun communalPrefsRepository(impl: CommunalPrefsRepositoryImpl): CommunalPrefsRepository +} 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 553b3ebc0813..1f4be4060223 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 @@ -56,9 +56,6 @@ interface CommunalRepository { /** Exposes the transition state of the communal [SceneTransitionLayout]. */ val transitionState: StateFlow<ObservableCommunalTransitionState> - /** Whether the CTA tile is visible in the hub under view mode. */ - val isCtaTileInViewModeVisible: Flow<Boolean> - /** Updates the requested scene. */ fun setDesiredScene(desiredScene: CommunalSceneKey) @@ -68,9 +65,6 @@ interface CommunalRepository { * Note that you must call is with `null` when the UI is done or risk a memory leak. */ fun setTransitionState(transitionState: Flow<ObservableCommunalTransitionState>?) - - /** Updates whether to display the CTA tile in the hub under view mode. */ - fun setCtaTileInViewModeVisibility(isVisible: Boolean) } @OptIn(ExperimentalCoroutinesApi::class) @@ -102,16 +96,6 @@ constructor( initialValue = defaultTransitionState, ) - // TODO(b/313462210) - persist the value in local storage, so the tile won't show up again - // once dismissed. - private val _isCtaTileInViewModeVisible: MutableStateFlow<Boolean> = MutableStateFlow(true) - override val isCtaTileInViewModeVisible: Flow<Boolean> = - _isCtaTileInViewModeVisible.asStateFlow() - - override fun setCtaTileInViewModeVisibility(isVisible: Boolean) { - _isCtaTileInViewModeVisible.value = isVisible - } - override fun setDesiredScene(desiredScene: CommunalSceneKey) { _desiredScene.value = desiredScene } 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 e6816e954b5d..bfc5019801d0 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 @@ -16,7 +16,6 @@ package com.android.systemui.communal.data.repository -import android.appwidget.AppWidgetHost import android.appwidget.AppWidgetManager import android.content.ComponentName import android.content.Intent @@ -29,6 +28,7 @@ import com.android.systemui.communal.data.db.CommunalWidgetDao import com.android.systemui.communal.data.db.CommunalWidgetItem 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.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background @@ -83,7 +83,7 @@ class CommunalWidgetRepositoryImpl @Inject constructor( private val appWidgetManager: Optional<AppWidgetManager>, - private val appWidgetHost: AppWidgetHost, + private val appWidgetHost: CommunalAppWidgetHost, @Application private val applicationScope: CoroutineScope, @Background private val bgDispatcher: CoroutineDispatcher, broadcastDispatcher: BroadcastDispatcher, diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryModule.kt b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryModule.kt index d0d9e3fabc7b..52f42c1aba4f 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryModule.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryModule.kt @@ -17,11 +17,11 @@ package com.android.systemui.communal.data.repository -import android.appwidget.AppWidgetHost import android.appwidget.AppWidgetManager import android.content.Context import android.content.res.Resources import com.android.systemui.communal.shared.CommunalWidgetHost +import com.android.systemui.communal.widgets.CommunalAppWidgetHost import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Main @@ -48,15 +48,15 @@ interface CommunalWidgetRepositoryModule { @SysUISingleton @Provides - fun provideAppWidgetHost(@Application context: Context): AppWidgetHost { - return AppWidgetHost(context, APP_WIDGET_HOST_ID) + fun provideCommunalAppWidgetHost(@Application context: Context): CommunalAppWidgetHost { + return CommunalAppWidgetHost(context, APP_WIDGET_HOST_ID) } @SysUISingleton @Provides fun provideCommunalWidgetHost( appWidgetManager: Optional<AppWidgetManager>, - appWidgetHost: AppWidgetHost, + appWidgetHost: CommunalAppWidgetHost, @CommunalLog logBuffer: LogBuffer, ): CommunalWidgetHost { return CommunalWidgetHost(appWidgetManager, appWidgetHost, logBuffer) 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 9fa4cd6c7985..aa4a9d0d9569 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 @@ -17,9 +17,9 @@ package com.android.systemui.communal.domain.interactor import android.app.smartspace.SmartspaceTarget -import android.appwidget.AppWidgetHost import android.content.ComponentName import com.android.systemui.communal.data.repository.CommunalMediaRepository +import com.android.systemui.communal.data.repository.CommunalPrefsRepository import com.android.systemui.communal.data.repository.CommunalRepository import com.android.systemui.communal.data.repository.CommunalWidgetRepository import com.android.systemui.communal.domain.model.CommunalContentModel @@ -29,6 +29,7 @@ import com.android.systemui.communal.shared.model.CommunalContentSize.HALF import com.android.systemui.communal.shared.model.CommunalContentSize.THIRD import com.android.systemui.communal.shared.model.CommunalSceneKey import com.android.systemui.communal.shared.model.ObservableCommunalTransitionState +import com.android.systemui.communal.widgets.CommunalAppWidgetHost import com.android.systemui.communal.widgets.EditWidgetsActivityStarter import com.android.systemui.dagger.SysUISingleton import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor @@ -49,10 +50,11 @@ class CommunalInteractor constructor( private val communalRepository: CommunalRepository, private val widgetRepository: CommunalWidgetRepository, + private val communalPrefsRepository: CommunalPrefsRepository, mediaRepository: CommunalMediaRepository, smartspaceRepository: SmartspaceRepository, keyguardInteractor: KeyguardInteractor, - private val appWidgetHost: AppWidgetHost, + private val appWidgetHost: CommunalAppWidgetHost, private val editWidgetsActivityStarter: EditWidgetsActivityStarter ) { @@ -122,7 +124,7 @@ constructor( } /** Dismiss the CTA tile from the hub in view mode. */ - fun dismissCtaTile() = communalRepository.setCtaTileInViewModeVisibility(isVisible = false) + suspend fun dismissCtaTile() = communalPrefsRepository.setCtaDismissedForCurrentUser() /** * Add a widget at the specified position. @@ -174,8 +176,8 @@ constructor( /** CTA tile to be displayed in the glanceable hub (view mode). */ val ctaTileContent: Flow<List<CommunalContentModel.CtaTileInViewMode>> = - communalRepository.isCtaTileInViewModeVisible.map { visible -> - if (visible) listOf(CommunalContentModel.CtaTileInViewMode()) else emptyList() + communalPrefsRepository.isCtaDismissed.map { isDismissed -> + if (isDismissed) emptyList() else listOf(CommunalContentModel.CtaTileInViewMode()) } /** A list of tutorial content to be displayed in the communal hub in tutorial mode. */ diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/model/CommunalContentModel.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/model/CommunalContentModel.kt index 46f957f3aaf2..0d52afd4fff5 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/domain/model/CommunalContentModel.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/domain/model/CommunalContentModel.kt @@ -16,10 +16,10 @@ package com.android.systemui.communal.domain.model -import android.appwidget.AppWidgetHost import android.appwidget.AppWidgetProviderInfo import android.widget.RemoteViews import com.android.systemui.communal.shared.model.CommunalContentSize +import com.android.systemui.communal.widgets.CommunalAppWidgetHost import java.util.UUID /** Encapsulates data for a communal content. */ @@ -44,7 +44,7 @@ sealed interface CommunalContentModel { class Widget( val appWidgetId: Int, val providerInfo: AppWidgetProviderInfo, - val appWidgetHost: AppWidgetHost, + val appWidgetHost: CommunalAppWidgetHost, ) : CommunalContentModel { override val key = KEY.widget(appWidgetId) // Widget size is always half. diff --git a/packages/SystemUI/src/com/android/systemui/communal/shared/CommunalWidgetHost.kt b/packages/SystemUI/src/com/android/systemui/communal/shared/CommunalWidgetHost.kt index 41f9cb4c98ed..7fe37ccf0dc8 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/shared/CommunalWidgetHost.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/shared/CommunalWidgetHost.kt @@ -16,11 +16,11 @@ package com.android.systemui.communal.shared -import android.appwidget.AppWidgetHost import android.appwidget.AppWidgetManager import android.appwidget.AppWidgetProviderInfo.WIDGET_FEATURE_CONFIGURATION_OPTIONAL import android.appwidget.AppWidgetProviderInfo.WIDGET_FEATURE_RECONFIGURABLE import android.content.ComponentName +import com.android.systemui.communal.widgets.CommunalAppWidgetHost import com.android.systemui.log.LogBuffer import com.android.systemui.log.core.Logger import com.android.systemui.log.dagger.CommunalLog @@ -35,7 +35,7 @@ class CommunalWidgetHost @Inject constructor( private val appWidgetManager: Optional<AppWidgetManager>, - private val appWidgetHost: AppWidgetHost, + private val appWidgetHost: CommunalAppWidgetHost, @CommunalLog logBuffer: LogBuffer, ) { companion object { 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 84708a49f469..4da348e6a92a 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 @@ -24,6 +24,7 @@ import com.android.systemui.communal.shared.model.CommunalSceneKey import com.android.systemui.communal.shared.model.ObservableCommunalTransitionState import com.android.systemui.media.controls.ui.MediaHost import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.flowOf @@ -36,6 +37,15 @@ abstract class BaseCommunalViewModel( val currentScene: StateFlow<CommunalSceneKey> = communalInteractor.desiredScene + /** Whether widgets are currently being re-ordered. */ + open val reorderingWidgets: StateFlow<Boolean> = MutableStateFlow(false) + + private val _selectedIndex: MutableStateFlow<Int?> = MutableStateFlow(null) + + /** The index of the currently selected item, or null if no item selected. */ + val selectedIndex: StateFlow<Int?> + get() = _selectedIndex + fun onSceneChanged(scene: CommunalSceneKey) { communalInteractor.onSceneChanged(scene) } @@ -105,4 +115,9 @@ abstract class BaseCommunalViewModel( /** Called as the user cancels dragging a widget to reorder. */ open fun onReorderWidgetCancel() {} + + /** Set the index of the currently selected item */ + fun setSelectedIndex(index: Int?) { + _selectedIndex.value = index + } } 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 7faf653cc177..317dd4040e7c 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 @@ -20,7 +20,6 @@ import android.app.Activity import android.app.Activity.RESULT_CANCELED import android.app.Activity.RESULT_OK import android.app.ActivityOptions -import android.appwidget.AppWidgetHost import android.content.ActivityNotFoundException import android.content.ComponentName import android.widget.RemoteViews @@ -28,6 +27,7 @@ import com.android.internal.logging.UiEventLogger import com.android.systemui.communal.domain.interactor.CommunalInteractor import com.android.systemui.communal.domain.model.CommunalContentModel import com.android.systemui.communal.shared.log.CommunalUiEvent +import com.android.systemui.communal.widgets.CommunalAppWidgetHost import com.android.systemui.dagger.SysUISingleton import com.android.systemui.media.controls.ui.MediaHost import com.android.systemui.media.dagger.MediaModule @@ -37,7 +37,10 @@ import javax.inject.Named import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach /** The view model for communal hub in edit mode. */ @SysUISingleton @@ -45,7 +48,7 @@ class CommunalEditModeViewModel @Inject constructor( private val communalInteractor: CommunalInteractor, - private val appWidgetHost: AppWidgetHost, + private val appWidgetHost: CommunalAppWidgetHost, @Named(MediaModule.COMMUNAL_HUB) mediaHost: MediaHost, private val uiEventLogger: UiEventLogger, ) : BaseCommunalViewModel(communalInteractor, mediaHost) { @@ -69,9 +72,15 @@ constructor( // Only widgets are editable. The CTA tile comes last in the list and remains visible. override val communalContent: Flow<List<CommunalContentModel>> = - communalInteractor.widgetContent.map { widgets -> - widgets + listOf(CommunalContentModel.CtaTileInEditMode()) - } + communalInteractor.widgetContent + // Clear the selected index when the list is updated. + .onEach { setSelectedIndex(null) } + .map { widgets -> widgets + listOf(CommunalContentModel.CtaTileInEditMode()) } + + private val _reorderingWidgets = MutableStateFlow(false) + + override val reorderingWidgets: StateFlow<Boolean> + get() = _reorderingWidgets override fun onDeleteWidget(id: Int) = communalInteractor.deleteWidget(id) @@ -135,14 +144,19 @@ constructor( } override fun onReorderWidgetStart() { + // Clear selection status + setSelectedIndex(null) + _reorderingWidgets.value = true uiEventLogger.log(CommunalUiEvent.COMMUNAL_HUB_REORDER_WIDGET_START) } override fun onReorderWidgetEnd() { + _reorderingWidgets.value = false uiEventLogger.log(CommunalUiEvent.COMMUNAL_HUB_REORDER_WIDGET_FINISH) } override fun onReorderWidgetCancel() { + _reorderingWidgets.value = false uiEventLogger.log(CommunalUiEvent.COMMUNAL_HUB_REORDER_WIDGET_CANCEL) } } diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt index 7a96fabd2433..d619362b0311 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt @@ -23,7 +23,9 @@ import com.android.systemui.communal.domain.model.CommunalContentModel import com.android.systemui.communal.widgets.WidgetInteractionHandler import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.media.controls.ui.MediaHierarchyManager import com.android.systemui.media.controls.ui.MediaHost +import com.android.systemui.media.controls.ui.MediaHostState import com.android.systemui.media.dagger.MediaModule import javax.inject.Inject import javax.inject.Named @@ -70,12 +72,25 @@ constructor( override val isPopupOnDismissCtaShowing: Flow<Boolean> = _isPopupOnDismissCtaShowing.asStateFlow() + init { + // Initialize our media host for the UMO. This only needs to happen once and must be done + // before the MediaHierarchyManager attempts to move the UMO to the hub. + with(mediaHost) { + expansion = MediaHostState.EXPANDED + showsOnlyActiveMedia = false + falsingProtectionNeeded = false + init(MediaHierarchyManager.LOCATION_COMMUNAL_HUB) + } + } + override fun onOpenWidgetEditor() = communalInteractor.showWidgetEditor() override fun onDismissCtaTile() { - communalInteractor.dismissCtaTile() - setPopupOnDismissCtaVisibility(true) - schedulePopupHiding() + scope.launch { + communalInteractor.dismissCtaTile() + setPopupOnDismissCtaVisibility(true) + schedulePopupHiding() + } } override fun getInteractionHandler(): RemoteViews.InteractionHandler = interactionHandler diff --git a/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHost.kt b/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHost.kt new file mode 100644 index 000000000000..003c9d50e789 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHost.kt @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.communal.widgets + +import android.appwidget.AppWidgetHost +import android.appwidget.AppWidgetHostView +import android.appwidget.AppWidgetProviderInfo +import android.content.Context + +/** Communal app widget host that creates a [CommunalAppWidgetHostView]. */ +class CommunalAppWidgetHost(context: Context, hostId: Int) : AppWidgetHost(context, hostId) { + override fun onCreateView( + context: Context, + appWidgetId: Int, + appWidget: AppWidgetProviderInfo? + ): AppWidgetHostView { + return CommunalAppWidgetHostView(context) + } + + /** + * Creates and returns a [CommunalAppWidgetHostView]. This method does the same thing as + * `createView`. The only difference is that the returned value will be casted to + * [CommunalAppWidgetHostView]. + */ + fun createViewForCommunal( + context: Context?, + appWidgetId: Int, + appWidget: AppWidgetProviderInfo? + ): CommunalAppWidgetHostView { + // `createView` internally calls `onCreateView` to create the view. We cannot override + // `createView`, but we are sure that the hostView is `CommunalAppWidgetHostView` + return createView(context, appWidgetId, appWidget) as CommunalAppWidgetHostView + } +} diff --git a/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostView.kt b/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostView.kt new file mode 100644 index 000000000000..2b7d82395b89 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostView.kt @@ -0,0 +1,76 @@ +/* + * 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.appwidget.AppWidgetHostView +import android.content.Context +import android.graphics.Outline +import android.graphics.Rect +import android.view.View +import android.view.ViewOutlineProvider + +/** AppWidgetHostView that displays in communal hub with support for rounded corners. */ +class CommunalAppWidgetHostView(context: Context) : AppWidgetHostView(context) { + // Mutable corner radius. + var enforcedCornerRadius: Float + + // Mutable `Rect`. The size will be mutated when the widget is reapplied. + var enforcedRectangle: Rect + + init { + enforcedCornerRadius = RoundedCornerEnforcement.computeEnforcedRadius(context) + enforcedRectangle = Rect() + } + + override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) { + super.onLayout(changed, l, t, r, b) + + enforceRoundedCorners() + } + + private val cornerRadiusEnforcementOutline: ViewOutlineProvider = + object : ViewOutlineProvider() { + override fun getOutline(view: View?, outline: Outline) { + if (enforcedRectangle.isEmpty || enforcedCornerRadius <= 0) { + outline.setEmpty() + } else { + outline.setRoundRect(enforcedRectangle, enforcedCornerRadius) + } + } + } + + private fun enforceRoundedCorners() { + if (enforcedCornerRadius <= 0) { + resetRoundedCorners() + return + } + val background: View? = RoundedCornerEnforcement.findBackground(this) + if (background == null || RoundedCornerEnforcement.hasAppWidgetOptedOut(this, background)) { + resetRoundedCorners() + return + } + RoundedCornerEnforcement.computeRoundedRectangle(this, background, enforcedRectangle) + outlineProvider = cornerRadiusEnforcementOutline + clipToOutline = true + invalidateOutline() + } + + private fun resetRoundedCorners() { + outlineProvider = ViewOutlineProvider.BACKGROUND + clipToOutline = false + } +} diff --git a/packages/SystemUI/src/com/android/systemui/communal/widgets/RoundedCornerEnforcement.kt b/packages/SystemUI/src/com/android/systemui/communal/widgets/RoundedCornerEnforcement.kt new file mode 100644 index 000000000000..abda44be09fa --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/communal/widgets/RoundedCornerEnforcement.kt @@ -0,0 +1,154 @@ +/* + * 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.annotation.IdRes +import android.annotation.Nullable +import android.content.Context +import android.content.res.Resources +import android.graphics.Rect +import android.view.View +import android.view.ViewGroup +import androidx.core.os.BuildCompat.isAtLeastS +import com.android.systemui.res.R +import kotlin.math.min + +/** + * Utilities to compute the enforced use of rounded corners on App Widgets. This is a fork of the + * Launcher3 source code to enforce the same visual treatment on communal hub. + */ +internal object RoundedCornerEnforcement { + /** + * Find the background view for a widget. + * + * @param appWidget the view containing the App Widget (typically the instance of + * [CommunalAppWidgetHostView]). + */ + fun findBackground(appWidget: View): View? { + val backgrounds = findViewsWithId(appWidget, R.id.background) + if (backgrounds.size == 1) { + return backgrounds[0] + } + // Really, the argument should contain the widget, so it cannot be the background. + if (appWidget is ViewGroup) { + val vg = appWidget + if (vg.childCount > 0) { + return findUndefinedBackground(vg.getChildAt(0)) + } + } + return appWidget + } + + /** Check whether the app widget has opted out of the enforcement. */ + fun hasAppWidgetOptedOut(appWidget: View?, background: View): Boolean { + return background.id == R.id.background && background.clipToOutline + } + + /** + * Computes the rounded rectangle needed for this app widget. + * + * @param appWidget View onto which the rounded rectangle will be applied. + * @param background Background view. This must be either `appWidget` or a descendant of + * `appWidget`. + * @param outRect Rectangle set to the rounded rectangle coordinates, in the reference frame of + * `appWidget`. + */ + fun computeRoundedRectangle(appWidget: View, background: View, outRect: Rect) { + var background = background + outRect.left = 0 + outRect.right = background.width + outRect.top = 0 + outRect.bottom = background.height + while (background !== appWidget) { + outRect.offset(background.left, background.top) + background = background.parent as View + } + } + + /** Get the radius of the rounded rectangle defined in the host's resource. */ + private fun getOwnedEnforcedRadius(context: Context): Float { + val res: Resources = context.resources + return res.getDimension(R.dimen.communal_enforced_rounded_corner_max_radius) + } + + /** + * Computes the radius of the rounded rectangle that should be applied to a widget expanded in + * the given context. + */ + fun computeEnforcedRadius(context: Context): Float { + if (!isAtLeastS()) { + return 0f + } + val res: Resources = context.resources + val systemRadius: Float = + res.getDimension(android.R.dimen.system_app_widget_background_radius) + val defaultRadius = getOwnedEnforcedRadius(context) + return min(defaultRadius, systemRadius) + } + + private fun findViewsWithId(view: View, @IdRes viewId: Int): List<View> { + val output: MutableList<View> = ArrayList() + accumulateViewsWithId(view, viewId, output) + return output + } + + // Traverse views. If the predicate returns true, continue on the children, otherwise, don't. + private fun accumulateViewsWithId(view: View, @IdRes viewId: Int, output: MutableList<View>) { + if (view.id == viewId) { + output.add(view) + return + } + if (view is ViewGroup) { + val vg = view + for (i in 0 until vg.childCount) { + accumulateViewsWithId(vg.getChildAt(i), viewId, output) + } + } + } + + private fun isViewVisible(view: View): Boolean { + return if (view.visibility != View.VISIBLE) { + false + } else !view.willNotDraw() || view.foreground != null || view.background != null + } + + @Nullable + private fun findUndefinedBackground(current: View): View? { + if (current.visibility != View.VISIBLE) { + return null + } + if (isViewVisible(current)) { + return current + } + var lastVisibleView: View? = null + // Find the first view that is either not a ViewGroup, or a ViewGroup which will draw + // something, or a ViewGroup that contains more than one view. + if (current is ViewGroup) { + val vg = current + for (i in 0 until vg.childCount) { + val visibleView = findUndefinedBackground(vg.getChildAt(i)) + if (visibleView != null) { + if (lastVisibleView != null) { + return current // At least two visible children + } + lastVisibleView = visibleView + } + } + } + return lastVisibleView + } +} diff --git a/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java b/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java index b2d70523c282..6d9994fb2205 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java @@ -556,7 +556,7 @@ public class FrameworkServicesModule { @Provides @Singleton static SubscriptionManager provideSubscriptionManager(Context context) { - return context.getSystemService(SubscriptionManager.class); + return context.getSystemService(SubscriptionManager.class).createForAllUserProfiles(); } @Provides diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt index 846736c04d98..ea8ba1029782 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt +++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt @@ -102,12 +102,6 @@ object Flags { 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 = diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/KeyboardModule.kt b/packages/SystemUI/src/com/android/systemui/keyboard/KeyboardModule.kt index 496c64e1120e..c6fb4f9d6956 100644 --- a/packages/SystemUI/src/com/android/systemui/keyboard/KeyboardModule.kt +++ b/packages/SystemUI/src/com/android/systemui/keyboard/KeyboardModule.kt @@ -19,6 +19,8 @@ package com.android.systemui.keyboard import com.android.systemui.keyboard.data.repository.KeyboardRepository import com.android.systemui.keyboard.data.repository.KeyboardRepositoryImpl +import com.android.systemui.keyboard.stickykeys.data.repository.StickyKeysRepository +import com.android.systemui.keyboard.stickykeys.data.repository.StickyKeysRepositoryImpl import dagger.Binds import dagger.Module @@ -27,4 +29,9 @@ abstract class KeyboardModule { @Binds abstract fun bindKeyboardRepository(repository: KeyboardRepositoryImpl): KeyboardRepository + + @Binds + abstract fun bindStickyKeysRepository( + repository: StickyKeysRepositoryImpl + ): StickyKeysRepository } diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/stickykeys/StickyKeysLogger.kt b/packages/SystemUI/src/com/android/systemui/keyboard/stickykeys/StickyKeysLogger.kt new file mode 100644 index 000000000000..37034f63aca7 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyboard/stickykeys/StickyKeysLogger.kt @@ -0,0 +1,37 @@ +/* + * 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 + +import com.android.systemui.keyboard.stickykeys.shared.model.Locked +import com.android.systemui.keyboard.stickykeys.shared.model.ModifierKey +import com.android.systemui.log.LogBuffer +import com.android.systemui.log.core.LogLevel +import com.android.systemui.log.dagger.KeyboardLog +import javax.inject.Inject + +private const val TAG = "stickyKeys" + +class StickyKeysLogger @Inject constructor(@KeyboardLog private val buffer: LogBuffer) { + fun logNewStickyKeysReceived(linkedHashMap: Map<ModifierKey, Locked>) { + buffer.log( + TAG, + LogLevel.VERBOSE, + { str1 = linkedHashMap.toString() }, + { "new sticky keys state received: $str1" } + ) + } +}
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/stickykeys/data/repository/StickyKeysRepository.kt b/packages/SystemUI/src/com/android/systemui/keyboard/stickykeys/data/repository/StickyKeysRepository.kt new file mode 100644 index 000000000000..34d288815570 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyboard/stickykeys/data/repository/StickyKeysRepository.kt @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyboard.stickykeys.data.repository + +import android.hardware.input.InputManager +import android.hardware.input.InputManager.StickyModifierStateListener +import android.hardware.input.StickyModifierState +import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging +import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow +import com.android.systemui.dagger.qualifiers.Background +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 +import com.android.systemui.keyboard.stickykeys.shared.model.ModifierKey.ALT +import com.android.systemui.keyboard.stickykeys.shared.model.ModifierKey.ALT_GR +import com.android.systemui.keyboard.stickykeys.shared.model.ModifierKey.CTRL +import com.android.systemui.keyboard.stickykeys.shared.model.ModifierKey.META +import com.android.systemui.keyboard.stickykeys.shared.model.ModifierKey.SHIFT +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach +import javax.inject.Inject + +interface StickyKeysRepository { + val stickyKeys: Flow<LinkedHashMap<ModifierKey, Locked>> + val settingEnabled: Flow<Boolean> +} + +class StickyKeysRepositoryImpl +@Inject +constructor( + private val inputManager: InputManager, + @Background private val backgroundDispatcher: CoroutineDispatcher, + private val stickyKeysLogger: StickyKeysLogger, +) : StickyKeysRepository { + + override val stickyKeys: Flow<LinkedHashMap<ModifierKey, Locked>> = + conflatedCallbackFlow { + val listener = StickyModifierStateListener { stickyModifierState -> + trySendWithFailureLogging(stickyModifierState, TAG) + } + // after registering, InputManager calls listener with the current value + inputManager.registerStickyModifierStateListener(Runnable::run, listener) + awaitClose { inputManager.unregisterStickyModifierStateListener(listener) } + } + .map { toStickyKeysMap(it) } + .onEach { stickyKeysLogger.logNewStickyKeysReceived(it) } + .flowOn(backgroundDispatcher) + + // TODO(b/319837892): Implement reading actual setting + override val settingEnabled: StateFlow<Boolean> = MutableStateFlow(true) + + private fun toStickyKeysMap(state: StickyModifierState): LinkedHashMap<ModifierKey, Locked> { + val keys = linkedMapOf<ModifierKey, Locked>() + state.apply { + if (isAltGrModifierOn) keys[ALT_GR] = Locked(false) + if (isAltGrModifierLocked) keys[ALT_GR] = Locked(true) + if (isAltModifierOn) keys[ALT] = Locked(false) + if (isAltModifierLocked) keys[ALT] = Locked(true) + if (isCtrlModifierOn) keys[CTRL] = Locked(false) + if (isCtrlModifierLocked) keys[CTRL] = Locked(true) + if (isMetaModifierOn) keys[META] = Locked(false) + if (isMetaModifierLocked) keys[META] = Locked(true) + if (isShiftModifierOn) keys[SHIFT] = Locked(false) + if (isShiftModifierLocked) keys[SHIFT] = Locked(true) + } + return keys + } + + companion object { + const val TAG = "StickyKeysRepositoryImpl" + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/stickykeys/shared/model/StickyKey.kt b/packages/SystemUI/src/com/android/systemui/keyboard/stickykeys/shared/model/StickyKey.kt new file mode 100644 index 000000000000..d5f082a2566f --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyboard/stickykeys/shared/model/StickyKey.kt @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyboard.stickykeys.shared.model + +@JvmInline +value class Locked(val locked: Boolean) + +enum class ModifierKey(val text: String) { + ALT("ALT LEFT"), + ALT_GR("ALT RIGHT"), + CTRL("CTRL"), + META("META"), + SHIFT("SHIFT"), +} diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/stickykeys/ui/viewmodel/StickyKeysIndicatorViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyboard/stickykeys/ui/viewmodel/StickyKeysIndicatorViewModel.kt new file mode 100644 index 000000000000..26eb706da200 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyboard/stickykeys/ui/viewmodel/StickyKeysIndicatorViewModel.kt @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyboard.stickykeys.ui.viewmodel + +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.keyboard.data.repository.KeyboardRepository +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.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.stateIn +import javax.inject.Inject + +class StickyKeysIndicatorViewModel +@Inject +constructor( + stickyKeysRepository: StickyKeysRepository, + keyboardRepository: KeyboardRepository, + @Application applicationScope: CoroutineScope, +) { + + @OptIn(ExperimentalCoroutinesApi::class) + val indicatorContent: Flow<Map<ModifierKey, Locked>> = + keyboardRepository.isAnyKeyboardConnected + .flatMapLatest { keyboardPresent -> + if (keyboardPresent) stickyKeysRepository.settingEnabled else flowOf(false) + } + .flatMapLatest { enabled -> + if (enabled) stickyKeysRepository.stickyKeys else flowOf(emptyMap()) + } + .stateIn(applicationScope, SharingStarted.Lazily, emptyMap()) +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardFaceAuthModule.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardFaceAuthModule.kt index b373f8520254..fede47957a7b 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardFaceAuthModule.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardFaceAuthModule.kt @@ -53,14 +53,14 @@ interface KeyguardFaceAuthModule { @SysUISingleton @FaceAuthTableLog fun provideFaceAuthTableLog(factory: TableLogBufferFactory): TableLogBuffer { - return factory.create("FaceAuthTableLog", 100) + return factory.create("FaceAuthTableLog", 400) } @Provides @SysUISingleton @FaceDetectTableLog fun provideFaceDetectTableLog(factory: TableLogBufferFactory): TableLogBuffer { - return factory.create("FaceDetectTableLog", 100) + return factory.create("FaceDetectTableLog", 400) } } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardSmartspaceRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardSmartspaceRepository.kt new file mode 100644 index 000000000000..afe9151ac7a0 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardSmartspaceRepository.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.keyguard.data.repository + +import android.view.View +import com.android.systemui.dagger.SysUISingleton +import javax.inject.Inject +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow + +@SysUISingleton +class KeyguardSmartspaceRepository @Inject constructor() { + private val _bcSmartspaceVisibility: MutableStateFlow<Int> = MutableStateFlow(View.GONE) + val bcSmartspaceVisibility: StateFlow<Int> = _bcSmartspaceVisibility.asStateFlow() + + fun setBcSmartspaceVisibility(visibility: Int) { + _bcSmartspaceVisibility.value = visibility + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt index fedd63be1454..8fa33ee7d0ca 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt @@ -54,12 +54,33 @@ constructor( ) { override fun start() { - listenForAodToLockscreenOrOccluded() + listenForAodToLockscreen() + listenForAodToPrimaryBouncer() listenForAodToGone() + listenForAodToOccluded() listenForTransitionToCamera(scope, keyguardInteractor) } - private fun listenForAodToLockscreenOrOccluded() { + /** + * There are cases where the transition to AOD begins but never completes, such as tapping power + * during an incoming phone call when unlocked. In this case, GONE->AOD should be interrupted to + * run AOD->OCCLUDED. + */ + private fun listenForAodToOccluded() { + scope.launch { + keyguardInteractor.isKeyguardOccluded.sample(startedKeyguardState, ::Pair).collect { + (isOccluded, startedKeyguardState) -> + if (isOccluded && startedKeyguardState == KeyguardState.AOD) { + startTransitionTo( + toState = KeyguardState.OCCLUDED, + modeOnCanceled = TransitionModeOnCanceled.RESET + ) + } + } + } + } + + private fun listenForAodToLockscreen() { scope.launch { keyguardInteractor .dozeTransitionTo(DozeStateModel.FINISH) @@ -72,20 +93,15 @@ constructor( ::toTriple ) .collect { (_, lastStartedStep, occluded) -> - if (lastStartedStep.to == KeyguardState.AOD) { - val toState = - if (occluded) KeyguardState.OCCLUDED else KeyguardState.LOCKSCREEN + if (lastStartedStep.to == KeyguardState.AOD && !occluded) { val modeOnCanceled = - if ( - toState == KeyguardState.LOCKSCREEN && - lastStartedStep.from == KeyguardState.LOCKSCREEN - ) { + if (lastStartedStep.from == KeyguardState.LOCKSCREEN) { TransitionModeOnCanceled.REVERSE } else { TransitionModeOnCanceled.LAST_VALUE } startTransitionTo( - toState = toState, + toState = KeyguardState.LOCKSCREEN, modeOnCanceled = modeOnCanceled, ) } @@ -93,11 +109,26 @@ constructor( } } + /** + * If there is a biometric lockout and FPS is tapped while on AOD, it should go directly to the + * PRIMARY_BOUNCER. + */ + private fun listenForAodToPrimaryBouncer() { + scope.launch { + keyguardInteractor.primaryBouncerShowing + .sample(startedKeyguardTransitionStep, ::Pair) + .collect { (isBouncerShowing, lastStartedTransitionStep) -> + if (isBouncerShowing && lastStartedTransitionStep.to == KeyguardState.AOD) { + startTransitionTo(KeyguardState.PRIMARY_BOUNCER) + } + } + } + } + private fun listenForAodToGone() { scope.launch { keyguardInteractor.biometricUnlockState.sample(finishedKeyguardState, ::Pair).collect { - pair -> - val (biometricUnlockState, keyguardState) = pair + (biometricUnlockState, keyguardState) -> if (keyguardState == KeyguardState.AOD && isWakeAndUnlock(biometricUnlockState)) { startTransitionTo(KeyguardState.GONE) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardSmartspaceInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardSmartspaceInteractor.kt new file mode 100644 index 000000000000..67b57456a5c6 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardSmartspaceInteractor.kt @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyguard.domain.interactor + +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.keyguard.data.repository.KeyguardSmartspaceRepository +import javax.inject.Inject +import kotlinx.coroutines.flow.StateFlow + +@SysUISingleton +class KeyguardSmartspaceInteractor +@Inject +constructor(private val keyguardSmartspaceRepository: KeyguardSmartspaceRepository) { + var bcSmartspaceVisibility: StateFlow<Int> = keyguardSmartspaceRepository.bcSmartspaceVisibility + + fun setBcSmartspaceVisibility(visibility: Int) { + keyguardSmartspaceRepository.setBcSmartspaceVisibility(visibility) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt index a8223ea83e1f..b43ab5e9110d 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt @@ -37,17 +37,19 @@ import com.android.systemui.keyguard.shared.model.TransitionState import com.android.systemui.keyguard.shared.model.TransitionStep import javax.inject.Inject import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.shareIn /** Encapsulates business-logic related to the keyguard transitions. */ +@OptIn(ExperimentalCoroutinesApi::class) @SysUISingleton class KeyguardTransitionInteractor @Inject @@ -192,29 +194,121 @@ constructor( val finishedKeyguardTransitionStep: Flow<TransitionStep> = repository.transitions.filter { step -> step.transitionState == TransitionState.FINISHED } - /** The destination state of the last started transition. */ + /** The destination state of the last [TransitionState.STARTED] transition. */ val startedKeyguardState: SharedFlow<KeyguardState> = startedKeyguardTransitionStep .map { step -> step.to } .shareIn(scope, SharingStarted.Eagerly, replay = 1) - /** The last completed [KeyguardState] transition */ + /** + * The last [KeyguardState] to which we [TransitionState.FINISHED] a transition. + * + * WARNING: This will NOT emit a value if a transition is CANCELED, and will also not emit a + * value when a subsequent transition is STARTED. It will *only* emit once we have finally + * FINISHED in a state. This can have unintuitive implications. + * + * For example, if we're transitioning from GONE -> DOZING, and that transition is CANCELED in + * favor of a DOZING -> LOCKSCREEN transition, the FINISHED state is still GONE, and will remain + * GONE throughout the DOZING -> LOCKSCREEN transition until the DOZING -> LOCKSCREEN transition + * finishes (at which point we'll be FINISHED in LOCKSCREEN). + * + * Since there's no real limit to how many consecutive transitions can be canceled, it's even + * possible for the FINISHED state to be the same as the STARTED state while still + * transitioning. + * + * For example: + * 1. We're finished in GONE. + * 2. The user presses the power button, starting a GONE -> DOZING transition. We're still + * FINISHED in GONE. + * 3. The user changes their mind, pressing the power button to wake up; this starts a DOZING -> + * LOCKSCREEN transition. We're still FINISHED in GONE. + * 4. The user quickly swipes away the lockscreen prior to DOZING -> LOCKSCREEN finishing; this + * starts a LOCKSCREEN -> GONE transition. We're still FINISHED in GONE, but we've also + * STARTED a transition *to* GONE. + * 5. We'll emit KeyguardState.GONE again once the transition finishes. + * + * If you just need to know when we eventually settle into a state, this flow is likely + * sufficient. However, if you're having issues with state *during* transitions started after + * one or more canceled transitions, you probably need to use [currentKeyguardState]. + */ val finishedKeyguardState: SharedFlow<KeyguardState> = finishedKeyguardTransitionStep .map { step -> step.to } .shareIn(scope, SharingStarted.Eagerly, replay = 1) /** - * Whether we're currently in a transition to a new [KeyguardState] and haven't yet completed - * it. + * The [KeyguardState] we're currently in. + * + * If we're not in transition, this is simply the [finishedKeyguardState]. If we're in + * transition, this is the state we're transitioning *from*. + * + * Absent CANCELED transitions, [currentKeyguardState] and [finishedKeyguardState] are always + * identical - if a transition FINISHES in a given state, the subsequent state we START a + * transition *from* would always be that same previously FINISHED state. + * + * However, if a transition is CANCELED, the next transition will START from a state we never + * FINISHED in. For example, if we transition from GONE -> DOZING, but CANCEL that transition in + * favor of DOZING -> LOCKSCREEN, we've STARTED a transition *from* DOZING despite never + * FINISHING in DOZING. Thus, the current state will be DOZING but the FINISHED state will still + * be GONE. + * + * In this example, if there was DOZING-related state that needs to be set up in order to + * properly render a DOZING -> LOCKSCREEN transition, it would never be set up if we were + * listening for [finishedKeyguardState] to emit DOZING. However, [currentKeyguardState] would + * emit DOZING immediately upon STARTING DOZING -> LOCKSCREEN, allowing us to set up the state. + * + * Whether you want to use [currentKeyguardState] or [finishedKeyguardState] depends on your + * specific use case and how you want to handle cancellations. In general, if you're dealing + * with state/UI present across multiple [KeyguardState]s, you probably want + * [currentKeyguardState]. If you're dealing with state/UI encapsulated within a single state, + * you likely want [finishedKeyguardState]. + * + * As an example, let's say you want to animate in a message on the lockscreen UI after waking + * up, and that TextView is not involved in animations between states. You'd want to collect + * [finishedKeyguardState], so you'll only animate it in once we're settled on the lockscreen. + * If you use [currentKeyguardState] in this case, a DOZING -> LOCKSCREEN transition that is + * interrupted by a LOCKSCREEN -> GONE transition would cause the message to become visible + * immediately upon LOCKSCREEN -> GONE STARTING, as the current state would become LOCKSCREEN in + * that case. That's likely not what you want. + * + * On the other hand, let's say you're animating the smartspace from alpha 0f to 1f during + * DOZING -> LOCKSCREEN, but the transition is interrupted by LOCKSCREEN -> GONE. LS -> GONE + * needs the smartspace to be alpha=1f so that it can play the shared-element unlock animation. + * In this case, we'd want to collect [currentKeyguardState] and ensure the smartspace is + * visible when the current state is LOCKSCREEN. If you use [finishedKeyguardState] in this + * case, the smartspace will never be set to alpha = 1f and you'll have a half-faded smartspace + * during the LS -> GONE transition. + * + * If you need special-case handling for cancellations (such as conditional handling depending + * on which [KeyguardState] was canceled) you can collect [canceledKeyguardTransitionStep] + * directly. + * + * As a helpful footnote, here's the values of [finishedKeyguardState] and + * [currentKeyguardState] during a sequence with two cancellations: + * 1. We're FINISHED in GONE. currentKeyguardState=GONE; finishedKeyguardState=GONE. + * 2. We START a transition from GONE -> DOZING. currentKeyguardState=GONE; + * finishedKeyguardState=GONE. + * 3. We CANCEL this transition and START a transition from DOZING -> LOCKSCREEN. + * currentKeyguardState=DOZING; finishedKeyguardState=GONE. + * 4. We subsequently also CANCEL DOZING -> LOCKSCREEN and START LOCKSCREEN -> GONE. + * currentKeyguardState=LOCKSCREEN finishedKeyguardState=GONE. + * 5. LOCKSCREEN -> GONE is allowed to FINISH. currentKeyguardState=GONE; + * finishedKeyguardState=GONE. */ - val isInTransitionToAnyState = - combine( - startedKeyguardTransitionStep, - finishedKeyguardState, - ) { startedStep, finishedState -> - startedStep.to != finishedState - } + val currentKeyguardState: SharedFlow<KeyguardState> = + repository.transitions + .mapLatest { + if (it.transitionState == TransitionState.FINISHED) { + it.to + } else { + it.from + } + } + .distinctUntilChanged() + .shareIn(scope, SharingStarted.Eagerly, replay = 1) + + /** Whether we've currently STARTED a transition and haven't yet FINISHED it. */ + val isInTransitionToAnyState = isInTransitionWhere({ true }, { true }) /** * The amount of transition into or out of the given [KeyguardState]. @@ -304,13 +398,12 @@ constructor( fromStatePredicate: (KeyguardState) -> Boolean, toStatePredicate: (KeyguardState) -> Boolean, ): Flow<Boolean> { - return combine( - startedKeyguardTransitionStep, - finishedKeyguardState, - ) { startedStep, finishedState -> - fromStatePredicate(startedStep.from) && - toStatePredicate(startedStep.to) && - finishedState != startedStep.to + return repository.transitions + .filter { it.transitionState != TransitionState.CANCELED } + .mapLatest { + it.transitionState != TransitionState.FINISHED && + fromStatePredicate(it.from) && + toStatePredicate(it.to) } .distinctUntilChanged() } 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 bf763b4e1f99..400b8bfff9b0 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 @@ -30,7 +30,10 @@ import androidx.constraintlayout.widget.ConstraintSet import androidx.lifecycle.Lifecycle import androidx.lifecycle.repeatOnLifecycle import com.android.app.animation.Interpolators +import com.android.keyguard.KeyguardClockSwitch.LARGE +import com.android.keyguard.KeyguardClockSwitch.SMALL import com.android.systemui.Flags.migrateClocksToBlueprint +import com.android.systemui.keyguard.domain.interactor.KeyguardBlueprintInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor import com.android.systemui.keyguard.ui.view.layout.sections.ClockSection import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel @@ -48,6 +51,7 @@ object KeyguardClockViewBinder { keyguardRootView: ConstraintLayout, viewModel: KeyguardClockViewModel, keyguardClockInteractor: KeyguardClockInteractor, + blueprintInteractor: KeyguardBlueprintInteractor, ) { keyguardRootView.repeatWhenAttached { repeatOnLifecycle(Lifecycle.State.CREATED) { @@ -61,18 +65,16 @@ object KeyguardClockViewBinder { viewModel.currentClock.collect { currentClock -> cleanupClockViews(viewModel.clock, keyguardRootView, viewModel.burnInLayer) viewModel.clock = currentClock - addClockViews(currentClock, keyguardRootView, viewModel.burnInLayer) - viewModel.burnInLayer?.updatePostLayout(keyguardRootView) - applyConstraints(clockSection, keyguardRootView, true) + addClockViews(currentClock, keyguardRootView) + updateBurnInLayer(keyguardRootView, viewModel) + blueprintInteractor.refreshBlueprint() } } - // TODO: Weather clock dozing animation - // will trigger both shouldBeCentered and clockSize change - // we should avoid this launch { if (!migrateClocksToBlueprint()) return@launch viewModel.clockSize.collect { - applyConstraints(clockSection, keyguardRootView, true) + updateBurnInLayer(keyguardRootView, viewModel) + blueprintInteractor.refreshBlueprint() } } launch { @@ -82,7 +84,7 @@ object KeyguardClockViewBinder { if (it.largeClock.config.hasCustomPositionUpdatedAnimation) { playClockCenteringAnimation(clockSection, keyguardRootView, it) } else { - applyConstraints(clockSection, keyguardRootView, true) + blueprintInteractor.refreshBlueprint() } } } @@ -90,6 +92,29 @@ object KeyguardClockViewBinder { } } } + @VisibleForTesting + fun updateBurnInLayer( + keyguardRootView: ConstraintLayout, + viewModel: KeyguardClockViewModel, + ) { + val burnInLayer = viewModel.burnInLayer + val clockController = viewModel.currentClock.value + clockController?.let { clock -> + when (viewModel.clockSize.value) { + LARGE -> { + clock.smallClock.layout.views.forEach { burnInLayer?.removeView(it) } + if (clock.config.useAlternateSmartspaceAODTransition) { + clock.largeClock.layout.views.forEach { burnInLayer?.addView(it) } + } + } + SMALL -> { + clock.smallClock.layout.views.forEach { burnInLayer?.addView(it) } + clock.largeClock.layout.views.forEach { burnInLayer?.removeView(it) } + } + } + } + viewModel.burnInLayer?.updatePostLayout(keyguardRootView) + } private fun cleanupClockViews( clockController: ClockController?, @@ -116,7 +141,6 @@ object KeyguardClockViewBinder { fun addClockViews( clockController: ClockController?, rootView: ConstraintLayout, - burnInLayer: Layer? ) { clockController?.let { clock -> clock.smallClock.layout.views[0].id = R.id.lockscreen_clock_view @@ -125,17 +149,10 @@ object KeyguardClockViewBinder { } // small clock should either be a single view or container with id // `lockscreen_clock_view` - clock.smallClock.layout.views.forEach { - rootView.addView(it) - burnInLayer?.addView(it) - } + clock.smallClock.layout.views.forEach { rootView.addView(it) } clock.largeClock.layout.views.forEach { rootView.addView(it) } - if (clock.config.useAlternateSmartspaceAODTransition) { - clock.largeClock.layout.views.forEach { burnInLayer?.addView(it) } - } } } - fun applyConstraints( clockSection: ClockSection, rootView: ConstraintLayout, diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSmartspaceViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSmartspaceViewBinder.kt index 81ce8f04d302..10392e3c1450 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSmartspaceViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSmartspaceViewBinder.kt @@ -16,15 +16,13 @@ package com.android.systemui.keyguard.ui.binder -import android.transition.TransitionManager import android.view.View import androidx.constraintlayout.helper.widget.Layer import androidx.constraintlayout.widget.ConstraintLayout -import androidx.constraintlayout.widget.ConstraintSet import androidx.lifecycle.Lifecycle import androidx.lifecycle.repeatOnLifecycle import com.android.systemui.Flags.migrateClocksToBlueprint -import com.android.systemui.keyguard.ui.view.layout.sections.SmartspaceSection +import com.android.systemui.keyguard.domain.interactor.KeyguardBlueprintInteractor import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel import com.android.systemui.keyguard.ui.viewmodel.KeyguardSmartspaceViewModel import com.android.systemui.lifecycle.repeatWhenAttached @@ -35,10 +33,10 @@ import kotlinx.coroutines.launch object KeyguardSmartspaceViewBinder { @JvmStatic fun bind( - smartspaceSection: SmartspaceSection, keyguardRootView: ConstraintLayout, clockViewModel: KeyguardClockViewModel, smartspaceViewModel: KeyguardSmartspaceViewModel, + blueprintInteractor: KeyguardBlueprintInteractor, ) { keyguardRootView.repeatWhenAttached { repeatOnLifecycle(Lifecycle.State.STARTED) { @@ -46,22 +44,56 @@ object KeyguardSmartspaceViewBinder { if (!migrateClocksToBlueprint()) return@launch clockViewModel.hasCustomWeatherDataDisplay.collect { hasCustomWeatherDataDisplay -> - if (hasCustomWeatherDataDisplay) { - removeDateWeatherToBurnInLayer(keyguardRootView, smartspaceViewModel) - } else { - addDateWeatherToBurnInLayer(keyguardRootView, smartspaceViewModel) - } - clockViewModel.burnInLayer?.updatePostLayout(keyguardRootView) - val constraintSet = ConstraintSet().apply { clone(keyguardRootView) } - smartspaceSection.applyConstraints(constraintSet) - TransitionManager.beginDelayedTransition(keyguardRootView) - constraintSet.applyTo(keyguardRootView) + updateDateWeatherToBurnInLayer( + keyguardRootView, + clockViewModel, + smartspaceViewModel + ) + blueprintInteractor.refreshBlueprint() + } + } + + launch { + smartspaceViewModel.bcSmartspaceVisibility.collect { + updateBCSmartspaceInBurnInLayer(keyguardRootView, clockViewModel) + blueprintInteractor.refreshBlueprint() } } } } } + private fun updateBCSmartspaceInBurnInLayer( + keyguardRootView: ConstraintLayout, + clockViewModel: KeyguardClockViewModel, + ) { + // Visibility is controlled by updateTargetVisibility in CardPagerAdapter + val burnInLayer = keyguardRootView.requireViewById<Layer>(R.id.burn_in_layer) + burnInLayer.apply { + val smartspaceView = + keyguardRootView.requireViewById<View>(sharedR.id.bc_smartspace_view) + if (smartspaceView.visibility == View.VISIBLE) { + addView(smartspaceView) + } else { + removeView(smartspaceView) + } + } + clockViewModel.burnInLayer?.updatePostLayout(keyguardRootView) + } + + private fun updateDateWeatherToBurnInLayer( + keyguardRootView: ConstraintLayout, + clockViewModel: KeyguardClockViewModel, + smartspaceViewModel: KeyguardSmartspaceViewModel + ) { + if (clockViewModel.hasCustomWeatherDataDisplay.value) { + removeDateWeatherFromBurnInLayer(keyguardRootView, smartspaceViewModel) + } else { + addDateWeatherToBurnInLayer(keyguardRootView, smartspaceViewModel) + } + clockViewModel.burnInLayer?.updatePostLayout(keyguardRootView) + } + private fun addDateWeatherToBurnInLayer( constraintLayout: ConstraintLayout, smartspaceViewModel: KeyguardSmartspaceViewModel @@ -76,13 +108,13 @@ object KeyguardSmartspaceViewBinder { constraintLayout.requireViewById<View>(sharedR.id.date_smartspace_view) val weatherView = constraintLayout.requireViewById<View>(sharedR.id.weather_smartspace_view) - addView(weatherView) addView(dateView) + addView(weatherView) } } } - private fun removeDateWeatherToBurnInLayer( + private fun removeDateWeatherFromBurnInLayer( constraintLayout: ConstraintLayout, smartspaceViewModel: KeyguardSmartspaceViewModel ) { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/SplitShadeKeyguardBlueprint.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/SplitShadeKeyguardBlueprint.kt index 24d06026dcf7..8472a9f6da6d 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/SplitShadeKeyguardBlueprint.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/SplitShadeKeyguardBlueprint.kt @@ -23,6 +23,7 @@ import com.android.systemui.keyguard.shared.model.KeyguardBlueprint import com.android.systemui.keyguard.shared.model.KeyguardSection import com.android.systemui.keyguard.ui.view.layout.sections.AodBurnInSection import com.android.systemui.keyguard.ui.view.layout.sections.AodNotificationIconsSection +import com.android.systemui.keyguard.ui.view.layout.sections.ClockSection import com.android.systemui.keyguard.ui.view.layout.sections.DefaultDeviceEntrySection import com.android.systemui.keyguard.ui.view.layout.sections.DefaultIndicationAreaSection import com.android.systemui.keyguard.ui.view.layout.sections.DefaultSettingsPopupMenuSection @@ -30,11 +31,10 @@ import com.android.systemui.keyguard.ui.view.layout.sections.DefaultShortcutsSec import com.android.systemui.keyguard.ui.view.layout.sections.DefaultStatusBarSection import com.android.systemui.keyguard.ui.view.layout.sections.DefaultStatusViewSection import com.android.systemui.keyguard.ui.view.layout.sections.KeyguardSectionsModule -import com.android.systemui.keyguard.ui.view.layout.sections.SplitShadeClockSection +import com.android.systemui.keyguard.ui.view.layout.sections.SmartspaceSection import com.android.systemui.keyguard.ui.view.layout.sections.SplitShadeGuidelines import com.android.systemui.keyguard.ui.view.layout.sections.SplitShadeMediaSection import com.android.systemui.keyguard.ui.view.layout.sections.SplitShadeNotificationStackScrollLayoutSection -import com.android.systemui.keyguard.ui.view.layout.sections.SplitShadeSmartspaceSection import com.android.systemui.util.kotlin.getOrNull import java.util.Optional import javax.inject.Inject @@ -62,8 +62,8 @@ constructor( aodNotificationIconsSection: AodNotificationIconsSection, aodBurnInSection: AodBurnInSection, communalTutorialIndicatorSection: CommunalTutorialIndicatorSection, - smartspaceSection: SplitShadeSmartspaceSection, - clockSection: SplitShadeClockSection, + clockSection: ClockSection, + smartspaceSection: SmartspaceSection, mediaSection: SplitShadeMediaSection, ) : KeyguardBlueprint { override val id: String = ID diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/transitions/BaseBlueprintTransition.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/transitions/BaseBlueprintTransition.kt index d0626d58a4ad..fd530b77707a 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/transitions/BaseBlueprintTransition.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/transitions/BaseBlueprintTransition.kt @@ -25,6 +25,8 @@ import android.transition.Visibility import android.view.View import android.view.ViewGroup import androidx.constraintlayout.helper.widget.Layer +import com.android.systemui.plugins.BcSmartspaceDataPlugin.SmartspaceView +import com.android.systemui.res.R class BaseBlueprintTransition : TransitionSet() { init { @@ -33,7 +35,16 @@ class BaseBlueprintTransition : TransitionSet() { .addTransition(ChangeBounds()) .addTransition(AlphaInVisibility()) excludeTarget(Layer::class.java, /* exclude= */ true) + excludeClockAndSmartspaceViews() } + + private fun excludeClockAndSmartspaceViews() { + excludeTarget(R.id.lockscreen_clock_view, true) + excludeTarget(R.id.lockscreen_clock_view_large, true) + excludeTarget(SmartspaceView::class.java, true) + // TODO(b/319468190): need to exclude views from large weather clock + } + class AlphaOutVisibility : Visibility() { override fun onDisappear( sceneRoot: ViewGroup?, diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodBurnInLayer.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodBurnInLayer.kt new file mode 100644 index 000000000000..67a20e588198 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodBurnInLayer.kt @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyguard.ui.view.layout.sections + +import android.content.Context +import android.view.View +import androidx.constraintlayout.helper.widget.Layer + +class AodBurnInLayer(context: Context) : Layer(context) { + // For setScale in Layer class, it stores it in mScaleX/Y and directly apply scale to + // referenceViews instead of keeping the value in fields of View class + // when we try to clone ConstraintSet, it will call getScaleX from View class and return 1.0 + // and when we clone and apply, it will reset everything in the layer + // which cause the flicker from AOD to LS + private var _scaleX = 1F + private var _scaleY = 1F + // As described for _scaleX and _scaleY, we have similar issue with translation + private var _translationX = 1F + private var _translationY = 1F + // avoid adding views with same ids + override fun addView(view: View?) { + view?.let { if (it.id !in referencedIds) super.addView(view) } + } + override fun setScaleX(scaleX: Float) { + _scaleX = scaleX + super.setScaleX(scaleX) + } + + override fun getScaleX(): Float { + return _scaleX + } + + override fun setScaleY(scaleY: Float) { + _scaleY = scaleY + super.setScaleY(scaleY) + } + + override fun getScaleY(): Float { + return _scaleY + } + + override fun setTranslationX(dx: Float) { + _translationX = dx + super.setTranslationX(dx) + } + + override fun getTranslationX(): Float { + return _translationX + } + + override fun setTranslationY(dy: Float) { + _translationY = dy + super.setTranslationY(dy) + } + + override fun getTranslationY(): Float { + return _translationY + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodBurnInSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodBurnInSection.kt index 1ccc6ccf2cec..3d36eb03a1bc 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodBurnInSection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodBurnInSection.kt @@ -19,16 +19,13 @@ package com.android.systemui.keyguard.ui.view.layout.sections import android.content.Context import android.view.View -import androidx.constraintlayout.helper.widget.Layer import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintSet import com.android.systemui.Flags.migrateClocksToBlueprint import com.android.systemui.keyguard.shared.KeyguardShadeMigrationNssl import com.android.systemui.keyguard.shared.model.KeyguardSection 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.shared.R as sharedR import javax.inject.Inject /** Adds a layer to group elements for translation for burn-in preventation */ @@ -37,10 +34,8 @@ class AodBurnInSection constructor( private val context: Context, private val clockViewModel: KeyguardClockViewModel, - private val smartspaceViewModel: KeyguardSmartspaceViewModel, ) : KeyguardSection() { - lateinit var burnInLayer: Layer - + private lateinit var burnInLayer: AodBurnInLayer override fun addViews(constraintLayout: ConstraintLayout) { if (!KeyguardShadeMigrationNssl.isEnabled) { return @@ -48,7 +43,7 @@ constructor( val nic = constraintLayout.requireViewById<View>(R.id.aod_notification_icon_container) burnInLayer = - Layer(context).apply { + AodBurnInLayer(context).apply { id = R.id.burn_in_layer addView(nic) if (!migrateClocksToBlueprint()) { @@ -57,11 +52,6 @@ constructor( addView(statusView) } } - if (migrateClocksToBlueprint()) { - // weather and date parts won't be added here, cause their visibility doesn't align - // with others in burnInLayer - addSmartspaceViews(constraintLayout) - } constraintLayout.addView(burnInLayer) } @@ -83,14 +73,4 @@ constructor( override fun removeViews(constraintLayout: ConstraintLayout) { constraintLayout.removeView(R.id.burn_in_layer) } - - private fun addSmartspaceViews(constraintLayout: ConstraintLayout) { - burnInLayer.apply { - if (smartspaceViewModel.isSmartspaceEnabled) { - val smartspaceView = - constraintLayout.requireViewById<View>(sharedR.id.bc_smartspace_view) - addView(smartspaceView) - } - } - } } 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 f560b5f068c9..ed7abff555e7 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 @@ -30,9 +30,7 @@ import com.android.systemui.Flags.migrateClocksToBlueprint import com.android.systemui.common.ui.ConfigurationState import com.android.systemui.keyguard.shared.KeyguardShadeMigrationNssl import com.android.systemui.keyguard.shared.model.KeyguardSection -import com.android.systemui.keyguard.ui.viewmodel.KeyguardSmartspaceViewModel import com.android.systemui.res.R -import com.android.systemui.shared.R as sharedR import com.android.systemui.statusbar.notification.icon.ui.viewbinder.AlwaysOnDisplayNotificationIconViewStore import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerViewBinder import com.android.systemui.statusbar.notification.icon.ui.viewbinder.StatusBarIconViewBindingFailureTracker @@ -53,13 +51,13 @@ constructor( private val nicAodViewModel: NotificationIconContainerAlwaysOnDisplayViewModel, private val nicAodIconViewStore: AlwaysOnDisplayNotificationIconViewStore, private val notificationIconAreaController: NotificationIconAreaController, - private val smartspaceViewModel: KeyguardSmartspaceViewModel, private val systemBarUtilsState: SystemBarUtilsState, ) : KeyguardSection() { 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) { @@ -118,7 +116,7 @@ constructor( } constraintSet.apply { if (migrateClocksToBlueprint()) { - connect(nicId, TOP, sharedR.id.bc_smartspace_view, BOTTOM, bottomMargin) + connect(nicId, TOP, R.id.smart_space_barrier_bottom, BOTTOM, bottomMargin) setGoneMargin(nicId, BOTTOM, bottomMargin) } else { connect(nicId, TOP, R.id.keyguard_status_view, topAlignment, bottomMargin) 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 b5f32c8a5608..b344d3b9afea 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 @@ -23,11 +23,14 @@ import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintSet import androidx.constraintlayout.widget.ConstraintSet.BOTTOM import androidx.constraintlayout.widget.ConstraintSet.END +import androidx.constraintlayout.widget.ConstraintSet.INVISIBLE import androidx.constraintlayout.widget.ConstraintSet.PARENT_ID import androidx.constraintlayout.widget.ConstraintSet.START import androidx.constraintlayout.widget.ConstraintSet.TOP +import androidx.constraintlayout.widget.ConstraintSet.VISIBLE import androidx.constraintlayout.widget.ConstraintSet.WRAP_CONTENT import com.android.systemui.Flags +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 @@ -35,8 +38,10 @@ import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel import com.android.systemui.plugins.clocks.ClockController import com.android.systemui.plugins.clocks.ClockFaceLayout import com.android.systemui.res.R +import com.android.systemui.shared.R as sharedR import com.android.systemui.statusbar.policy.SplitShadeStateController import com.android.systemui.util.Utils +import dagger.Lazy import javax.inject.Inject internal fun ConstraintSet.setVisibility( @@ -56,6 +61,7 @@ constructor( protected val keyguardClockViewModel: KeyguardClockViewModel, private val context: Context, private val splitShadeStateController: SplitShadeStateController, + val blueprintInteractor: Lazy<KeyguardBlueprintInteractor>, ) : KeyguardSection() { override fun addViews(constraintLayout: ConstraintLayout) {} @@ -68,6 +74,7 @@ constructor( constraintLayout, keyguardClockViewModel, clockInteractor, + blueprintInteractor.get() ) } @@ -88,12 +95,16 @@ constructor( ): ConstraintSet { // Add constraint between rootView and clockContainer applyDefaultConstraints(constraintSet) + getNonTargetClockFace(clock).applyConstraints(constraintSet) getTargetClockFace(clock).applyConstraints(constraintSet) // Add constraint between elements in clock and clock container return constraintSet.apply { - setAlpha(getTargetClockFace(clock).views, 1F) - setAlpha(getNonTargetClockFace(clock).views, 0F) + setVisibility(getTargetClockFace(clock).views, VISIBLE) + setVisibility(getNonTargetClockFace(clock).views, INVISIBLE) + if (!keyguardClockViewModel.useLargeClock) { + connect(sharedR.id.bc_smartspace_view, TOP, sharedR.id.date_smartspace_view, BOTTOM) + } } } @@ -107,9 +118,12 @@ constructor( private fun getLargeClockFace(clock: ClockController): ClockFaceLayout = clock.largeClock.layout private fun getSmallClockFace(clock: ClockController): ClockFaceLayout = clock.smallClock.layout open fun applyDefaultConstraints(constraints: ConstraintSet) { + val guideline = + if (keyguardClockViewModel.clockShouldBeCentered.value) PARENT_ID + else R.id.split_shade_guideline constraints.apply { connect(R.id.lockscreen_clock_view_large, START, PARENT_ID, START) - connect(R.id.lockscreen_clock_view_large, END, PARENT_ID, END) + connect(R.id.lockscreen_clock_view_large, END, guideline, END) connect(R.id.lockscreen_clock_view_large, BOTTOM, R.id.lock_icon_view, TOP) var largeClockTopMargin = context.resources.getDimensionPixelSize(R.dimen.status_bar_height) + 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 b0eee0a68b1f..8c5e9b4c6817 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,6 +18,7 @@ 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 @@ -27,11 +28,9 @@ import androidx.constraintlayout.widget.ConstraintSet.TOP import com.android.systemui.Flags.migrateClocksToBlueprint import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.keyguard.shared.KeyguardShadeMigrationNssl -import com.android.systemui.keyguard.ui.viewmodel.KeyguardSmartspaceViewModel import com.android.systemui.res.R import com.android.systemui.scene.shared.flag.SceneContainerFlags import com.android.systemui.shade.NotificationPanelView -import com.android.systemui.shared.R as sharedR import com.android.systemui.statusbar.notification.stack.AmbientState import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController import com.android.systemui.statusbar.notification.stack.NotificationStackSizeCalculator @@ -54,7 +53,6 @@ constructor( ambientState: AmbientState, controller: NotificationStackScrollLayoutController, notificationStackSizeCalculator: NotificationStackSizeCalculator, - private val smartspaceViewModel: KeyguardSmartspaceViewModel, @Main mainDispatcher: CoroutineDispatcher, ) : NotificationStackScrollLayoutSection( @@ -69,6 +67,7 @@ constructor( notificationStackSizeCalculator, mainDispatcher, ) { + private val smartSpaceBarrier = View.generateViewId() override fun applyConstraints(constraintSet: ConstraintSet) { if (!KeyguardShadeMigrationNssl.isEnabled) { return @@ -76,16 +75,14 @@ constructor( constraintSet.apply { val bottomMargin = context.resources.getDimensionPixelSize(R.dimen.keyguard_status_view_bottom_margin) - if (migrateClocksToBlueprint()) { connect( R.id.nssl_placeholder, TOP, - sharedR.id.bc_smartspace_view, + R.id.smart_space_barrier_bottom, BOTTOM, bottomMargin ) - setGoneMargin(R.id.nssl_placeholder, TOP, bottomMargin) } else { connect(R.id.nssl_placeholder, TOP, R.id.keyguard_status_view, BOTTOM, bottomMargin) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultShortcutsSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultShortcutsSection.kt index 2a68f26d3ae7..0c0eb8a673a4 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultShortcutsSection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultShortcutsSection.kt @@ -24,6 +24,7 @@ import androidx.constraintlayout.widget.ConstraintSet.BOTTOM import androidx.constraintlayout.widget.ConstraintSet.LEFT import androidx.constraintlayout.widget.ConstraintSet.PARENT_ID import androidx.constraintlayout.widget.ConstraintSet.RIGHT +import androidx.constraintlayout.widget.ConstraintSet.VISIBILITY_MODE_IGNORE import com.android.systemui.Flags.keyguardBottomAreaRefactor import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.keyguard.ui.binder.KeyguardQuickAffordanceViewBinder @@ -96,6 +97,11 @@ constructor( constrainHeight(R.id.end_button, height) connect(R.id.end_button, RIGHT, PARENT_ID, RIGHT, horizontalOffsetMargin) connect(R.id.end_button, BOTTOM, PARENT_ID, BOTTOM, verticalOffsetMargin) + + // The constraint set visibility for start and end button are default visible, set to + // ignore so the view's own initial visibility (invisible) is used + setVisibilityMode(R.id.start_button, VISIBILITY_MODE_IGNORE) + setVisibilityMode(R.id.end_button, VISIBILITY_MODE_IGNORE) } } } 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 eacd466bc473..37842a84c3d3 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 @@ -18,37 +18,41 @@ package com.android.systemui.keyguard.ui.view.layout.sections import android.content.Context import android.view.View +import android.view.View.GONE +import android.view.ViewTreeObserver.OnGlobalLayoutListener +import androidx.constraintlayout.widget.Barrier import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintSet -import androidx.constraintlayout.widget.ConstraintSet.BOTTOM -import androidx.constraintlayout.widget.ConstraintSet.END -import androidx.constraintlayout.widget.ConstraintSet.PARENT_ID -import androidx.constraintlayout.widget.ConstraintSet.START -import androidx.constraintlayout.widget.ConstraintSet.TOP -import androidx.constraintlayout.widget.ConstraintSet.WRAP_CONTENT import com.android.systemui.Flags.migrateClocksToBlueprint import com.android.systemui.keyguard.KeyguardUnlockAnimationController +import com.android.systemui.keyguard.domain.interactor.KeyguardBlueprintInteractor +import com.android.systemui.keyguard.domain.interactor.KeyguardSmartspaceInteractor import com.android.systemui.keyguard.shared.model.KeyguardSection 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.res.R +import com.android.systemui.shared.R import com.android.systemui.statusbar.lockscreen.LockscreenSmartspaceController +import dagger.Lazy import javax.inject.Inject open class SmartspaceSection @Inject constructor( + val context: Context, val keyguardClockViewModel: KeyguardClockViewModel, val keyguardSmartspaceViewModel: KeyguardSmartspaceViewModel, - private val context: Context, + val keyguardSmartspaceInteractor: KeyguardSmartspaceInteractor, val smartspaceController: LockscreenSmartspaceController, val keyguardUnlockAnimationController: KeyguardUnlockAnimationController, + val blueprintInteractor: Lazy<KeyguardBlueprintInteractor>, ) : KeyguardSection() { private var smartspaceView: View? = null private var weatherView: View? = null private var dateView: View? = null + private var smartspaceVisibilityListener: OnGlobalLayoutListener? = null + override fun addViews(constraintLayout: ConstraintLayout) { if (!migrateClocksToBlueprint()) { return @@ -64,6 +68,20 @@ constructor( } } keyguardUnlockAnimationController.lockscreenSmartspace = smartspaceView + smartspaceVisibilityListener = + object : OnGlobalLayoutListener { + var pastVisibility = GONE + override fun onGlobalLayout() { + smartspaceView?.let { + val newVisibility = it.visibility + if (pastVisibility != newVisibility) { + keyguardSmartspaceInteractor.setBcSmartspaceVisibility(newVisibility) + pastVisibility = newVisibility + } + } + } + } + smartspaceView?.viewTreeObserver?.addOnGlobalLayoutListener(smartspaceVisibilityListener) } override fun bindData(constraintLayout: ConstraintLayout) { @@ -71,10 +89,10 @@ constructor( return } KeyguardSmartspaceViewBinder.bind( - this, constraintLayout, keyguardClockViewModel, keyguardSmartspaceViewModel, + blueprintInteractor.get(), ) } @@ -82,65 +100,96 @@ constructor( if (!migrateClocksToBlueprint()) { return } - // Generally, weather should be next to dateView - // smartspace should be below date & weather views constraintSet.apply { // migrate addDateWeatherView, addWeatherView from KeyguardClockSwitchController - dateView?.let { dateView -> - constrainHeight(dateView.id, WRAP_CONTENT) - constrainWidth(dateView.id, WRAP_CONTENT) - connect( - dateView.id, - START, - PARENT_ID, - START, - context.resources.getDimensionPixelSize(R.dimen.below_clock_padding_start) + constrainHeight(R.id.date_smartspace_view, ConstraintSet.WRAP_CONTENT) + constrainWidth(R.id.date_smartspace_view, ConstraintSet.WRAP_CONTENT) + connect( + R.id.date_smartspace_view, + ConstraintSet.START, + ConstraintSet.PARENT_ID, + ConstraintSet.START, + context.resources.getDimensionPixelSize( + com.android.systemui.res.R.dimen.below_clock_padding_start ) - } - weatherView?.let { - constrainWidth(it.id, WRAP_CONTENT) - dateView?.let { dateView -> - connect(it.id, TOP, dateView.id, TOP) - connect(it.id, BOTTOM, dateView.id, BOTTOM) - connect(it.id, START, dateView.id, END, 4) - } - } + ) + constrainWidth(R.id.weather_smartspace_view, ConstraintSet.WRAP_CONTENT) + connect( + R.id.weather_smartspace_view, + ConstraintSet.TOP, + R.id.date_smartspace_view, + ConstraintSet.TOP + ) + connect( + R.id.weather_smartspace_view, + ConstraintSet.BOTTOM, + R.id.date_smartspace_view, + ConstraintSet.BOTTOM + ) + connect( + R.id.weather_smartspace_view, + ConstraintSet.START, + R.id.date_smartspace_view, + ConstraintSet.END, + 4 + ) + // migrate addSmartspaceView from KeyguardClockSwitchController - smartspaceView?.let { - constrainHeight(it.id, WRAP_CONTENT) - connect( - it.id, - START, - PARENT_ID, - START, - context.resources.getDimensionPixelSize(R.dimen.below_clock_padding_start) + constrainHeight(R.id.bc_smartspace_view, ConstraintSet.WRAP_CONTENT) + connect( + R.id.bc_smartspace_view, + ConstraintSet.START, + ConstraintSet.PARENT_ID, + ConstraintSet.START, + context.resources.getDimensionPixelSize( + com.android.systemui.res.R.dimen.below_clock_padding_start ) - connect( - it.id, - END, - PARENT_ID, - END, - context.resources.getDimensionPixelSize(R.dimen.below_clock_padding_end) + ) + connect( + R.id.bc_smartspace_view, + ConstraintSet.END, + if (keyguardClockViewModel.clockShouldBeCentered.value) ConstraintSet.PARENT_ID + else com.android.systemui.res.R.id.split_shade_guideline, + ConstraintSet.END, + context.resources.getDimensionPixelSize( + com.android.systemui.res.R.dimen.below_clock_padding_end ) - } + ) if (keyguardClockViewModel.hasCustomWeatherDataDisplay.value) { - dateView?.let { dateView -> - smartspaceView?.let { smartspaceView -> - connect(dateView.id, BOTTOM, smartspaceView.id, TOP) - } - } + clear(R.id.date_smartspace_view, ConstraintSet.TOP) + connect( + R.id.date_smartspace_view, + ConstraintSet.BOTTOM, + R.id.bc_smartspace_view, + ConstraintSet.TOP + ) } else { - dateView?.let { dateView -> - clear(dateView.id, BOTTOM) - connect(dateView.id, TOP, R.id.lockscreen_clock_view, BOTTOM) - constrainHeight(dateView.id, WRAP_CONTENT) - smartspaceView?.let { smartspaceView -> - clear(smartspaceView.id, TOP) - connect(smartspaceView.id, TOP, dateView.id, BOTTOM) - } - } + clear(R.id.date_smartspace_view, ConstraintSet.BOTTOM) + connect( + R.id.date_smartspace_view, + ConstraintSet.TOP, + com.android.systemui.res.R.id.lockscreen_clock_view, + ConstraintSet.BOTTOM + ) + connect( + R.id.bc_smartspace_view, + ConstraintSet.TOP, + R.id.date_smartspace_view, + ConstraintSet.BOTTOM + ) } + + createBarrier( + com.android.systemui.res.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, + ) + ) } updateVisibility(constraintSet) } @@ -156,30 +205,28 @@ constructor( } } } + smartspaceView?.viewTreeObserver?.removeOnGlobalLayoutListener(smartspaceVisibilityListener) + smartspaceVisibilityListener = null } private fun updateVisibility(constraintSet: ConstraintSet) { constraintSet.apply { - weatherView?.let { - setVisibility( - it.id, - when (keyguardClockViewModel.hasCustomWeatherDataDisplay.value) { - true -> ConstraintSet.GONE - false -> - when (keyguardSmartspaceViewModel.isWeatherEnabled) { - true -> ConstraintSet.VISIBLE - false -> ConstraintSet.GONE - } - } - ) - } - dateView?.let { - setVisibility( - it.id, - if (keyguardClockViewModel.hasCustomWeatherDataDisplay.value) ConstraintSet.GONE - else ConstraintSet.VISIBLE - ) - } + setVisibility( + R.id.weather_smartspace_view, + when (keyguardClockViewModel.hasCustomWeatherDataDisplay.value) { + true -> ConstraintSet.GONE + false -> + when (keyguardSmartspaceViewModel.isWeatherEnabled) { + true -> ConstraintSet.VISIBLE + false -> ConstraintSet.GONE + } + } + ) + setVisibility( + R.id.date_smartspace_view, + if (keyguardClockViewModel.hasCustomWeatherDataDisplay.value) ConstraintSet.GONE + else ConstraintSet.VISIBLE + ) } } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeClockSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeClockSection.kt deleted file mode 100644 index 19ba1aa4763a..000000000000 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeClockSection.kt +++ /dev/null @@ -1,49 +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.keyguard.ui.view.layout.sections - -import android.content.Context -import androidx.constraintlayout.widget.ConstraintSet -import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor -import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel -import com.android.systemui.res.R -import com.android.systemui.statusbar.policy.SplitShadeStateController -import javax.inject.Inject - -class SplitShadeClockSection -@Inject -constructor( - clockInteractor: KeyguardClockInteractor, - keyguardClockViewModel: KeyguardClockViewModel, - context: Context, - splitShadeStateController: SplitShadeStateController, -) : ClockSection(clockInteractor, keyguardClockViewModel, context, splitShadeStateController) { - override fun applyDefaultConstraints(constraints: ConstraintSet) { - super.applyDefaultConstraints(constraints) - val largeClockEndGuideline = - if (keyguardClockViewModel.clockShouldBeCentered.value) ConstraintSet.PARENT_ID - else R.id.split_shade_guideline - constraints.apply { - connect( - R.id.lockscreen_clock_view_large, - ConstraintSet.END, - largeClockEndGuideline, - ConstraintSet.END - ) - } - } -} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeMediaSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeMediaSection.kt index f20ab06bcda9..b12a8a811955 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeMediaSection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeMediaSection.kt @@ -20,7 +20,6 @@ import android.content.Context import android.view.View import android.view.ViewGroup.LayoutParams.WRAP_CONTENT import android.widget.FrameLayout -import androidx.constraintlayout.widget.Barrier import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintSet import androidx.constraintlayout.widget.ConstraintSet.BOTTOM @@ -31,11 +30,9 @@ import androidx.constraintlayout.widget.ConstraintSet.START import androidx.constraintlayout.widget.ConstraintSet.TOP import com.android.systemui.Flags.migrateClocksToBlueprint import com.android.systemui.keyguard.shared.model.KeyguardSection -import com.android.systemui.keyguard.ui.viewmodel.KeyguardSmartspaceViewModel import com.android.systemui.media.controls.ui.KeyguardMediaController import com.android.systemui.res.R import com.android.systemui.shade.NotificationPanelView -import com.android.systemui.shared.R as sharedR import javax.inject.Inject /** Aligns media on left side for split shade, below smartspace, date, and weather. */ @@ -44,11 +41,9 @@ class SplitShadeMediaSection constructor( private val context: Context, private val notificationPanelView: NotificationPanelView, - private val keyguardSmartspaceViewModel: KeyguardSmartspaceViewModel, private val keyguardMediaController: KeyguardMediaController ) : KeyguardSection() { private val mediaContainerId = R.id.status_view_media_container - private val smartSpaceBarrier = R.id.smart_space_barrier_bottom override fun addViews(constraintLayout: ConstraintLayout) { if (!migrateClocksToBlueprint()) { @@ -85,18 +80,7 @@ constructor( constraintSet.apply { constrainWidth(mediaContainerId, MATCH_CONSTRAINT) constrainHeight(mediaContainerId, WRAP_CONTENT) - - createBarrier( - smartSpaceBarrier, - Barrier.BOTTOM, - 0, - *intArrayOf( - sharedR.id.bc_smartspace_view, - sharedR.id.date_smartspace_view, - sharedR.id.weather_smartspace_view, - ) - ) - connect(mediaContainerId, TOP, smartSpaceBarrier, BOTTOM) + connect(mediaContainerId, TOP, R.id.smart_space_barrier_bottom, BOTTOM) connect(mediaContainerId, START, PARENT_ID, START) connect(mediaContainerId, END, R.id.split_shade_guideline, END) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeSmartspaceSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeSmartspaceSection.kt deleted file mode 100644 index 8728adadd8c3..000000000000 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeSmartspaceSection.kt +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.keyguard.ui.view.layout.sections - -import android.content.Context -import com.android.systemui.keyguard.KeyguardUnlockAnimationController -import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel -import com.android.systemui.keyguard.ui.viewmodel.KeyguardSmartspaceViewModel -import com.android.systemui.statusbar.lockscreen.LockscreenSmartspaceController -import javax.inject.Inject - -/* - * We need this class for the splitShadeBlueprint so `addViews` and `removeViews` will be called - * when switching to and from splitShade. - */ -class SplitShadeSmartspaceSection -@Inject -constructor( - keyguardClockViewModel: KeyguardClockViewModel, - keyguardSmartspaceViewModel: KeyguardSmartspaceViewModel, - context: Context, - smartspaceController: LockscreenSmartspaceController, - keyguardUnlockAnimationController: KeyguardUnlockAnimationController, -) : - SmartspaceSection( - keyguardClockViewModel, - keyguardSmartspaceViewModel, - context, - smartspaceController, - keyguardUnlockAnimationController, - ) 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 5bb27824753d..f37d9f801db3 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 @@ -99,34 +99,4 @@ constructor( context.resources.getDimensionPixelSize(R.dimen.keyguard_clock_top_margin) + Utils.getStatusBarHeaderHeightKeyguard(context) } - - fun getLargeClockTopMargin(context: Context): Int { - var largeClockTopMargin = - context.resources.getDimensionPixelSize(R.dimen.status_bar_height) + - context.resources.getDimensionPixelSize( - com.android.systemui.customization.R.dimen.small_clock_padding_top - ) + - context.resources.getDimensionPixelSize(R.dimen.keyguard_smartspace_top_offset) - largeClockTopMargin += getDimen(context, DATE_WEATHER_VIEW_HEIGHT) - largeClockTopMargin += getDimen(context, ENHANCED_SMARTSPACE_HEIGHT) - if (!useLargeClock) { - largeClockTopMargin -= - context.resources.getDimensionPixelSize( - com.android.systemui.customization.R.dimen.small_clock_height - ) - } - - return largeClockTopMargin - } - - private fun getDimen(context: Context, name: String): Int { - val res = context.packageManager.getResourcesForApplication(context.packageName) - val id = res.getIdentifier(name, "dimen", context.packageName) - return res.getDimensionPixelSize(id) - } - - companion object { - private const val DATE_WEATHER_VIEW_HEIGHT = "date_weather_view_height" - private const val ENHANCED_SMARTSPACE_HEIGHT = "enhanced_smartspace_height" - } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardSmartspaceViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardSmartspaceViewModel.kt index a1dd720a82f1..e8c1ab5feccc 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardSmartspaceViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardSmartspaceViewModel.kt @@ -18,6 +18,7 @@ package com.android.systemui.keyguard.ui.viewmodel import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.keyguard.domain.interactor.KeyguardSmartspaceInteractor import com.android.systemui.statusbar.lockscreen.LockscreenSmartspaceController import javax.inject.Inject import kotlinx.coroutines.CoroutineScope @@ -33,6 +34,7 @@ constructor( @Application applicationScope: CoroutineScope, smartspaceController: LockscreenSmartspaceController, keyguardClockViewModel: KeyguardClockViewModel, + smartspaceInteractor: KeyguardSmartspaceInteractor, ) { /** Whether the smartspace section is available in the build. */ val isSmartspaceEnabled: Boolean = smartspaceController.isEnabled() @@ -78,4 +80,7 @@ constructor( ): Boolean { return !clockIncludesCustomWeatherDisplay && isWeatherEnabled } + + /* trigger clock and smartspace constraints change when smartspace appears */ + var bcSmartspaceVisibility: StateFlow<Int> = smartspaceInteractor.bcSmartspaceVisibility } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModel.kt index 36bbe4e49415..d79288947e78 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModel.kt @@ -16,9 +16,13 @@ package com.android.systemui.keyguard.ui.viewmodel +import android.content.res.Resources +import com.android.keyguard.KeyguardClockSwitch import com.android.systemui.biometrics.AuthController import com.android.systemui.dagger.SysUISingleton import com.android.systemui.keyguard.domain.interactor.KeyguardBlueprintInteractor +import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor +import com.android.systemui.res.R import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.SharingStarted @@ -31,12 +35,28 @@ import kotlinx.coroutines.flow.stateIn class LockscreenContentViewModel @Inject constructor( + clockInteractor: KeyguardClockInteractor, private val interactor: KeyguardBlueprintInteractor, private val authController: AuthController, val longPress: KeyguardLongPressViewModel, ) { + private val clockSize = clockInteractor.clockSize + val isUdfpsVisible: Boolean get() = authController.isUdfpsSupported + val isLargeClockVisible: Boolean + get() = clockSize.value == KeyguardClockSwitch.LARGE + val areNotificationsVisible: Boolean + get() = !isLargeClockVisible + + fun getSmartSpacePaddingTop(resources: Resources): Int { + return if (isLargeClockVisible) { + resources.getDimensionPixelSize(R.dimen.keyguard_smartspace_top_offset) + + resources.getDimensionPixelSize(R.dimen.keyguard_clock_top_margin) + } else { + 0 + } + } fun blueprintId(scope: CoroutineScope): StateFlow<String> { return interactor.blueprint diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/KeyboardLog.kt b/packages/SystemUI/src/com/android/systemui/log/dagger/KeyboardLog.kt new file mode 100644 index 000000000000..5910701d9f2a --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/KeyboardLog.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.log.dagger + +import javax.inject.Qualifier + +/** A [com.android.systemui.log.LogBuffer] for keyboard-related functionality. */ +@Qualifier +@MustBeDocumented +@Retention(AnnotationRetention.RUNTIME) +annotation class KeyboardLog diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java index 24cb8fff9b67..3e0094081638 100644 --- a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java +++ b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java @@ -603,6 +603,14 @@ public class LogModule { return factory.create("BluetoothTileDialogLog", 50); } + /** Provides a {@link LogBuffer} for the keyboard functionalities. */ + @Provides + @SysUISingleton + @KeyboardLog + public static LogBuffer provideKeyboardLogBuffer(LogBufferFactory factory) { + return factory.create("KeyboardLog", 50); + } + /** Provides a {@link LogBuffer} for {@link PackageChangeRepository} */ @Provides @SysUISingleton 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 0d641ac9c688..58e042868607 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java @@ -624,7 +624,7 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack } if (!mIsEnabled) { - mGestureNavigationSettingsObserver.unregister(); + mBackgroundExecutor.execute(mGestureNavigationSettingsObserver::unregister); if (DEBUG_MISSING_GESTURE) { Log.d(DEBUG_MISSING_GESTURE_TAG, "Unregister display listener"); } @@ -642,7 +642,7 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack } } else { - mGestureNavigationSettingsObserver.register(); + mBackgroundExecutor.execute(mGestureNavigationSettingsObserver::register); updateDisplaySize(); if (DEBUG_MISSING_GESTURE) { Log.d(DEBUG_MISSING_GESTURE_TAG, "Register display listener"); diff --git a/packages/SystemUI/src/com/android/systemui/qs/LeftRightArrowPressedListener.kt b/packages/SystemUI/src/com/android/systemui/qs/LeftRightArrowPressedListener.kt new file mode 100644 index 000000000000..ca790e830f7f --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/LeftRightArrowPressedListener.kt @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.qs + +import android.view.KeyEvent +import android.view.View +import androidx.core.util.Consumer + +/** + * Listens for left and right arrow keys pressed while focus is on the view. + * + * Key press is treated as correct when its full lifecycle happened on the view: first + * [KeyEvent.ACTION_DOWN] was performed, view didn't lose focus in the meantime and then + * [KeyEvent.ACTION_UP] was performed with the same [KeyEvent.getKeyCode] + */ +class LeftRightArrowPressedListener private constructor() : + View.OnKeyListener, View.OnFocusChangeListener { + + private var lastKeyCode: Int? = 0 + private var listener: Consumer<Int>? = null + + fun setArrowKeyPressedListener(arrowPressedListener: Consumer<Int>) { + listener = arrowPressedListener + } + + override fun onKey(view: View, keyCode: Int, keyEvent: KeyEvent): Boolean { + if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) { + // only scroll on ACTION_UP as we don't handle longpressing for now. Still we need + // to intercept even ACTION_DOWN otherwise keyboard focus will be moved before we + // have a chance to intercept ACTION_UP. + if (keyEvent.action == KeyEvent.ACTION_UP && keyCode == lastKeyCode) { + listener?.accept(keyCode) + lastKeyCode = null + } else if (keyEvent.repeatCount == 0) { + // we only read key events that are NOT coming from long pressing because that also + // causes reading ACTION_DOWN event (with repeated count > 0) when moving focus with + // arrow from another sibling view + lastKeyCode = keyCode + } + return true + } + return false + } + + override fun onFocusChange(view: View, hasFocus: Boolean) { + // resetting lastKeyCode so we get fresh cleared state on focus + if (hasFocus) { + lastKeyCode = null + } + } + + companion object { + @JvmStatic + fun createAndRegisterListenerForView(view: View): LeftRightArrowPressedListener { + val listener = LeftRightArrowPressedListener() + view.setOnKeyListener(listener) + view.onFocusChangeListener = listener + return listener + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/PageIndicator.java b/packages/SystemUI/src/com/android/systemui/qs/PageIndicator.java index 4770d5272508..1c9f5fdde6f4 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/PageIndicator.java +++ b/packages/SystemUI/src/com/android/systemui/qs/PageIndicator.java @@ -1,5 +1,8 @@ package com.android.systemui.qs; +import static com.android.systemui.qs.PageIndicator.PageScrollActionListener.LEFT; +import static com.android.systemui.qs.PageIndicator.PageScrollActionListener.RIGHT; + import android.content.Context; import android.content.res.ColorStateList; import android.content.res.Resources; @@ -9,10 +12,12 @@ import android.graphics.drawable.AnimatedVectorDrawable; import android.graphics.drawable.Drawable; import android.util.AttributeSet; import android.util.Log; +import android.view.KeyEvent; import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; +import androidx.annotation.IntDef; import androidx.annotation.NonNull; import com.android.settingslib.Utils; @@ -43,6 +48,7 @@ public class PageIndicator extends ViewGroup { private int mPosition = -1; private boolean mAnimating; + private PageScrollActionListener mPageScrollActionListener; private final Animatable2.AnimationCallback mAnimationCallback = new Animatable2.AnimationCallback() { @@ -77,6 +83,14 @@ public class PageIndicator extends ViewGroup { mPageIndicatorWidth = res.getDimensionPixelSize(R.dimen.qs_page_indicator_width); mPageIndicatorHeight = res.getDimensionPixelSize(R.dimen.qs_page_indicator_height); mPageDotWidth = res.getDimensionPixelSize(R.dimen.qs_page_indicator_dot_width); + LeftRightArrowPressedListener arrowListener = + LeftRightArrowPressedListener.createAndRegisterListenerForView(this); + arrowListener.setArrowKeyPressedListener(keyCode -> { + if (mPageScrollActionListener != null) { + int swipeDirection = keyCode == KeyEvent.KEYCODE_DPAD_LEFT ? LEFT : RIGHT; + mPageScrollActionListener.onScrollActionTriggered(swipeDirection); + } + }); } public void setNumPages(int numPages) { @@ -280,4 +294,19 @@ public class PageIndicator extends ViewGroup { getChildAt(i).layout(left, 0, mPageIndicatorWidth + left, mPageIndicatorHeight); } } + + void setPageScrollActionListener(PageScrollActionListener listener) { + mPageScrollActionListener = listener; + } + + interface PageScrollActionListener { + + @IntDef({LEFT, RIGHT}) + @interface Direction { } + + int LEFT = 0; + int RIGHT = 1; + + void onScrollActionTriggered(@Direction int swipeDirection); + } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java b/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java index 052c0daf0b56..43f3a2242da4 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java +++ b/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java @@ -1,6 +1,8 @@ package com.android.systemui.qs; import static com.android.internal.jank.InteractionJankMonitor.CUJ_NOTIFICATION_SHADE_QS_SCROLL_SWIPE; +import static com.android.systemui.qs.PageIndicator.PageScrollActionListener.LEFT; +import static com.android.systemui.qs.PageIndicator.PageScrollActionListener.RIGHT; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; @@ -12,7 +14,6 @@ import android.content.Context; import android.content.res.Configuration; import android.os.Bundle; import android.util.AttributeSet; -import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -30,6 +31,7 @@ import androidx.viewpager.widget.ViewPager; import com.android.internal.jank.InteractionJankMonitor; import com.android.internal.logging.UiEventLogger; import com.android.systemui.plugins.qs.QSTile; +import com.android.systemui.qs.PageIndicator.PageScrollActionListener.Direction; import com.android.systemui.qs.QSPanel.QSTileLayout; import com.android.systemui.qs.QSPanelControllerBase.TileRecord; import com.android.systemui.qs.logging.QSLogger; @@ -310,26 +312,18 @@ public class PagedTileLayout extends ViewPager implements QSTileLayout { mPageIndicator = indicator; mPageIndicator.setNumPages(mPages.size()); mPageIndicator.setLocation(mPageIndicatorPosition); - mPageIndicator.setOnKeyListener((view, keyCode, keyEvent) -> { - if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) { - // only scroll on ACTION_UP as we don't handle longpressing for now. Still we need - // to intercept even ACTION_DOWN otherwise keyboard focus will be moved before we - // have a chance to intercept ACTION_UP. - if (keyEvent.getAction() == KeyEvent.ACTION_UP && mScroller.isFinished()) { - scrollByX(getDeltaXForKeyboardScrolling(keyCode), - SINGLE_PAGE_SCROLL_DURATION_MILLIS); - } - return true; + mPageIndicator.setPageScrollActionListener(swipeDirection -> { + if (mScroller.isFinished()) { + scrollByX(getDeltaXForPageScrolling(swipeDirection), + SINGLE_PAGE_SCROLL_DURATION_MILLIS); } - return false; }); } - private int getDeltaXForKeyboardScrolling(int keyCode) { - if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT && getCurrentItem() != 0) { + private int getDeltaXForPageScrolling(@Direction int swipeDirection) { + if (swipeDirection == LEFT && getCurrentItem() != 0) { return -getWidth(); - } else if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT - && getCurrentItem() != mPages.size() - 1) { + } else if (swipeDirection == RIGHT && getCurrentItem() != mPages.size() - 1) { return getWidth(); } return 0; diff --git a/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModel.kt index aff4a6759a47..2077d733172d 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModel.kt @@ -88,9 +88,9 @@ class FooterActionsViewModel( /** Called when the expansion of the Quick Settings changed. */ fun onQuickSettingsExpansionChanged(expansion: Float, isInSplitShade: Boolean) { if (isInSplitShade) { - // In split shade, we want to fade in the background only at the very end (see - // b/240563302). - val delay = 0.99f + // In split shade, we want to fade in the background when the QS background starts to + // show. + val delay = 0.15f _alpha.value = expansion _backgroundAlpha.value = max(0f, expansion - delay) / (1f - delay) } else { 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 6e7e09959697..bcd09bd877fd 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 @@ -18,23 +18,21 @@ package com.android.systemui.qs.pipeline.data.repository import android.Manifest.permission.BIND_QUICK_SETTINGS_TILE import android.annotation.WorkerThread -import android.content.BroadcastReceiver import android.content.ComponentName import android.content.Context import android.content.Intent -import android.content.IntentFilter import android.content.pm.PackageManager import android.content.pm.PackageManager.ResolveInfoFlags import android.os.UserHandle import android.service.quicksettings.TileService -import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow +import com.android.systemui.common.data.repository.PackageChangeRepository +import com.android.systemui.common.data.shared.model.PackageChangeModel import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application 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.channels.awaitClose import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flowOn @@ -52,6 +50,7 @@ class InstalledTilesComponentRepositoryImpl constructor( @Application private val applicationContext: Context, @Background private val backgroundDispatcher: CoroutineDispatcher, + private val packageChangeRepository: PackageChangeRepository ) : InstalledTilesComponentRepository { override fun getInstalledTilesComponents(userId: Int): Flow<Set<ComponentName>> { @@ -70,24 +69,9 @@ constructor( ) .packageManager } - return conflatedCallbackFlow { - val receiver = - object : BroadcastReceiver() { - override fun onReceive(context: Context?, intent: Intent?) { - trySend(Unit) - } - } - applicationContext.registerReceiverAsUser( - receiver, - UserHandle.of(userId), - INTENT_FILTER, - /* broadcastPermission = */ null, - /* scheduler = */ null - ) - - awaitClose { applicationContext.unregisterReceiver(receiver) } - } - .onStart { emit(Unit) } + return packageChangeRepository + .packageChanged(UserHandle.of(userId)) + .onStart { emit(PackageChangeModel.Empty) } .map { reloadComponents(userId, packageManager) } .distinctUntilChanged() .flowOn(backgroundDispatcher) @@ -104,14 +88,6 @@ constructor( } companion object { - private val INTENT_FILTER = - IntentFilter().apply { - addAction(Intent.ACTION_PACKAGE_ADDED) - addAction(Intent.ACTION_PACKAGE_CHANGED) - addAction(Intent.ACTION_PACKAGE_REMOVED) - addAction(Intent.ACTION_PACKAGE_REPLACED) - addDataScheme("package") - } private val INTENT = Intent(TileService.ACTION_QS_TILE) private val FLAGS = ResolveInfoFlags.of( 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 a6cccf1cb41b..e2959fe834d4 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 @@ -24,7 +24,9 @@ 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.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 kotlinx.coroutines.CoroutineScope @@ -44,6 +46,7 @@ constructor( private val keyguardRepository: KeyguardRepository, private val headsUpManager: HeadsUpManager, private val powerInteractor: PowerInteractor, + private val activeNotificationsInteractor: ActiveNotificationsInteractor, ) : CoreStartable { private var notificationPresenter: NotificationPresenter? = null @@ -117,6 +120,14 @@ constructor( return if (headsUpManager.hasPinnedHeadsUp() && isNotifPresenterFullyCollapsed) { 1 } else { + getActiveNotificationsCount() + } + } + + private fun getActiveNotificationsCount(): Int { + return if (NotificationsLiveDataStoreRefactor.isEnabled) { + activeNotificationsInteractor.allNotificationsCountValue + } else { notificationsController?.getActiveNotificationsCount() ?: 0 } } diff --git a/packages/SystemUI/src/com/android/systemui/scene/shared/flag/SceneContainerFlags.kt b/packages/SystemUI/src/com/android/systemui/scene/shared/flag/SceneContainerFlags.kt index 8c3e4a5d0be2..a755805d1872 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/shared/flag/SceneContainerFlags.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/shared/flag/SceneContainerFlags.kt @@ -37,6 +37,7 @@ object SceneContainerFlag { /** The flag description -- not an aconfig flag name */ const val DESCRIPTION = "SceneContainerFlag" + @JvmStatic inline val isEnabled get() = SCENE_CONTAINER_ENABLED && // mainStaticFlag @@ -44,6 +45,7 @@ object SceneContainerFlag { keyguardBottomAreaRefactor() && KeyguardShadeMigrationNssl.isEnabled && MediaInSceneContainerFlag.isEnabled && + // NOTE: Changes should also be made in getSecondaryFlags and @EnableSceneContainer ComposeFacade.isComposeAvailable() /** @@ -63,6 +65,7 @@ object SceneContainerFlag { FlagToken(FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR, keyguardBottomAreaRefactor()), KeyguardShadeMigrationNssl.token, MediaInSceneContainerFlag.token, + // NOTE: Changes should also be made in isEnabled and @EnableSceneContainer ) /** The full set of requirements for SceneContainer */ diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootViewBinder.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootViewBinder.kt index 4a839b8df9ba..93cfc5dbcbe3 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootViewBinder.kt @@ -85,12 +85,13 @@ object SceneWindowRootViewBinder { view.addView( ComposeFacade.createSceneContainerView( - scope = this, - context = view.context, - viewModel = viewModel, - windowInsets = windowInsets, - sceneByKey = sortedSceneByKey, - ) + scope = this, + context = view.context, + viewModel = viewModel, + windowInsets = windowInsets, + sceneByKey = sortedSceneByKey, + ) + .also { it.id = R.id.scene_container_root_composable } ) val legacyView = view.requireViewById<View>(R.id.legacy_window_root) diff --git a/packages/SystemUI/src/com/android/systemui/settings/UserFileManagerExt.kt b/packages/SystemUI/src/com/android/systemui/settings/UserFileManagerExt.kt new file mode 100644 index 000000000000..b09bfe21e014 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/settings/UserFileManagerExt.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.settings + +import android.annotation.UserIdInt +import android.content.Context +import android.content.SharedPreferences +import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow + +/** Extension functions for [UserFileManager]. */ +object UserFileManagerExt { + + /** Returns a flow of [Unit] that is invoked each time the shared preference is updated. */ + fun UserFileManager.observeSharedPreferences( + fileName: String, + @Context.PreferencesMode mode: Int, + @UserIdInt userId: Int + ): Flow<Unit> = conflatedCallbackFlow { + val sharedPrefs = getSharedPreferences(fileName, mode, userId) + + val listener = SharedPreferences.OnSharedPreferenceChangeListener { _, _ -> trySend(Unit) } + + sharedPrefs.registerOnSharedPreferenceChangeListener(listener) + awaitClose { sharedPrefs.unregisterOnSharedPreferenceChangeListener(listener) } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModel.kt b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModel.kt index d0da945ca7d0..9af2d58910b6 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModel.kt @@ -20,15 +20,10 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor import com.android.systemui.media.controls.pipeline.MediaDataManager -import com.android.systemui.media.controls.ui.MediaHierarchyManager -import com.android.systemui.media.controls.ui.MediaHost -import com.android.systemui.media.controls.ui.MediaHostState -import com.android.systemui.media.dagger.MediaModule import com.android.systemui.qs.ui.adapter.QSSceneAdapter import com.android.systemui.scene.shared.model.SceneKey import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationsPlaceholderViewModel import javax.inject.Inject -import javax.inject.Named import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow @@ -46,7 +41,6 @@ constructor( val shadeHeaderViewModel: ShadeHeaderViewModel, val notifications: NotificationsPlaceholderViewModel, val mediaDataManager: MediaDataManager, - @Named(MediaModule.QUICK_QS_PANEL) private val mediaHost: MediaHost, ) { /** The key of the scene we should switch to when swiping up. */ val upDestinationSceneKey: StateFlow<SceneKey> = @@ -83,12 +77,6 @@ constructor( } } - init { - mediaHost.expansion = MediaHostState.EXPANDED - mediaHost.showsOnlyActiveMedia = true - mediaHost.init(MediaHierarchyManager.LOCATION_QQS) - } - fun isMediaVisible(): Boolean { // TODO(b/296122467): handle updates to carousel visibility while scene is still visible return mediaDataManager.hasActiveMediaOrRecommendation() diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/NotificationVisibilityProviderImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/NotificationVisibilityProviderImpl.kt index ec10aaf3cfe3..88e94e354913 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/NotificationVisibilityProviderImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/provider/NotificationVisibilityProviderImpl.kt @@ -22,12 +22,17 @@ import com.android.systemui.statusbar.notification.collection.NotifLiveDataStore import com.android.systemui.statusbar.notification.collection.NotificationEntry import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection import com.android.systemui.statusbar.notification.collection.render.NotificationVisibilityProvider +import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor import com.android.systemui.statusbar.notification.logging.NotificationLogger +import com.android.systemui.statusbar.notification.shared.NotificationsLiveDataStoreRefactor import javax.inject.Inject /** pipeline-agnostic implementation for getting [NotificationVisibility]. */ @SysUISingleton -class NotificationVisibilityProviderImpl @Inject constructor( +class NotificationVisibilityProviderImpl +@Inject +constructor( + private val activeNotificationsInteractor: ActiveNotificationsInteractor, private val notifDataStore: NotifLiveDataStore, private val notifCollection: CommonNotifCollection ) : NotificationVisibilityProvider { @@ -47,5 +52,10 @@ class NotificationVisibilityProviderImpl @Inject constructor( override fun getLocation(key: String): NotificationVisibility.NotificationLocation = NotificationLogger.getNotificationLocation(notifCollection.getEntry(key)) - private fun getCount() = notifDataStore.activeNotifCount.value + private fun getCount() = + if (NotificationsLiveDataStoreRefactor.isEnabled) { + activeNotificationsInteractor.allNotificationsCountValue + } else { + notifDataStore.activeNotifCount.value + } } 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 2d5afd56da72..3cdb2cd9b5c6 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 androidx.annotation.NonNull; 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 @@ public class GroupExpansionManagerImpl implements GroupExpansionManager, Dumpabl */ 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 class GroupExpansionManagerImpl implements GroupExpansionManager, Dumpabl }; 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 @@ public class GroupExpansionManagerImpl implements GroupExpansionManager, Dumpabl @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 @@ public class GroupExpansionManagerImpl implements GroupExpansionManager, Dumpabl } // 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 cb7935369564..da1247953c4c 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.NonNull; 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 @@ import javax.inject.Inject; */ @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 @@ public class GroupMembershipManagerImpl implements GroupMembershipManager { @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/dagger/NotificationStatsLoggerModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationStatsLoggerModule.kt new file mode 100644 index 000000000000..cbd988720b94 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationStatsLoggerModule.kt @@ -0,0 +1,148 @@ +/* + * 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.dagger + +import com.android.systemui.CoreStartable +import com.android.systemui.NoOpCoreStartable +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.UiBackground +import com.android.systemui.plugins.statusbar.StatusBarStateController +import com.android.systemui.scene.domain.interactor.WindowRootViewVisibilityInteractor +import com.android.systemui.statusbar.NotificationListener +import com.android.systemui.statusbar.notification.collection.NotifLiveDataStore +import com.android.systemui.statusbar.notification.collection.NotifPipeline +import com.android.systemui.statusbar.notification.collection.render.NotificationVisibilityProvider +import com.android.systemui.statusbar.notification.logging.NotificationLogger +import com.android.systemui.statusbar.notification.logging.NotificationLogger.ExpansionStateLogger +import com.android.systemui.statusbar.notification.logging.NotificationPanelLogger +import com.android.systemui.statusbar.notification.shared.NotificationsLiveDataStoreRefactor +import com.android.systemui.statusbar.notification.stack.ui.view.NotificationRowStatsLogger +import com.android.systemui.statusbar.notification.stack.ui.view.NotificationStatsLogger +import com.android.systemui.statusbar.notification.stack.ui.view.NotificationStatsLoggerImpl +import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationLoggerViewModel +import com.android.systemui.util.kotlin.JavaAdapter +import com.android.systemui.util.kotlin.getOrNull +import dagger.Binds +import dagger.Module +import dagger.Provides +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap +import java.util.Optional +import java.util.concurrent.Executor +import javax.inject.Provider + +@Module +interface NotificationStatsLoggerModule { + + /** Binds an implementation to the [NotificationStatsLogger]. */ + @Binds fun bindsStatsLoggerImpl(impl: NotificationStatsLoggerImpl): NotificationStatsLogger + + companion object { + + /** Provides a [NotificationStatsLogger] if the refactor flag is on. */ + @Provides + fun provideStatsLogger( + provider: Provider<NotificationStatsLogger> + ): Optional<NotificationStatsLogger> { + return if (NotificationsLiveDataStoreRefactor.isEnabled) { + Optional.of(provider.get()) + } else { + Optional.empty() + } + } + + /** Provides a [NotificationLoggerViewModel] if the refactor flag is on. */ + @Provides + fun provideViewModel( + provider: Provider<NotificationLoggerViewModel> + ): Optional<NotificationLoggerViewModel> { + return if (NotificationsLiveDataStoreRefactor.isEnabled) { + Optional.of(provider.get()) + } else { + Optional.empty() + } + } + + /** Provides the legacy [NotificationLogger] if the refactor flag is off. */ + @Provides + @SysUISingleton + fun provideLegacyLoggerOptional( + notificationListener: NotificationListener?, + @UiBackground uiBgExecutor: Executor?, + notifLiveDataStore: NotifLiveDataStore?, + visibilityProvider: NotificationVisibilityProvider?, + notifPipeline: NotifPipeline?, + statusBarStateController: StatusBarStateController?, + windowRootViewVisibilityInteractor: WindowRootViewVisibilityInteractor?, + javaAdapter: JavaAdapter?, + expansionStateLogger: ExpansionStateLogger?, + notificationPanelLogger: NotificationPanelLogger? + ): Optional<NotificationLogger> { + return if (NotificationsLiveDataStoreRefactor.isEnabled) { + Optional.empty() + } else { + Optional.of( + NotificationLogger( + notificationListener, + uiBgExecutor, + notifLiveDataStore, + visibilityProvider, + notifPipeline, + statusBarStateController, + windowRootViewVisibilityInteractor, + javaAdapter, + expansionStateLogger, + notificationPanelLogger + ) + ) + } + } + + /** + * Provides a the legacy [NotificationLogger] or the new [NotificationStatsLogger] to the + * notification row. + * + * TODO(b/308623704) remove the [NotificationRowStatsLogger] interface, and provide a + * [NotificationStatsLogger] to the row directly. + */ + @Provides + fun provideRowStatsLogger( + newProvider: Provider<NotificationStatsLogger>, + legacyLoggerOptional: Optional<NotificationLogger>, + ): NotificationRowStatsLogger { + return legacyLoggerOptional.getOrNull() ?: newProvider.get() + } + + /** + * Binds the legacy [NotificationLogger] as a [CoreStartable] if the feature flag is off, or + * binds a no-op [CoreStartable] otherwise. + * + * The old [NotificationLogger] is a [CoreStartable], because it's managing its own data + * updates, but the new [NotificationStatsLogger] is not. Currently Dagger doesn't support + * optionally binding entries with @[IntoMap], therefore we provide a no-op [CoreStartable] + * here if the feature flag is on, but this can be removed once the flag is released. + */ + @Provides + @IntoMap + @ClassKey(NotificationLogger::class) + fun provideCoreStartable( + legacyLoggerOptional: Optional<NotificationLogger> + ): CoreStartable { + return legacyLoggerOptional.getOrNull() ?: NoOpCoreStartable() + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java index 3a722050dab2..6bba72b2cd49 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java @@ -17,14 +17,12 @@ package com.android.systemui.statusbar.notification.dagger; import android.content.Context; +import android.service.notification.NotificationListenerService; import com.android.internal.jank.InteractionJankMonitor; import com.android.systemui.CoreStartable; import com.android.systemui.dagger.SysUISingleton; -import com.android.systemui.dagger.qualifiers.UiBackground; -import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.res.R; -import com.android.systemui.scene.domain.interactor.WindowRootViewVisibilityInteractor; import com.android.systemui.statusbar.NotificationListener; import com.android.systemui.statusbar.notification.NotificationActivityStarter; import com.android.systemui.statusbar.notification.NotificationLaunchAnimatorControllerProvider; @@ -67,7 +65,6 @@ import com.android.systemui.statusbar.notification.interruption.NotificationInte import com.android.systemui.statusbar.notification.interruption.VisualInterruptionDecisionProvider; import com.android.systemui.statusbar.notification.interruption.VisualInterruptionDecisionProviderImpl; import com.android.systemui.statusbar.notification.interruption.VisualInterruptionRefactor; -import com.android.systemui.statusbar.notification.logging.NotificationLogger; import com.android.systemui.statusbar.notification.logging.NotificationPanelLogger; import com.android.systemui.statusbar.notification.logging.NotificationPanelLoggerImpl; import com.android.systemui.statusbar.notification.row.NotificationGutsManager; @@ -80,7 +77,8 @@ import com.android.systemui.statusbar.notification.stack.StackScrollAlgorithm; import com.android.systemui.statusbar.phone.KeyguardBypassController; import com.android.systemui.statusbar.phone.StatusBarNotificationActivityStarter; import com.android.systemui.statusbar.policy.HeadsUpManager; -import com.android.systemui.util.kotlin.JavaAdapter; + +import javax.inject.Provider; import dagger.Binds; import dagger.Module; @@ -88,10 +86,6 @@ import dagger.Provides; import dagger.multibindings.ClassKey; import dagger.multibindings.IntoMap; -import java.util.concurrent.Executor; - -import javax.inject.Provider; - /** * Dagger Module for classes found within the com.android.systemui.statusbar.notification package. */ @@ -104,6 +98,7 @@ import javax.inject.Provider; NotificationSectionHeadersModule.class, ActivatableNotificationViewModelModule.class, NotificationMemoryModule.class, + NotificationStatsLoggerModule.class, }) public interface NotificationsModule { @Binds @@ -128,39 +123,6 @@ public interface NotificationsModule { VisibilityLocationProvider bindVisibilityLocationProvider( VisibilityLocationProviderDelegator visibilityLocationProviderDelegator); - /** Provides an instance of {@link NotificationLogger} */ - @SysUISingleton - @Provides - static NotificationLogger provideNotificationLogger( - NotificationListener notificationListener, - @UiBackground Executor uiBgExecutor, - NotifLiveDataStore notifLiveDataStore, - NotificationVisibilityProvider visibilityProvider, - NotifPipeline notifPipeline, - StatusBarStateController statusBarStateController, - WindowRootViewVisibilityInteractor windowRootViewVisibilityInteractor, - JavaAdapter javaAdapter, - NotificationLogger.ExpansionStateLogger expansionStateLogger, - NotificationPanelLogger notificationPanelLogger) { - return new NotificationLogger( - notificationListener, - uiBgExecutor, - notifLiveDataStore, - visibilityProvider, - notifPipeline, - statusBarStateController, - windowRootViewVisibilityInteractor, - javaAdapter, - expansionStateLogger, - notificationPanelLogger); - } - - /** Binds {@link NotificationLogger} as a {@link CoreStartable}. */ - @Binds - @IntoMap - @ClassKey(NotificationLogger.class) - CoreStartable bindsNotificationLogger(NotificationLogger notificationLogger); - /** Provides an instance of {@link NotificationPanelLogger} */ @SysUISingleton @Provides @@ -272,6 +234,10 @@ public interface NotificationsModule { NotifLiveDataStore bindNotifLiveDataStore(NotifLiveDataStoreImpl notifLiveDataStoreImpl); /** */ + @Binds + NotificationListenerService bindNotificationListener(NotificationListener notificationListener); + + /** */ @Provides @SysUISingleton static VisualInterruptionDecisionProvider provideVisualInterruptionDecisionProvider( diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/repository/ActiveNotificationListRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/repository/ActiveNotificationListRepository.kt index 5ed82cc1ed5c..5c844bcf749c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/repository/ActiveNotificationListRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/repository/ActiveNotificationListRepository.kt @@ -55,6 +55,12 @@ data class ActiveNotificationsStore( * invoking [get]. */ val renderList: List<Key> = emptyList(), + + /** + * Map of notification key to rank, where rank is the 0-based index of the notification on the + * system server, meaning that in the unfiltered flattened list of notification entries. + */ + val rankingsMap: Map<String, Int> = emptyMap() ) { operator fun get(key: Key): ActiveNotificationEntryModel? { return when (key) { @@ -74,8 +80,9 @@ data class ActiveNotificationsStore( private val groups = mutableMapOf<String, ActiveNotificationGroupModel>() private val individuals = mutableMapOf<String, ActiveNotificationModel>() private val renderList = mutableListOf<Key>() + private var rankingsMap: Map<String, Int> = emptyMap() - fun build() = ActiveNotificationsStore(groups, individuals, renderList) + fun build() = ActiveNotificationsStore(groups, individuals, renderList, rankingsMap) fun addEntry(entry: ActiveNotificationEntryModel) { when (entry) { @@ -95,5 +102,9 @@ data class ActiveNotificationsStore( individuals[group.summary.key] = group.summary group.children.forEach { individuals[it.key] = it } } + + fun setRankingsMap(map: Map<String, Int>) { + rankingsMap = map.toMap() + } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractor.kt index e90ddf98db00..b22e9fd2fb17 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractor.kt @@ -33,7 +33,10 @@ constructor( private val repository: ActiveNotificationListRepository, @Background private val backgroundDispatcher: CoroutineDispatcher, ) { - /** Notifications actively presented to the user in the notification stack, in order. */ + /** + * Top level list of Notifications actively presented to the user in the notification stack, in + * order. + */ val topLevelRepresentativeNotifications: Flow<List<ActiveNotificationModel>> = repository.activeNotifications .map { store -> @@ -51,6 +54,23 @@ constructor( } .flowOn(backgroundDispatcher) + /** + * Flattened list of Notifications actively presented to the user in the notification stack, in + * order. + */ + val allRepresentativeNotifications: Flow<Map<String, ActiveNotificationModel>> = + repository.activeNotifications.map { store -> store.individuals } + + /** Size of the flattened list of Notifications actively presented in the stack. */ + val allNotificationsCount: Flow<Int> = + repository.activeNotifications.map { store -> store.individuals.size } + + /** + * The same as [allNotificationsCount], but without flows, for easy access in synchronous code. + */ + val allNotificationsCountValue: Int + get() = repository.activeNotifications.value.individuals.size + /** Are any notifications being actively presented in the notification stack? */ val areAnyNotificationsPresent: Flow<Boolean> = repository.activeNotifications @@ -65,6 +85,16 @@ constructor( val areAnyNotificationsPresentValue: Boolean get() = repository.activeNotifications.value.renderList.isNotEmpty() + /** + * Map of notification key to rank, where rank is the 0-based index of the notification in the + * system server, meaning that in the unfiltered flattened list of notification entries. Used + * for logging purposes. + * + * @see [com.android.systemui.statusbar.notification.stack.ui.view.NotificationStatsLogger]. + */ + val activeNotificationRanks: Flow<Map<String, Int>> = + repository.activeNotifications.map { store -> store.rankingsMap } + /** Are there are any notifications that can be cleared by the "Clear all" button? */ val hasClearableNotifications: Flow<Boolean> = repository.notifStats diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/RenderNotificationListInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/RenderNotificationListInteractor.kt index 6f4ed9db20b1..695f21569f3c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/RenderNotificationListInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/RenderNotificationListInteractor.kt @@ -16,6 +16,7 @@ package com.android.systemui.statusbar.notification.domain.interactor import android.graphics.drawable.Icon +import android.util.ArrayMap import com.android.systemui.statusbar.notification.collection.GroupEntry import com.android.systemui.statusbar.notification.collection.ListEntry import com.android.systemui.statusbar.notification.collection.NotificationEntry @@ -46,6 +47,7 @@ constructor( repository.activeNotifications.update { existingModels -> buildActiveNotificationsStore(existingModels, sectionStyleProvider) { entries.forEach(::addListEntry) + setRankingsMap(entries) } } } @@ -94,6 +96,27 @@ private class ActiveNotificationsStoreBuilder( } } + fun setRankingsMap(entries: List<ListEntry>) { + builder.setRankingsMap(flatMapToRankingsMap(entries)) + } + + fun flatMapToRankingsMap(entries: List<ListEntry>): Map<String, Int> { + val result = ArrayMap<String, Int>() + for (entry in entries) { + if (entry is NotificationEntry) { + entry.representativeEntry?.let { representativeEntry -> + result[representativeEntry.key] = representativeEntry.ranking.rank + } + } else if (entry is GroupEntry) { + entry.summary?.let { summary -> result[summary.key] = summary.ranking.rank } + for (child in entry.children) { + result[child.key] = child.ranking.rank + } + } + } + return result + } + private fun NotificationEntry.toModel(): ActiveNotificationModel = existingModels.createOrReuse( key = key, @@ -107,6 +130,11 @@ private class ActiveNotificationsStoreBuilder( aodIcon = icons.aodIcon?.sourceIcon, shelfIcon = icons.shelfIcon?.sourceIcon, statusBarIcon = icons.statusBarIcon?.sourceIcon, + uid = sbn.uid, + packageName = sbn.packageName, + instanceId = sbn.instanceId?.id, + isGroupSummary = sbn.notification.isGroupSummary, + bucket = bucket, ) } @@ -121,7 +149,12 @@ private fun ActiveNotificationsStore.createOrReuse( isPulsing: Boolean, aodIcon: Icon?, shelfIcon: Icon?, - statusBarIcon: Icon? + statusBarIcon: Icon?, + uid: Int, + packageName: String, + instanceId: Int?, + isGroupSummary: Boolean, + bucket: Int, ): ActiveNotificationModel { return individuals[key]?.takeIf { it.isCurrent( @@ -135,7 +168,12 @@ private fun ActiveNotificationsStore.createOrReuse( isPulsing = isPulsing, aodIcon = aodIcon, shelfIcon = shelfIcon, - statusBarIcon = statusBarIcon + statusBarIcon = statusBarIcon, + uid = uid, + instanceId = instanceId, + isGroupSummary = isGroupSummary, + packageName = packageName, + bucket = bucket, ) } ?: ActiveNotificationModel( @@ -150,6 +188,11 @@ private fun ActiveNotificationsStore.createOrReuse( aodIcon = aodIcon, shelfIcon = shelfIcon, statusBarIcon = statusBarIcon, + uid = uid, + instanceId = instanceId, + isGroupSummary = isGroupSummary, + packageName = packageName, + bucket = bucket, ) } @@ -164,7 +207,12 @@ private fun ActiveNotificationModel.isCurrent( isPulsing: Boolean, aodIcon: Icon?, shelfIcon: Icon?, - statusBarIcon: Icon? + statusBarIcon: Icon?, + uid: Int, + packageName: String, + instanceId: Int?, + isGroupSummary: Boolean, + bucket: Int, ): Boolean { return when { key != this.key -> false @@ -178,6 +226,11 @@ private fun ActiveNotificationModel.isCurrent( aodIcon != this.aodIcon -> false shelfIcon != this.shelfIcon -> false statusBarIcon != this.statusBarIcon -> false + uid != this.uid -> false + instanceId != this.instanceId -> false + isGroupSummary != this.isGroupSummary -> false + packageName != this.packageName -> false + bucket != this.bucket -> false else -> true } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerImpl.kt index 7d1cca81e614..1677418c5c30 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/init/NotificationsControllerImpl.kt @@ -18,7 +18,6 @@ package com.android.systemui.statusbar.notification.init import android.service.notification.StatusBarNotification import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.flags.FeatureFlags import com.android.systemui.people.widget.PeopleSpaceWidgetManager import com.android.systemui.plugins.statusbar.NotificationSwipeActionHelper.SnoozeOption import com.android.systemui.statusbar.NotificationListener @@ -39,6 +38,7 @@ import com.android.systemui.statusbar.notification.collection.render.NotifStackC import com.android.systemui.statusbar.notification.interruption.HeadsUpViewBinder import com.android.systemui.statusbar.notification.logging.NotificationLogger import com.android.systemui.statusbar.notification.row.NotifBindPipelineInitializer +import com.android.systemui.statusbar.notification.shared.NotificationsLiveDataStoreRefactor import com.android.systemui.statusbar.notification.stack.NotificationListContainer import com.android.wm.shell.bubbles.Bubbles import dagger.Lazy @@ -53,7 +53,9 @@ import javax.inject.Inject * any initialization work that notifications require. */ @SysUISingleton -class NotificationsControllerImpl @Inject constructor( +class NotificationsControllerImpl +@Inject +constructor( private val notificationListener: NotificationListener, private val commonNotifCollection: Lazy<CommonNotifCollection>, private val notifPipeline: Lazy<NotifPipeline>, @@ -61,7 +63,7 @@ class NotificationsControllerImpl @Inject constructor( private val targetSdkResolver: TargetSdkResolver, private val notifPipelineInitializer: Lazy<NotifPipelineInitializer>, private val notifBindPipelineInitializer: NotifBindPipelineInitializer, - private val notificationLogger: NotificationLogger, + private val notificationLoggerOptional: Optional<NotificationLogger>, private val notificationRowBinder: NotificationRowBinderImpl, private val notificationsMediaManager: NotificationMediaManager, private val headsUpViewBinder: HeadsUpViewBinder, @@ -69,7 +71,6 @@ class NotificationsControllerImpl @Inject constructor( private val animatedImageNotificationManager: AnimatedImageNotificationManager, private val peopleSpaceWidgetManager: PeopleSpaceWidgetManager, private val bubblesOptional: Optional<Bubbles>, - private val featureFlags: FeatureFlags ) : NotificationsController { override fun initialize( @@ -80,28 +81,35 @@ class NotificationsControllerImpl @Inject constructor( ) { notificationListener.registerAsSystemService() - notifPipeline.get().addCollectionListener(object : NotifCollectionListener { - override fun onEntryRemoved(entry: NotificationEntry, reason: Int) { - listContainer.cleanUpViewStateForEntry(entry) - } - }) + notifPipeline + .get() + .addCollectionListener( + object : NotifCollectionListener { + override fun onEntryRemoved(entry: NotificationEntry, reason: Int) { + listContainer.cleanUpViewStateForEntry(entry) + } + } + ) notificationRowBinder.setNotificationClicker( - clickerBuilder.build(bubblesOptional, notificationActivityStarter)) + clickerBuilder.build(bubblesOptional, notificationActivityStarter) + ) notificationRowBinder.setUpWithPresenter(presenter, listContainer) headsUpViewBinder.setPresenter(presenter) notifBindPipelineInitializer.initialize() animatedImageNotificationManager.bind() - notifPipelineInitializer.get().initialize( - notificationListener, - notificationRowBinder, - listContainer, - stackController) + notifPipelineInitializer + .get() + .initialize(notificationListener, notificationRowBinder, listContainer, stackController) targetSdkResolver.initialize(notifPipeline.get()) notificationsMediaManager.setUpWithPresenter(presenter) - notificationLogger.setUpWithContainer(listContainer) + if (!NotificationsLiveDataStoreRefactor.isEnabled) { + notificationLoggerOptional.ifPresent { logger -> + logger.setUpWithContainer(listContainer) + } + } peopleSpaceWidgetManager.attach(notificationListener) } @@ -120,11 +128,14 @@ class NotificationsControllerImpl @Inject constructor( notificationListener.snoozeNotification(sbn.key, snoozeOption.snoozeCriterion.id) } else { notificationListener.snoozeNotification( - sbn.key, - snoozeOption.minutesToSnoozeFor * 60 * 1000.toLong()) + sbn.key, + snoozeOption.minutesToSnoozeFor * 60 * 1000.toLong() + ) } } - override fun getActiveNotificationsCount(): Int = - notifLiveDataStore.activeNotifCount.value + override fun getActiveNotificationsCount(): Int { + NotificationsLiveDataStoreRefactor.assertInLegacyMode() + return notifLiveDataStore.activeNotifCount.value + } } 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 8d2a63e9b3fa..4349b3b5aeb3 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 @@ -33,6 +33,7 @@ import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.statusbar.IStatusBarService; import com.android.internal.statusbar.NotificationVisibility; +import com.android.internal.statusbar.NotificationVisibility.NotificationLocation; import com.android.systemui.CoreStartable; import com.android.systemui.dagger.qualifiers.UiBackground; import com.android.systemui.plugins.statusbar.StatusBarStateController; @@ -46,8 +47,10 @@ import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener; import com.android.systemui.statusbar.notification.collection.render.NotificationVisibilityProvider; import com.android.systemui.statusbar.notification.dagger.NotificationsModule; +import com.android.systemui.statusbar.notification.shared.NotificationsLiveDataStoreRefactor; import com.android.systemui.statusbar.notification.stack.ExpandableViewState; import com.android.systemui.statusbar.notification.stack.NotificationListContainer; +import com.android.systemui.statusbar.notification.stack.ui.view.NotificationRowStatsLogger; import com.android.systemui.util.Compile; import com.android.systemui.util.kotlin.JavaAdapter; @@ -64,7 +67,8 @@ import javax.inject.Inject; * Handles notification logging, in particular, logging which notifications are visible and which * are not. */ -public class NotificationLogger implements StateListener, CoreStartable { +public class NotificationLogger implements StateListener, CoreStartable, + NotificationRowStatsLogger { static final String TAG = "NotificationLogger"; private static final boolean DEBUG = Compile.IS_DEBUG && Log.isLoggable(TAG, Log.DEBUG); @@ -166,31 +170,31 @@ public class NotificationLogger implements StateListener, CoreStartable { /** * Returns the location of the notification referenced by the given {@link NotificationEntry}. */ - public static NotificationVisibility.NotificationLocation getNotificationLocation( + public static NotificationLocation getNotificationLocation( NotificationEntry entry) { if (entry == null || entry.getRow() == null || entry.getRow().getViewState() == null) { - return NotificationVisibility.NotificationLocation.LOCATION_UNKNOWN; + return NotificationLocation.LOCATION_UNKNOWN; } return convertNotificationLocation(entry.getRow().getViewState().location); } - private static NotificationVisibility.NotificationLocation convertNotificationLocation( + private static NotificationLocation convertNotificationLocation( int location) { switch (location) { case ExpandableViewState.LOCATION_FIRST_HUN: - return NotificationVisibility.NotificationLocation.LOCATION_FIRST_HEADS_UP; + return NotificationLocation.LOCATION_FIRST_HEADS_UP; case ExpandableViewState.LOCATION_HIDDEN_TOP: - return NotificationVisibility.NotificationLocation.LOCATION_HIDDEN_TOP; + return NotificationLocation.LOCATION_HIDDEN_TOP; case ExpandableViewState.LOCATION_MAIN_AREA: - return NotificationVisibility.NotificationLocation.LOCATION_MAIN_AREA; + return NotificationLocation.LOCATION_MAIN_AREA; case ExpandableViewState.LOCATION_BOTTOM_STACK_PEEKING: - return NotificationVisibility.NotificationLocation.LOCATION_BOTTOM_STACK_PEEKING; + return NotificationLocation.LOCATION_BOTTOM_STACK_PEEKING; case ExpandableViewState.LOCATION_BOTTOM_STACK_HIDDEN: - return NotificationVisibility.NotificationLocation.LOCATION_BOTTOM_STACK_HIDDEN; + return NotificationLocation.LOCATION_BOTTOM_STACK_HIDDEN; case ExpandableViewState.LOCATION_GONE: - return NotificationVisibility.NotificationLocation.LOCATION_GONE; + return NotificationLocation.LOCATION_GONE; default: - return NotificationVisibility.NotificationLocation.LOCATION_UNKNOWN; + return NotificationLocation.LOCATION_UNKNOWN; } } @@ -207,6 +211,9 @@ public class NotificationLogger implements StateListener, CoreStartable { JavaAdapter javaAdapter, ExpansionStateLogger expansionStateLogger, NotificationPanelLogger notificationPanelLogger) { + // Not expected to be constructed if the feature flag is on + NotificationsLiveDataStoreRefactor.assertInLegacyMode(); + mNotificationListener = notificationListener; mUiBgExecutor = uiBgExecutor; mNotifLiveDataStore = notifLiveDataStore; @@ -382,9 +389,11 @@ public class NotificationLogger implements StateListener, CoreStartable { /** * Called when the notification is expanded / collapsed. */ - public void onExpansionChanged(String key, boolean isUserAction, boolean isExpanded) { - NotificationVisibility.NotificationLocation location = mVisibilityProvider.getLocation(key); - mExpansionStateLogger.onExpansionChanged(key, isUserAction, isExpanded, location); + @Override + public void onNotificationExpansionChanged(@NonNull String key, boolean isExpanded, + int location, boolean isUserAction) { + NotificationLocation notifLocation = mVisibilityProvider.getLocation(key); + mExpansionStateLogger.onExpansionChanged(key, isUserAction, isExpanded, notifLocation); } @VisibleForTesting @@ -440,7 +449,7 @@ public class NotificationLogger implements StateListener, CoreStartable { @VisibleForTesting void onExpansionChanged(String key, boolean isUserAction, boolean isExpanded, - NotificationVisibility.NotificationLocation location) { + NotificationLocation location) { State state = getState(key); state.mIsUserAction = isUserAction; state.mIsExpanded = isExpanded; @@ -528,7 +537,7 @@ public class NotificationLogger implements StateListener, CoreStartable { @Nullable Boolean mIsVisible; @Nullable - NotificationVisibility.NotificationLocation mLocation; + NotificationLocation mLocation; private State() {} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLogger.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLogger.java index 5ca13c95309f..6c63d1d6dcb5 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLogger.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLogger.java @@ -40,6 +40,11 @@ public interface NotificationPanelLogger { /** * Log a NOTIFICATION_PANEL_REPORTED statsd event. + */ + void logPanelShown(boolean isLockscreen, Notifications.NotificationList proto); + + /** + * Log a NOTIFICATION_PANEL_REPORTED statsd event. * @param visibleNotifications as provided by NotificationEntryManager.getVisibleNotifications() */ void logPanelShown(boolean isLockscreen, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLoggerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLoggerImpl.java index 9a632282ae16..d7f7b760dd04 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLoggerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLoggerImpl.java @@ -21,6 +21,7 @@ import static com.android.systemui.statusbar.notification.logging.NotificationPa import com.android.systemui.shared.system.SysUiStatsLog; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.logging.nano.Notifications; +import com.android.systemui.statusbar.notification.shared.NotificationsLiveDataStoreRefactor; import com.google.protobuf.nano.MessageNano; @@ -31,15 +32,25 @@ import java.util.List; * Normal implementation of NotificationPanelLogger. */ public class NotificationPanelLoggerImpl implements NotificationPanelLogger { + + @Override + public void logPanelShown(boolean isLockscreen, Notifications.NotificationList proto) { + SysUiStatsLog.write(SysUiStatsLog.NOTIFICATION_PANEL_REPORTED, + /* event_id = */ NotificationPanelEvent.fromLockscreen(isLockscreen).getId(), + /* num_notifications = */ proto.notifications.length, + /* notifications = */ MessageNano.toByteArray(proto)); + } + @Override public void logPanelShown(boolean isLockscreen, List<NotificationEntry> visibleNotifications) { + NotificationsLiveDataStoreRefactor.assertInLegacyMode(); final Notifications.NotificationList proto = NotificationPanelLogger.toNotificationProto( visibleNotifications); SysUiStatsLog.write(SysUiStatsLog.NOTIFICATION_PANEL_REPORTED, - /* int event_id */ NotificationPanelEvent.fromLockscreen(isLockscreen).getId(), - /* int num_notifications*/ proto.notifications.length, - /* byte[] notifications*/ MessageNano.toByteArray(proto)); + /* event_id = */ NotificationPanelEvent.fromLockscreen(isLockscreen).getId(), + /* num_notifications = */ proto.notifications.length, + /* notifications = */ MessageNano.toByteArray(proto)); } @Override @@ -47,8 +58,8 @@ public class NotificationPanelLoggerImpl implements NotificationPanelLogger { final Notifications.NotificationList proto = NotificationPanelLogger.toNotificationProto( Collections.singletonList(draggedNotification)); SysUiStatsLog.write(SysUiStatsLog.NOTIFICATION_PANEL_REPORTED, - /* int event_id */ NOTIFICATION_DRAG.getId(), - /* int num_notifications*/ proto.notifications.length, - /* byte[] notifications*/ MessageNano.toByteArray(proto)); + /* event_id = */ NOTIFICATION_DRAG.getId(), + /* num_notifications = */ proto.notifications.length, + /* notifications = */ MessageNano.toByteArray(proto)); } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java index e200e65a9f4a..5eeb0665da0e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java @@ -118,6 +118,7 @@ import java.io.PrintWriter; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.concurrent.TimeUnit; import java.util.function.BooleanSupplier; @@ -988,6 +989,25 @@ public class ExpandableNotificationRow extends ActivatableNotificationView } /** + * Recursively collects the [{@link ExpandableViewState#location}]s populating the provided + * map. + * The visibility of each child is determined by the {@link View#getVisibility()}. + * Locations are added to the provided map including locations from child views, that are + * visible. + */ + public void collectVisibleLocations(Map<String, Integer> locationsMap) { + if (getVisibility() == View.VISIBLE) { + locationsMap.put(getEntry().getKey(), getViewState().location); + if (mChildrenContainer != null) { + List<ExpandableNotificationRow> children = mChildrenContainer.getAttachedChildren(); + for (int i = 0; i < children.size(); i++) { + children.get(i).collectVisibleLocations(locationsMap); + } + } + } + } + + /** * Updates states of all children. */ public void updateChildrenStates() { @@ -1615,7 +1635,8 @@ public class ExpandableNotificationRow extends ActivatableNotificationView /** * Called when the notification is expanded / collapsed. */ - void logNotificationExpansion(String key, boolean userAction, boolean expanded); + void logNotificationExpansion(String key, int location, boolean userAction, + boolean expanded); /** * Called when a notification which was previously kept in its parent for the @@ -3312,7 +3333,8 @@ public class ExpandableNotificationRow extends ActivatableNotificationView if (nowExpanded != wasExpanded) { updateShelfIconColor(); if (mLogger != null) { - mLogger.logNotificationExpansion(mLoggingKey, userAction, nowExpanded); + mLogger.logNotificationExpansion(mLoggingKey, getViewState().location, userAction, + nowExpanded); } if (mIsSummaryWithChildren) { mChildrenContainer.onExpansionChanged(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java index af55f44b785a..0afdefabd4f0 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java @@ -48,13 +48,13 @@ import com.android.systemui.statusbar.notification.collection.render.GroupExpans import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManager; import com.android.systemui.statusbar.notification.collection.render.NodeController; import com.android.systemui.statusbar.notification.collection.render.NotifViewController; -import com.android.systemui.statusbar.notification.logging.NotificationLogger; import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier; import com.android.systemui.statusbar.notification.row.dagger.AppName; import com.android.systemui.statusbar.notification.row.dagger.NotificationKey; import com.android.systemui.statusbar.notification.row.dagger.NotificationRowScope; import com.android.systemui.statusbar.notification.stack.NotificationChildrenContainerLogger; import com.android.systemui.statusbar.notification.stack.NotificationListContainer; +import com.android.systemui.statusbar.notification.stack.ui.view.NotificationRowStatsLogger; import com.android.systemui.statusbar.phone.KeyguardBypassController; import com.android.systemui.statusbar.policy.HeadsUpManager; import com.android.systemui.statusbar.policy.SmartReplyConstants; @@ -90,7 +90,7 @@ public class ExpandableNotificationRowController implements NotifViewController private final GroupMembershipManager mGroupMembershipManager; private final GroupExpansionManager mGroupExpansionManager; private final RowContentBindStage mRowContentBindStage; - private final NotificationLogger mNotificationLogger; + private final NotificationRowStatsLogger mStatsLogger; private final NotificationRowLogger mLogBufferLogger; private final HeadsUpManager mHeadsUpManager; private final ExpandableNotificationRow.OnExpandClickListener mOnExpandClickListener; @@ -130,9 +130,10 @@ public class ExpandableNotificationRowController implements NotifViewController private final ExpandableNotificationRow.ExpandableNotificationRowLogger mLoggerCallback = new ExpandableNotificationRow.ExpandableNotificationRowLogger() { @Override - public void logNotificationExpansion(String key, boolean userAction, + public void logNotificationExpansion(String key, int location, boolean userAction, boolean expanded) { - mNotificationLogger.onExpansionChanged(key, userAction, expanded); + mStatsLogger.onNotificationExpansionChanged(key, expanded, location, + userAction); } @Override @@ -212,7 +213,7 @@ public class ExpandableNotificationRowController implements NotifViewController GroupMembershipManager groupMembershipManager, GroupExpansionManager groupExpansionManager, RowContentBindStage rowContentBindStage, - NotificationLogger notificationLogger, + NotificationRowStatsLogger statsLogger, HeadsUpManager headsUpManager, ExpandableNotificationRow.OnExpandClickListener onExpandClickListener, StatusBarStateController statusBarStateController, @@ -239,7 +240,7 @@ public class ExpandableNotificationRowController implements NotifViewController mGroupMembershipManager = groupMembershipManager; mGroupExpansionManager = groupExpansionManager; mRowContentBindStage = rowContentBindStage; - mNotificationLogger = notificationLogger; + mStatsLogger = statsLogger; mHeadsUpManager = headsUpManager; mOnExpandClickListener = onExpandClickListener; mStatusBarStateController = statusBarStateController; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/shared/AsyncGroupHeaderViewInflation.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/shared/AsyncGroupHeaderViewInflation.kt new file mode 100644 index 000000000000..44fc77f8bd55 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/shared/AsyncGroupHeaderViewInflation.kt @@ -0,0 +1,53 @@ +/* + * 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.shared + +import com.android.systemui.Flags +import com.android.systemui.flags.FlagToken +import com.android.systemui.flags.RefactorFlagUtils + +/** Helper for reading or using the Async Group Header Inflation flag state. */ +@Suppress("NOTHING_TO_INLINE") +object AsyncGroupHeaderViewInflation { + /** The aconfig flag name */ + const val FLAG_NAME = Flags.FLAG_NOTIFICATION_ASYNC_GROUP_HEADER_INFLATION + + /** A token used for dependency declaration */ + val token: FlagToken + get() = FlagToken(FLAG_NAME, isEnabled) + + /** Is the async inflation of group header views enabled */ + @JvmStatic + inline val isEnabled + get() = Flags.notificationAsyncGroupHeaderInflation() + + /** + * Called to ensure code is only run when the flag is enabled. This protects users from the + * unintended behaviors caused by accidentally running new logic, while also crashing on an eng + * build to ensure that the refactor author catches issues in testing. + */ + @JvmStatic + inline fun isUnexpectedlyInLegacyMode() = + RefactorFlagUtils.isUnexpectedlyInLegacyMode(isEnabled, FLAG_NAME) + + /** + * Called to ensure code is only run when the flag is disabled. This will throw an exception if + * the flag is enabled to ensure that the refactor author catches issues in testing. + */ + @JvmStatic + inline fun assertInLegacyMode() = RefactorFlagUtils.assertInLegacyMode(isEnabled, FLAG_NAME) +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/ActiveNotificationModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/ActiveNotificationModel.kt index eb1c1bafae9e..5527efc4029d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/ActiveNotificationModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/ActiveNotificationModel.kt @@ -16,6 +16,7 @@ package com.android.systemui.statusbar.notification.shared import android.graphics.drawable.Icon +import com.android.systemui.statusbar.notification.stack.PriorityBucket /** * Model for a top-level "entry" in the notification list, either an @@ -55,6 +56,16 @@ data class ActiveNotificationModel( val shelfIcon: Icon?, /** Icon to display in the status bar. */ val statusBarIcon: Icon?, + /** The notifying app's [packageName]'s uid. */ + val uid: Int, + /** The notifying app's packageName. */ + val packageName: String, + /** A small per-notification ID, used for statsd logging. */ + val instanceId: Int?, + /** If this notification is the group summary for a group of notifications. */ + val isGroupSummary: Boolean, + /** Indicates in which section the notification is displayed in. @see [PriorityBucket]. */ + @PriorityBucket val bucket: Int, ) : ActiveNotificationEntryModel() /** Model for a group of notifications. */ diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java index d9c51089d5f8..c527bb537f19 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/AmbientState.java @@ -26,9 +26,9 @@ import android.util.MathUtils; import androidx.annotation.VisibleForTesting; import com.android.systemui.Dumpable; -import com.android.systemui.res.R; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dump.DumpManager; +import com.android.systemui.res.R; import com.android.systemui.shade.transition.LargeScreenShadeInterpolator; import com.android.systemui.statusbar.NotificationShelf; import com.android.systemui.statusbar.StatusBarState; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/DisplaySwitchNotificationsHiderTracker.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/DisplaySwitchNotificationsHiderTracker.kt new file mode 100644 index 000000000000..a1fb98388f14 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/DisplaySwitchNotificationsHiderTracker.kt @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.notification.stack + +import com.android.internal.util.LatencyTracker +import com.android.internal.util.LatencyTracker.ACTION_NOTIFICATIONS_HIDDEN_FOR_MEASURE +import com.android.internal.util.LatencyTracker.ACTION_NOTIFICATIONS_HIDDEN_FOR_MEASURE_WITH_SHADE_OPEN +import com.android.systemui.shade.domain.interactor.ShadeInteractor +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged +import javax.inject.Inject + +/** + * Tracks latencies related to temporary hiding notifications while measuring + * them, which is an optimization to show some content as early as possible + * and perform notifications measurement later. + * See [HideNotificationsInteractor]. + */ +class DisplaySwitchNotificationsHiderTracker @Inject constructor( + private val notificationsInteractor: ShadeInteractor, + private val latencyTracker: LatencyTracker +) { + + suspend fun trackNotificationHideTime(shouldHideNotifications: Flow<Boolean>) { + shouldHideNotifications + .collect { shouldHide -> + if (shouldHide) { + latencyTracker.onActionStart(ACTION_NOTIFICATIONS_HIDDEN_FOR_MEASURE) + } else { + latencyTracker.onActionEnd(ACTION_NOTIFICATIONS_HIDDEN_FOR_MEASURE) + } + } + } + + suspend fun trackNotificationHideTimeWhenVisible(shouldHideNotifications: Flow<Boolean>) { + combine(shouldHideNotifications, notificationsInteractor.isAnyExpanded) + { hidden, shadeExpanded -> hidden && shadeExpanded } + .distinctUntilChanged() + .collect { hiddenButShouldBeVisible -> + if (hiddenButShouldBeVisible) { + latencyTracker.onActionStart( + ACTION_NOTIFICATIONS_HIDDEN_FOR_MEASURE_WITH_SHADE_OPEN) + } else { + latencyTracker.onActionEnd( + ACTION_NOTIFICATIONS_HIDDEN_FOR_MEASURE_WITH_SHADE_OPEN) + } + } + } +}
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationListContainer.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationListContainer.java index 6bb957339b6f..5c9a0b939dc7 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationListContainer.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationListContainer.java @@ -23,7 +23,6 @@ import androidx.annotation.Nullable; import com.android.systemui.plugins.statusbar.NotificationSwipeActionHelper; import com.android.systemui.statusbar.notification.LaunchAnimationParameters; -import com.android.systemui.statusbar.notification.NotificationActivityStarter; import com.android.systemui.statusbar.notification.VisibilityLocationProvider; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.logging.NotificationLogger; 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 ea414d2c78d0..04db653282ac 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 @@ -94,6 +94,7 @@ import com.android.systemui.flags.RefactorFlag; import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.plugins.statusbar.NotificationSwipeActionHelper; import com.android.systemui.res.R; +import com.android.systemui.scene.shared.flag.SceneContainerFlag; import com.android.systemui.shade.TouchLogger; import com.android.systemui.statusbar.EmptyShadeView; import com.android.systemui.statusbar.NotificationShelf; @@ -113,6 +114,7 @@ import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow import com.android.systemui.statusbar.notification.row.ExpandableView; import com.android.systemui.statusbar.notification.row.StackScrollerDecorView; import com.android.systemui.statusbar.notification.shared.NotificationsImprovedHunAnimation; +import com.android.systemui.statusbar.notification.shared.NotificationsLiveDataStoreRefactor; import com.android.systemui.statusbar.phone.HeadsUpAppearanceController; import com.android.systemui.statusbar.phone.HeadsUpTouchHelper; import com.android.systemui.statusbar.phone.ScreenOffAnimationController; @@ -129,9 +131,12 @@ import java.lang.annotation.Retention; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; +import java.util.HashMap; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Set; +import java.util.concurrent.Callable; import java.util.function.BiConsumer; import java.util.function.Consumer; @@ -183,6 +188,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable private int mOverflingDistance; private float mMaxOverScroll; private boolean mIsBeingDragged; + private boolean mSendingTouchesToSceneFramework; private int mLastMotionY; private int mDownX; private int mActivePointerId = INVALID_POINTER; @@ -245,6 +251,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable */ private float mOverScrolledBottomPixels; private NotificationLogger.OnChildLocationsChangedListener mListener; + private OnNotificationLocationsChangedListener mLocationsChangedListener; private OnOverscrollTopChangedListener mOverscrollTopChangedListener; private ExpandableView.OnHeightChangedListener mOnHeightChangedListener; private Runnable mOnHeightChangedRunnable; @@ -390,6 +397,14 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable } }; + private final Callable<Map<String, Integer>> collectVisibleLocationsCallable = + new Callable<>() { + @Override + public Map<String, Integer> call() { + return collectVisibleNotificationLocations(); + } + }; + private boolean mPulsing; private boolean mScrollable; private View mForcedScroll; @@ -1242,8 +1257,21 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable } } + /** + * @param listener to be notified after the location of Notification children might have + * changed. + */ + public void setNotificationLocationsChangedListener( + @Nullable OnNotificationLocationsChangedListener listener) { + if (NotificationsLiveDataStoreRefactor.isUnexpectedlyInLegacyMode()) { + return; + } + mLocationsChangedListener = listener; + } + public void setChildLocationsChangedListener( NotificationLogger.OnChildLocationsChangedListener listener) { + NotificationsLiveDataStoreRefactor.assertInLegacyMode(); mListener = listener; } @@ -1483,7 +1511,12 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable */ public void setExpandedHeight(float height) { final boolean skipHeightUpdate = shouldSkipHeightUpdate(); - updateStackPosition(); + + // when scene framework is enabled, updateStackPosition is already called by + // updateTopPadding every time the stack moves, so skip it here to avoid flickering. + if (!SceneContainerFlag.isEnabled()) { + updateStackPosition(); + } if (!skipHeightUpdate) { mExpandedHeight = height; @@ -2424,6 +2457,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable /* notificationStackScrollLayout= */ this, mMaxDisplayedNotifications, shelfIntrinsicHeight); mIntrinsicContentHeight = height; + mController.setIntrinsicContentHeight(mIntrinsicContentHeight); // The topPadding can be bigger than the regular padding when qs is expanded, in that // state the maxPanelHeight and the contentHeight should be bigger @@ -3532,8 +3566,11 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable @Override public boolean onTouchEvent(MotionEvent ev) { - if (mTouchHandler != null && mTouchHandler.onTouchEvent(ev)) { - return true; + if (mTouchHandler != null) { + boolean touchHandled = mTouchHandler.onTouchEvent(ev); + if (SceneContainerFlag.isEnabled() || touchHandled) { + return touchHandled; + } } return super.onTouchEvent(ev); @@ -3541,6 +3578,27 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable @Override public boolean dispatchTouchEvent(MotionEvent ev) { + if (SceneContainerFlag.isEnabled() && mIsBeingDragged) { + if (!mSendingTouchesToSceneFramework) { + // if this is the first touch being sent to the scene framework, + // convert it into a synthetic DOWN event. + mSendingTouchesToSceneFramework = true; + MotionEvent downEvent = MotionEvent.obtain(ev); + downEvent.setAction(MotionEvent.ACTION_DOWN); + mController.sendTouchToSceneFramework(downEvent); + downEvent.recycle(); + } else { + mController.sendTouchToSceneFramework(ev); + } + + if ( + ev.getActionMasked() == MotionEvent.ACTION_UP + || ev.getActionMasked() == MotionEvent.ACTION_CANCEL + ) { + setIsBeingDragged(false); + } + return false; + } return TouchLogger.logDispatchTouch(TAG, ev, super.dispatchTouchEvent(ev)); } @@ -3607,6 +3665,18 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable return true; } + // If the scene framework is enabled, ignore all non-move gestures if we are currently + // dragging - they should be dispatched to the scene framework. Move gestures should be let + // through to determine if we are still dragging or not. + if ( + SceneContainerFlag.isEnabled() + && mIsBeingDragged + && action != MotionEvent.ACTION_MOVE + ) { + setIsBeingDragged(false); + return false; + } + switch (action) { case MotionEvent.ACTION_DOWN: { if (getChildCount() == 0 || !isInContentBounds(ev)) { @@ -3650,6 +3720,11 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable } } if (mIsBeingDragged) { + // Defer actual scrolling to the scene framework if enabled + if (SceneContainerFlag.isEnabled()) { + setIsBeingDragged(false); + return false; + } // Scroll to follow the motion event mLastMotionY = y; float scrollAmount; @@ -3744,9 +3819,8 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable } protected boolean isInsideQsHeader(MotionEvent ev) { - if (mQsHeader == null) { - Log.wtf(TAG, "qsHeader is null while NSSL is handling a touch"); - return false; + if (SceneContainerFlag.isEnabled()) { + return ev.getY() < mController.getPlaceholderTop(); } mQsHeader.getBoundsOnScreen(mQsHeaderBound); @@ -4000,9 +4074,16 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable requestDisallowInterceptTouchEvent(true); cancelLongPress(); resetExposedMenuView(true /* animate */, true /* force */); + } else { + mSendingTouchesToSceneFramework = false; } } + @VisibleForTesting + boolean getIsBeingDragged() { + return mIsBeingDragged; + } + public void requestDisallowLongPress() { cancelLongPress(); } @@ -4398,15 +4479,40 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable child.applyViewState(); } - if (mListener != null) { - mListener.onChildLocationsChanged(); + if (NotificationsLiveDataStoreRefactor.isEnabled()) { + if (mLocationsChangedListener != null) { + mLocationsChangedListener.onChildLocationsChanged(collectVisibleLocationsCallable); + } + } else { + if (mListener != null) { + mListener.onChildLocationsChanged(); + } } + runAnimationFinishedRunnables(); setAnimationRunning(false); updateBackground(); updateViewShadows(); } + /** + * Retrieves a map of visible [{@link ExpandableViewState#location}]s of the actively displayed + * Notification children associated by their Notification keys. + * Locations are collected recursively including locations from the child views of Notification + * Groups, that are visible. + */ + private Map<String, Integer> collectVisibleNotificationLocations() { + Map<String, Integer> visibilities = new HashMap<>(); + int numChildren = getChildCount(); + for (int i = 0; i < numChildren; i++) { + ExpandableView child = getChildAtIndex(i); + if (child instanceof ExpandableNotificationRow row) { + row.collectVisibleLocations(visibilities); + } + } + return visibilities; + } + private void updateViewShadows() { // we need to work around an issue where the shadow would not cast between siblings when // their z difference is between 0 and 0.1 @@ -5901,7 +6007,11 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable } } - protected void setLogger(StackStateLogger logger) { + /** + * Sets a {@link StackStateLogger} which is notified as the {@link StackStateAnimator} updates + * the views. + */ + protected void setStackStateLogger(StackStateLogger logger) { mStateAnimator.setLogger(logger); } @@ -5938,6 +6048,18 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable void flingTopOverscroll(float velocity, boolean open); } + /** + * A listener that is notified when some ExpandableNotificationRow locations might have changed. + */ + public interface OnNotificationLocationsChangedListener { + /** + * Called when the location of ExpandableNotificationRows might have changed. + * + * @param locations mapping of Notification keys to locations. + */ + void onChildLocationsChanged(Callable<Map<String, Integer>> locations); + } + private void updateSpeedBumpIndex() { mSpeedBumpIndexDirty = true; } 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 a30c29456b3b..6a66bb74f16d 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 @@ -80,6 +80,9 @@ import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin.OnMenuEv import com.android.systemui.plugins.statusbar.NotificationSwipeActionHelper; 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.scene.shared.flag.SceneContainerFlags; +import com.android.systemui.scene.ui.view.WindowRootView; import com.android.systemui.shade.ShadeController; import com.android.systemui.shade.ShadeViewController; import com.android.systemui.statusbar.CommandQueue; @@ -120,6 +123,7 @@ import com.android.systemui.statusbar.notification.row.ExpandableView; import com.android.systemui.statusbar.notification.row.NotificationGuts; import com.android.systemui.statusbar.notification.row.NotificationGutsManager; import com.android.systemui.statusbar.notification.row.NotificationSnooze; +import com.android.systemui.statusbar.notification.stack.domain.interactor.NotificationStackAppearanceInteractor; import com.android.systemui.statusbar.notification.stack.ui.viewbinder.NotificationListViewBinder; import com.android.systemui.statusbar.phone.HeadsUpAppearanceController; import com.android.systemui.statusbar.phone.HeadsUpTouchHelper; @@ -145,6 +149,7 @@ import java.util.function.Consumer; import javax.inject.Inject; import javax.inject.Named; +import javax.inject.Provider; /** * Controller for {@link NotificationStackScrollLayout}. @@ -181,6 +186,8 @@ public class NotificationStackScrollLayoutController implements Dumpable { private final NotificationRemoteInputManager mRemoteInputManager; private final VisibilityLocationProviderDelegator mVisibilityLocationProviderDelegator; private final ShadeController mShadeController; + private final Provider<WindowRootView> mWindowRootView; + private final NotificationStackAppearanceInteractor mStackAppearanceInteractor; private final KeyguardMediaController mKeyguardMediaController; private final SysuiStatusBarStateController mStatusBarStateController; private final KeyguardBypassController mKeyguardBypassController; @@ -689,6 +696,9 @@ public class NotificationStackScrollLayoutController implements Dumpable { SeenNotificationsInteractor seenNotificationsInteractor, NotificationListViewBinder viewBinder, ShadeController shadeController, + SceneContainerFlags sceneContainerFlags, + Provider<WindowRootView> windowRootView, + NotificationStackAppearanceInteractor stackAppearanceInteractor, InteractionJankMonitor jankMonitor, StackStateLogger stackLogger, NotificationStackScrollLogger logger, @@ -739,6 +749,8 @@ public class NotificationStackScrollLayoutController implements Dumpable { mVisibilityLocationProviderDelegator = visibilityLocationProviderDelegator; mSeenNotificationsInteractor = seenNotificationsInteractor; mShadeController = shadeController; + mWindowRootView = windowRootView; + mStackAppearanceInteractor = stackAppearanceInteractor; mFeatureFlags = featureFlags; mNotificationTargetsHelper = notificationTargetsHelper; mSecureSettings = secureSettings; @@ -751,7 +763,7 @@ public class NotificationStackScrollLayoutController implements Dumpable { } private void setUpView() { - mView.setLogger(mStackStateLogger); + mView.setStackStateLogger(mStackStateLogger); mView.setController(this); mView.setLogger(mLogger); mView.setTouchHandler(new TouchHandler()); @@ -1076,6 +1088,28 @@ public class NotificationStackScrollLayoutController implements Dumpable { return mView.getIntrinsicContentHeight(); } + /** + * Dispatch a touch to the scene container framework. + * TODO(b/316965302): Replace findViewById to avoid DFS + */ + public void sendTouchToSceneFramework(MotionEvent ev) { + View sceneContainer = mWindowRootView.get() + .findViewById(R.id.scene_container_root_composable); + if (sceneContainer != null) { + sceneContainer.dispatchTouchEvent(ev); + } + } + + /** Get the y-coordinate of the top bound of the stack. */ + public float getPlaceholderTop() { + return mStackAppearanceInteractor.getStackBounds().getValue().getTop(); + } + + /** Set the intrinsic height of the stack content without additional padding. */ + public void setIntrinsicContentHeight(float intrinsicContentHeight) { + mStackAppearanceInteractor.setIntrinsicContentHeight(intrinsicContentHeight); + } + public void setIntrinsicPadding(int intrinsicPadding) { mView.setIntrinsicPadding(intrinsicPadding); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/data/repository/NotificationStackAppearanceRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/data/repository/NotificationStackAppearanceRepository.kt index e78a694735e0..aac3c28a3426 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/data/repository/NotificationStackAppearanceRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/data/repository/NotificationStackAppearanceRepository.kt @@ -30,4 +30,18 @@ class NotificationStackAppearanceRepository @Inject constructor() { /** The corner radius of the notification stack, in dp. */ val cornerRadiusDp = MutableStateFlow(32f) + + /** + * The height in px of the contents of notification stack. Depending on the number of + * notifications, this can exceed the space available on screen to show notifications, at which + * point the notification stack should become scrollable. + */ + val intrinsicContentHeight = MutableStateFlow(0f) + + /** + * The y-coordinate in px of top of the contents of the notification stack. This value can be + * negative, if the stack is scrolled such that its top extends beyond the top edge of the + * screen. + */ + val contentTop = MutableStateFlow(0f) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractor.kt index 61a4dfcbd201..1dfde09f3a85 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractor.kt @@ -34,12 +34,32 @@ constructor( /** The bounds of the notification stack in the current scene. */ val stackBounds: StateFlow<NotificationContainerBounds> = repository.stackBounds.asStateFlow() + /** The corner radius of the notification stack, in dp. */ + val cornerRadiusDp: StateFlow<Float> = repository.cornerRadiusDp.asStateFlow() + + /** + * The height in px of the contents of notification stack. Depending on the number of + * notifications, this can exceed the space available on screen to show notifications, at which + * point the notification stack should become scrollable. + */ + val intrinsicContentHeight = repository.intrinsicContentHeight.asStateFlow() + + /** The y-coordinate in px of top of the contents of the notification stack. */ + val contentTop = repository.contentTop.asStateFlow() + /** Sets the position of the notification stack in the current scene. */ fun setStackBounds(bounds: NotificationContainerBounds) { check(bounds.top <= bounds.bottom) { "Invalid bounds: $bounds" } repository.stackBounds.value = bounds } - /** The corner radius of the notification stack, in dp. */ - val cornerRadiusDp: StateFlow<Float> = repository.cornerRadiusDp.asStateFlow() + /** Sets the height of the contents of the notification stack. */ + fun setIntrinsicContentHeight(height: Float) { + repository.intrinsicContentHeight.value = height + } + + /** Sets the y-coord in px of the top of the contents of the notification stack. */ + fun setContentTop(startY: Float) { + repository.contentTop.value = startY + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationRowStatsLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationRowStatsLogger.kt new file mode 100644 index 000000000000..2305c7e880bf --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationRowStatsLogger.kt @@ -0,0 +1,26 @@ +/* + * 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.stack.ui.view + +interface NotificationRowStatsLogger { + fun onNotificationExpansionChanged( + key: String, + isExpanded: Boolean, + location: Int, + isUserAction: Boolean + ) +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationStatsLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationStatsLogger.kt new file mode 100644 index 000000000000..54186165c54a --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationStatsLogger.kt @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.notification.stack.ui.view + +import com.android.systemui.statusbar.notification.shared.ActiveNotificationModel +import java.util.concurrent.Callable + +/** + * Logs UI events of Notifications, in particular, logging which Notifications are visible and which + * are not. + */ +interface NotificationStatsLogger : NotificationRowStatsLogger { + fun onLockscreenOrShadeInteractive( + isOnLockScreen: Boolean, + activeNotifications: List<ActiveNotificationModel>, + ) + fun onLockscreenOrShadeNotInteractive(activeNotifications: List<ActiveNotificationModel>) + fun onNotificationRemoved(key: String) + fun onNotificationUpdated(key: String) + fun onNotificationListUpdated( + locationsProvider: Callable<Map<String, Int>>, + notificationRanks: Map<String, Int>, + ) + override fun onNotificationExpansionChanged( + key: String, + isExpanded: Boolean, + location: Int, + isUserAction: Boolean + ) +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationStatsLoggerImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationStatsLoggerImpl.kt new file mode 100644 index 000000000000..0cb00bc8d01d --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationStatsLoggerImpl.kt @@ -0,0 +1,326 @@ +/* + * 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.stack.ui.view + +import android.service.notification.NotificationListenerService +import androidx.annotation.VisibleForTesting +import com.android.internal.statusbar.IStatusBarService +import com.android.internal.statusbar.NotificationVisibility +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.statusbar.notification.logging.NotificationPanelLogger +import com.android.systemui.statusbar.notification.logging.nano.Notifications +import com.android.systemui.statusbar.notification.shared.ActiveNotificationModel +import com.android.systemui.statusbar.notification.stack.ExpandableViewState +import java.util.concurrent.Callable +import java.util.concurrent.ConcurrentHashMap +import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +@VisibleForTesting const val UNKNOWN_RANK = -1 + +@SysUISingleton +class NotificationStatsLoggerImpl +@Inject +constructor( + @Application private val applicationScope: CoroutineScope, + @Background private val bgDispatcher: CoroutineDispatcher, + private val notificationListenerService: NotificationListenerService, + private val notificationPanelLogger: NotificationPanelLogger, + private val statusBarService: IStatusBarService, +) : NotificationStatsLogger { + private val lastLoggedVisibilities = mutableMapOf<String, VisibilityState>() + private var logVisibilitiesJob: Job? = null + + private val expansionStates: MutableMap<String, ExpansionState> = + ConcurrentHashMap<String, ExpansionState>() + private val lastReportedExpansionValues: MutableMap<String, Boolean> = + ConcurrentHashMap<String, Boolean>() + + override fun onNotificationListUpdated( + locationsProvider: Callable<Map<String, Int>>, + notificationRanks: Map<String, Int>, + ) { + if (logVisibilitiesJob?.isActive == true) { + return + } + + logVisibilitiesJob = + startLogVisibilitiesJob( + newVisibilities = + combine( + visibilities = locationsProvider.call(), + rankingsMap = notificationRanks + ), + activeNotifCount = notificationRanks.size, + ) + } + + override fun onNotificationExpansionChanged( + key: String, + isExpanded: Boolean, + location: Int, + isUserAction: Boolean, + ) { + val expansionState = + ExpansionState( + key = key, + isExpanded = isExpanded, + isUserAction = isUserAction, + location = location, + ) + expansionStates[key] = expansionState + maybeLogNotificationExpansionChange(expansionState) + } + + private fun maybeLogNotificationExpansionChange(expansionState: ExpansionState) { + if (expansionState.visible.not()) { + // Only log visible expansion changes + return + } + + val loggedExpansionValue: Boolean? = lastReportedExpansionValues[expansionState.key] + if (loggedExpansionValue == null && !expansionState.isExpanded) { + // Consider the Notification initially collapsed, so only expanded is logged in the + // first time. + return + } + + if (loggedExpansionValue != null && loggedExpansionValue == expansionState.isExpanded) { + // We have already logged this state, don't log it again + return + } + + logNotificationExpansionChange(expansionState) + lastReportedExpansionValues[expansionState.key] = expansionState.isExpanded + } + + private fun logNotificationExpansionChange(expansionState: ExpansionState) = + applicationScope.launch { + withContext(bgDispatcher) { + statusBarService.onNotificationExpansionChanged( + /* key = */ expansionState.key, + /* userAction = */ expansionState.isUserAction, + /* expanded = */ expansionState.isExpanded, + /* notificationLocation = */ expansionState.location + .toNotificationLocation() + .ordinal + ) + } + } + + override fun onLockscreenOrShadeInteractive( + isOnLockScreen: Boolean, + activeNotifications: List<ActiveNotificationModel>, + ) { + applicationScope.launch { + withContext(bgDispatcher) { + notificationPanelLogger.logPanelShown( + isOnLockScreen, + activeNotifications.toNotificationProto() + ) + } + } + } + + override fun onLockscreenOrShadeNotInteractive( + activeNotifications: List<ActiveNotificationModel> + ) { + logVisibilitiesJob = + startLogVisibilitiesJob( + newVisibilities = emptyMap(), + activeNotifCount = activeNotifications.size + ) + } + + // TODO(b/308623704) wire this in with NotifPipeline updates + override fun onNotificationRemoved(key: String) { + // No need to track expansion states for Notifications that are removed. + expansionStates.remove(key) + lastReportedExpansionValues.remove(key) + } + + // TODO(b/308623704) wire this in with NotifPipeline updates + override fun onNotificationUpdated(key: String) { + // When the Notification is updated, we should consider it as not yet logged. + lastReportedExpansionValues.remove(key) + } + + private fun combine( + visibilities: Map<String, Int>, + rankingsMap: Map<String, Int> + ): Map<String, VisibilityState> = + visibilities.mapValues { entry -> + VisibilityState(entry.key, entry.value, rankingsMap[entry.key] ?: UNKNOWN_RANK) + } + + private fun startLogVisibilitiesJob( + newVisibilities: Map<String, VisibilityState>, + activeNotifCount: Int, + ) = + applicationScope.launch { + val newlyVisible = newVisibilities - lastLoggedVisibilities.keys + val noLongerVisible = lastLoggedVisibilities - newVisibilities.keys + + maybeLogVisibilityChanges(newlyVisible, noLongerVisible, activeNotifCount) + updateExpansionStates(newlyVisible, noLongerVisible) + + lastLoggedVisibilities.clear() + lastLoggedVisibilities.putAll(newVisibilities) + } + + private suspend fun maybeLogVisibilityChanges( + newlyVisible: Map<String, VisibilityState>, + noLongerVisible: Map<String, VisibilityState>, + activeNotifCount: Int, + ) { + if (newlyVisible.isEmpty() && noLongerVisible.isEmpty()) { + return + } + + val newlyVisibleAr = + newlyVisible.mapToNotificationVisibilitiesAr(visible = true, count = activeNotifCount) + + val noLongerVisibleAr = + noLongerVisible.mapToNotificationVisibilitiesAr( + visible = false, + count = activeNotifCount + ) + + withContext(bgDispatcher) { + statusBarService.onNotificationVisibilityChanged(newlyVisibleAr, noLongerVisibleAr) + if (newlyVisible.isNotEmpty()) { + notificationListenerService.setNotificationsShown(newlyVisible.keys.toTypedArray()) + } + } + } + + private fun updateExpansionStates( + newlyVisible: Map<String, VisibilityState>, + noLongerVisible: Map<String, VisibilityState> + ) { + expansionStates.forEach { (key, expansionState) -> + if (newlyVisible.contains(key)) { + val newState = + expansionState.copy( + visible = true, + location = newlyVisible.getValue(key).location, + ) + expansionStates[key] = newState + maybeLogNotificationExpansionChange(newState) + } + + if (noLongerVisible.contains(key)) { + expansionStates[key] = + expansionState.copy( + visible = false, + location = noLongerVisible.getValue(key).location, + ) + } + } + } + + private data class VisibilityState( + val key: String, + val location: Int, + val rank: Int, + ) + + private data class ExpansionState( + val key: String, + val isUserAction: Boolean, + val isExpanded: Boolean, + val visible: Boolean, + val location: Int, + ) { + constructor( + key: String, + isExpanded: Boolean, + location: Int, + isUserAction: Boolean, + ) : this( + key = key, + isExpanded = isExpanded, + isUserAction = isUserAction, + visible = isVisibleLocation(location), + location = location, + ) + } + + private fun Map<String, VisibilityState>.mapToNotificationVisibilitiesAr( + visible: Boolean, + count: Int, + ): Array<NotificationVisibility> = + this.map { (key, state) -> + NotificationVisibility.obtain( + /* key = */ key, + /* rank = */ state.rank, + /* count = */ count, + /* visible = */ visible, + /* location = */ state.location.toNotificationLocation() + ) + } + .toTypedArray() +} + +private fun Int.toNotificationLocation(): NotificationVisibility.NotificationLocation { + return when (this) { + ExpandableViewState.LOCATION_FIRST_HUN -> + NotificationVisibility.NotificationLocation.LOCATION_FIRST_HEADS_UP + ExpandableViewState.LOCATION_HIDDEN_TOP -> + NotificationVisibility.NotificationLocation.LOCATION_HIDDEN_TOP + ExpandableViewState.LOCATION_MAIN_AREA -> + NotificationVisibility.NotificationLocation.LOCATION_MAIN_AREA + ExpandableViewState.LOCATION_BOTTOM_STACK_PEEKING -> + NotificationVisibility.NotificationLocation.LOCATION_BOTTOM_STACK_PEEKING + ExpandableViewState.LOCATION_BOTTOM_STACK_HIDDEN -> + NotificationVisibility.NotificationLocation.LOCATION_BOTTOM_STACK_HIDDEN + ExpandableViewState.LOCATION_GONE -> + NotificationVisibility.NotificationLocation.LOCATION_GONE + else -> NotificationVisibility.NotificationLocation.LOCATION_UNKNOWN + } +} + +private fun List<ActiveNotificationModel>.toNotificationProto(): Notifications.NotificationList { + val notificationList = Notifications.NotificationList() + val protoArray: Array<Notifications.Notification> = + map { notification -> + Notifications.Notification().apply { + uid = notification.uid + packageName = notification.packageName + notification.instanceId?.let { instanceId = it } + // TODO(b/308623704) check if we can set groupInstanceId as well + isGroupSummary = notification.isGroupSummary + section = NotificationPanelLogger.toNotificationSection(notification.bucket) + } + } + .toTypedArray() + + if (protoArray.isNotEmpty()) { + notificationList.notifications = protoArray + } + + return notificationList +} + +private fun isVisibleLocation(location: Int): Boolean = + location and ExpandableViewState.VISIBLE_LOCATIONS != 0 diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/HideNotificationsBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/HideNotificationsBinder.kt index 910b40f594f6..c2bc9ba83293 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/HideNotificationsBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/HideNotificationsBinder.kt @@ -16,22 +16,36 @@ package com.android.systemui.statusbar.notification.stack.ui.viewbinder import androidx.core.view.doOnDetach +import com.android.systemui.statusbar.notification.stack.DisplaySwitchNotificationsHiderTracker import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationListViewModel +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.SharingStarted.Companion.Lazily +import kotlinx.coroutines.flow.shareIn +import kotlinx.coroutines.launch /** * Binds a [NotificationStackScrollLayoutController] to its [view model][NotificationListViewModel]. */ object HideNotificationsBinder { - suspend fun bindHideList( + fun CoroutineScope.bindHideList( viewController: NotificationStackScrollLayoutController, - viewModel: NotificationListViewModel + viewModel: NotificationListViewModel, + hiderTracker: DisplaySwitchNotificationsHiderTracker ) { viewController.view.doOnDetach { viewController.bindHideState(shouldHide = false) } - viewModel.hideListViewModel.shouldHideListForPerformance.collect { shouldHide -> - viewController.bindHideState(shouldHide) + val hideListFlow = viewModel.hideListViewModel.shouldHideListForPerformance + .shareIn(this, started = Lazily) + + launch { + hideListFlow.collect { shouldHide -> + viewController.bindHideState(shouldHide) + } } + + launch { hiderTracker.trackNotificationHideTime(hideListFlow) } + launch { hiderTracker.trackNotificationHideTimeWhenVisible(hideListFlow) } } private fun NotificationStackScrollLayoutController.bindHideState(shouldHide: Boolean) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt index 1b3666078b27..44a7e7e27455 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt @@ -33,13 +33,17 @@ import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefac import com.android.systemui.statusbar.notification.footer.ui.view.FooterView import com.android.systemui.statusbar.notification.footer.ui.viewbinder.FooterViewBinder import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerShelfViewBinder +import com.android.systemui.statusbar.notification.shared.NotificationsLiveDataStoreRefactor import com.android.systemui.statusbar.notification.shelf.ui.viewbinder.NotificationShelfViewBinder +import com.android.systemui.statusbar.notification.stack.DisplaySwitchNotificationsHiderTracker import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController +import com.android.systemui.statusbar.notification.stack.ui.view.NotificationStatsLogger import com.android.systemui.statusbar.notification.stack.ui.viewbinder.HideNotificationsBinder.bindHideList import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationListViewModel import com.android.systemui.statusbar.phone.NotificationIconAreaController import com.android.systemui.util.kotlin.getOrNull +import java.util.Optional import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.combine @@ -49,13 +53,15 @@ import kotlinx.coroutines.launch class NotificationListViewBinder @Inject constructor( - private val viewModel: NotificationListViewModel, @Background private val backgroundDispatcher: CoroutineDispatcher, + private val hiderTracker: DisplaySwitchNotificationsHiderTracker, private val configuration: ConfigurationState, private val falsingManager: FalsingManager, private val iconAreaController: NotificationIconAreaController, private val metricsLogger: MetricsLogger, private val nicBinder: NotificationIconContainerShelfViewBinder, + private val loggerOptional: Optional<NotificationStatsLogger>, + private val viewModel: NotificationListViewModel, ) { fun bindWhileAttached( @@ -70,15 +76,20 @@ constructor( view.repeatWhenAttached { lifecycleScope.launch { launch { bindShelf(shelf) } - launch { bindHideList(viewController, viewModel) } + bindHideList(viewController, viewModel, hiderTracker) if (FooterViewRefactor.isEnabled) { launch { bindFooter(view) } launch { bindEmptyShade(view) } - viewModel.isImportantForAccessibility.collect { isImportantForAccessibility -> - view.setImportantForAccessibilityYesNo(isImportantForAccessibility) + launch { + viewModel.isImportantForAccessibility.collect { isImportantForAccessibility + -> + view.setImportantForAccessibilityYesNo(isImportantForAccessibility) + } } } + + launch { bindLogger(view) } } } } @@ -136,4 +147,18 @@ constructor( ) } } + + private suspend fun bindLogger(view: NotificationStackScrollLayout) { + if (NotificationsLiveDataStoreRefactor.isEnabled) { + viewModel.logger.getOrNull()?.let { viewModel -> + loggerOptional.getOrNull()?.let { logger -> + NotificationStatsLoggerBinder.bindLogger( + view, + logger, + viewModel, + ) + } + } + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationStackAppearanceViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationStackAppearanceViewBinder.kt index a9b542dcce2d..ed15f557fb39 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationStackAppearanceViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationStackAppearanceViewBinder.kt @@ -25,6 +25,7 @@ import com.android.systemui.statusbar.notification.stack.AmbientState import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController import com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificationContainer import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationStackAppearanceViewModel +import kotlin.math.pow import kotlin.math.roundToInt import kotlinx.coroutines.launch @@ -43,24 +44,28 @@ object NotificationStackAppearanceViewBinder { repeatOnLifecycle(Lifecycle.State.CREATED) { launch { viewModel.stackBounds.collect { bounds -> - controller.updateTopPadding( - bounds.top, - controller.isAddOrRemoveAnimationPending - ) controller.setRoundedClippingBounds( - it.left, - it.top, - it.right, - it.bottom, + bounds.left.roundToInt(), + bounds.top.roundToInt(), + bounds.right.roundToInt(), + bounds.bottom.roundToInt(), viewModel.cornerRadiusDp.value.dpToPx(context), viewModel.cornerRadiusDp.value.dpToPx(context), ) } } + + launch { + viewModel.contentTop.collect { + controller.updateTopPadding(it, controller.isAddOrRemoveAnimationPending) + } + } + launch { viewModel.expandFraction.collect { expandFraction -> ambientState.expansionFraction = expandFraction controller.expandedHeight = expandFraction * controller.view.height + controller.setMaxAlphaForExpansion(expandFraction.pow(0.75f)) } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationStatsLoggerBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationStatsLoggerBinder.kt new file mode 100644 index 000000000000..a05ad6e45991 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationStatsLoggerBinder.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.statusbar.notification.stack.ui.viewbinder + +import com.android.systemui.common.coroutine.ConflatedCallbackFlow +import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout +import com.android.systemui.statusbar.notification.stack.ui.view.NotificationStatsLogger +import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationLoggerViewModel +import com.android.systemui.util.kotlin.Utils +import com.android.systemui.util.kotlin.sample +import com.android.systemui.util.kotlin.throttle +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.combine + +/** + * Binds a [NotificationStatsLogger] to its [NotificationLoggerViewModel], and wires in + * [NotificationStackScrollLayout.OnNotificationLocationsChangedListener] updates to it. + */ +object NotificationStatsLoggerBinder { + + /** minimum delay in ms between Notification location updates */ + private const val NOTIFICATION_UPDATE_PERIOD_MS = 500L + + suspend fun bindLogger( + view: NotificationStackScrollLayout, + logger: NotificationStatsLogger, + viewModel: NotificationLoggerViewModel, + ) { + viewModel.isLockscreenOrShadeInteractive + .sample( + combine(viewModel.isOnLockScreen, viewModel.activeNotifications, ::Pair), + Utils.Companion::toTriple + ) + .collectLatest { (isPanelInteractive, isOnLockScreen, notifications) -> + if (isPanelInteractive) { + logger.onLockscreenOrShadeInteractive( + isOnLockScreen = isOnLockScreen, + activeNotifications = notifications, + ) + view.onNotificationsUpdated + // Delay the updates with [NOTIFICATION_UPDATES_PERIOD_MS]. If the original + // flow emits more than once during this period, only the latest value is + // emitted, meaning that we won't log the intermediate Notification states. + .throttle(NOTIFICATION_UPDATE_PERIOD_MS) + .sample(viewModel.activeNotificationRanks, ::Pair) + .collect { (locationsProvider, notificationRanks) -> + logger.onNotificationListUpdated(locationsProvider, notificationRanks) + } + } else { + logger.onLockscreenOrShadeNotInteractive( + activeNotifications = notifications, + ) + } + } + } +} + +private val NotificationStackScrollLayout.onNotificationsUpdated + get() = + ConflatedCallbackFlow.conflatedCallbackFlow { + val callback = + NotificationStackScrollLayout.OnNotificationLocationsChangedListener { callable -> + trySend(callable) + } + setNotificationLocationsChangedListener(callback) + awaitClose { setNotificationLocationsChangedListener(null) } + } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModel.kt index 569ae248ad23..86c0a67868e1 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModel.kt @@ -40,6 +40,7 @@ constructor( val shelf: NotificationShelfViewModel, val hideListViewModel: HideListViewModel, val footer: Optional<FooterViewModel>, + val logger: Optional<NotificationLoggerViewModel>, activeNotificationsInteractor: ActiveNotificationsInteractor, keyguardTransitionInteractor: KeyguardTransitionInteractor, seenNotificationsInteractor: SeenNotificationsInteractor, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationLoggerViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationLoggerViewModel.kt new file mode 100644 index 000000000000..0901a7f817eb --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationLoggerViewModel.kt @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.notification.stack.ui.viewmodel + +import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor +import com.android.systemui.scene.domain.interactor.WindowRootViewVisibilityInteractor +import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor +import com.android.systemui.statusbar.notification.shared.ActiveNotificationModel +import javax.inject.Inject +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +class NotificationLoggerViewModel +@Inject +constructor( + activeNotificationsInteractor: ActiveNotificationsInteractor, + keyguardInteractor: KeyguardInteractor, + windowRootViewVisibilityInteractor: WindowRootViewVisibilityInteractor, +) { + val activeNotifications: Flow<List<ActiveNotificationModel>> = + activeNotificationsInteractor.allRepresentativeNotifications.map { it.values.toList() } + + val activeNotificationRanks: Flow<Map<String, Int>> = + activeNotificationsInteractor.activeNotificationRanks + + val isLockscreenOrShadeInteractive: Flow<Boolean> = + windowRootViewVisibilityInteractor.isLockscreenOrShadeVisibleAndInteractive + + val isOnLockScreen: Flow<Boolean> = keyguardInteractor.isKeyguardShowing +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationStackAppearanceViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationStackAppearanceViewModel.kt index 834d3ffe63c9..74db5831f7f8 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationStackAppearanceViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationStackAppearanceViewModel.kt @@ -41,4 +41,7 @@ constructor( /** The corner radius of the notification stack, in dp. */ val cornerRadiusDp: StateFlow<Float> = stackAppearanceInteractor.cornerRadiusDp + + /** The y-coordinate in px of top of the contents of the notification stack. */ + val contentTop: StateFlow<Float> = stackAppearanceInteractor.contentTop } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt index 9f22118e3332..385f0619288d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt @@ -21,9 +21,11 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.flags.FeatureFlagsClassic import com.android.systemui.flags.Flags import com.android.systemui.scene.shared.flag.SceneContainerFlags +import com.android.systemui.shade.domain.interactor.ShadeInteractor import com.android.systemui.statusbar.notification.stack.domain.interactor.NotificationStackAppearanceInteractor import com.android.systemui.statusbar.notification.stack.shared.flexiNotifsEnabled import javax.inject.Inject +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow /** @@ -35,6 +37,7 @@ class NotificationsPlaceholderViewModel @Inject constructor( private val interactor: NotificationStackAppearanceInteractor, + shadeInteractor: ShadeInteractor, flags: SceneContainerFlags, featureFlags: FeatureFlagsClassic, ) { @@ -66,4 +69,22 @@ constructor( /** The corner radius of the placeholder, in dp. */ val cornerRadiusDp: StateFlow<Float> = interactor.cornerRadiusDp + + /** + * The height in px of the contents of notification stack. Depending on the number of + * notifications, this can exceed the space available on screen to show notifications, at which + * point the notification stack should become scrollable. + */ + val intrinsicContentHeight = interactor.intrinsicContentHeight + + /** + * The amount [0-1] that the shade has been opened. At 0, the shade is closed; at 1, the shade + * is open. + */ + val expandFraction: Flow<Float> = shadeInteractor.shadeExpansion + + /** Sets the y-coord in px of the top of the contents of the notification stack. */ + fun onContentTopChanged(padding: Float) { + interactor.setContentTop(padding) + } } 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 5ee38bebc99b..a48fb45861d2 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 @@ -240,13 +240,13 @@ constructor( */ val translationY: Flow<Float> = combine( - isOnLockscreen, + isOnLockscreenWithoutShade, merge( keyguardInteractor.keyguardTranslationY, occludedToLockscreenTransitionViewModel.lockscreenTranslationY, ) - ) { isOnLockscreen, translationY -> - if (isOnLockscreen) { + ) { isOnLockscreenWithoutShade, translationY -> + if (isOnLockscreenWithoutShade) { translationY } else { 0f diff --git a/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerBaseTest.java index 3d7d701ee5d6..da6bfe84eee7 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerBaseTest.java +++ b/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerBaseTest.java @@ -48,10 +48,10 @@ import com.android.systemui.dump.DumpManager; import com.android.systemui.flags.FakeFeatureFlags; import com.android.systemui.keyguard.domain.interactor.KeyguardInteractorFactory; import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractorFactory; +import com.android.systemui.kosmos.KosmosJavaAdapter; import com.android.systemui.plugins.FalsingManager; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.res.R; -import com.android.systemui.scene.SceneTestUtils; import com.android.systemui.statusbar.StatusBarState; import com.android.systemui.statusbar.VibratorHelper; import com.android.systemui.statusbar.policy.ConfigurationController; @@ -76,7 +76,7 @@ public class LockIconViewControllerBaseTest extends SysuiTestCase { protected MockitoSession mStaticMockSession; - protected final SceneTestUtils mSceneTestUtils = new SceneTestUtils(this); + protected final KosmosJavaAdapter mKosmos = new KosmosJavaAdapter(this); protected @Mock DeviceEntryInteractor mDeviceEntryInteractor; protected @Mock LockIconView mLockIconView; protected @Mock AnimatedStateListDrawable mIconDrawable; @@ -175,7 +175,7 @@ public class LockIconViewControllerBaseTest extends SysuiTestCase { mPrimaryBouncerInteractor, mContext, () -> mDeviceEntryInteractor, - mSceneTestUtils.getSceneContainerFlags() + mKosmos.getFakeSceneContainerFlags() ); } diff --git a/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerTest.java index 93a5393b41cf..b0887efed4d7 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerTest.java @@ -373,7 +373,7 @@ public class LockIconViewControllerTest extends LockIconViewControllerBaseTest { @Test public void longPress_showBouncer_sceneContainerNotEnabled() { init(/* useMigrationFlag= */ false); - mSceneTestUtils.getSceneContainerFlags().setEnabled(false); + mKosmos.getFakeSceneContainerFlags().setEnabled(false); when(mFalsingManager.isFalseLongTap(anyInt())).thenReturn(false); // WHEN longPress @@ -387,7 +387,7 @@ public class LockIconViewControllerTest extends LockIconViewControllerBaseTest { @Test public void longPress_showBouncer() { init(/* useMigrationFlag= */ false); - mSceneTestUtils.getSceneContainerFlags().setEnabled(true); + mKosmos.getFakeSceneContainerFlags().setEnabled(true); when(mFalsingManager.isFalseLongTap(anyInt())).thenReturn(false); // WHEN longPress @@ -401,7 +401,7 @@ public class LockIconViewControllerTest extends LockIconViewControllerBaseTest { @Test public void longPress_falsingTriggered_doesNotShowBouncer() { init(/* useMigrationFlag= */ false); - mSceneTestUtils.getSceneContainerFlags().setEnabled(true); + mKosmos.getFakeSceneContainerFlags().setEnabled(true); when(mFalsingManager.isFalseLongTap(anyInt())).thenReturn(true); // WHEN longPress diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationAnimationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationAnimationControllerTest.java index 2afb3a15b4be..d86d12303140 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationAnimationControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationAnimationControllerTest.java @@ -640,11 +640,10 @@ public class WindowMagnificationAnimationControllerTest extends SysuiTestCase { @Test public void deleteWindowMagnification_enabling_expectedValuesAndInvokeCallback() throws RemoteException { - enableWindowMagnificationAndWaitAnimating(mWaitPartialAnimationDuration, mAnimationCallback); - Mockito.reset(mSpyController); + resetMockObjects(); getInstrumentation().runOnMainSync(() -> { mWindowMagnificationAnimationController.deleteWindowMagnification( mAnimationCallback2); @@ -658,6 +657,11 @@ public class WindowMagnificationAnimationControllerTest extends SysuiTestCase { mValueAnimator.end(); }); + // wait for animation returns + waitForIdleSync(); + + // {@link ValueAnimator.AnimatorUpdateListener#onAnimationUpdate(ValueAnimator)} will only + // be triggered once in {@link ValueAnimator#end()} verify(mSpyController).enableWindowMagnificationInternal( mScaleCaptor.capture(), mCenterXCaptor.capture(), mCenterYCaptor.capture(), @@ -717,7 +721,11 @@ public class WindowMagnificationAnimationControllerTest extends SysuiTestCase { deleteWindowMagnificationAndWaitAnimating(mWaitPartialAnimationDuration, mAnimationCallback); - deleteWindowMagnificationAndWaitAnimating(0, null); + // Verifying that WindowMagnificationController#deleteWindowMagnification is never called + // in previous steps + verify(mSpyController, never()).deleteWindowMagnification(); + + deleteWindowMagnificationWithoutAnimation(); verify(mSpyController).deleteWindowMagnification(); verifyFinalSpec(Float.NaN, Float.NaN, Float.NaN); @@ -810,6 +818,8 @@ public class WindowMagnificationAnimationControllerTest extends SysuiTestCase { mWindowMagnificationAnimationController.enableWindowMagnification( targetScale, targetCenterX, targetCenterY, null); }); + // wait for animation returns + waitForIdleSync(); } private void enableWindowMagnificationAndWaitAnimating(long duration, @@ -829,12 +839,16 @@ public class WindowMagnificationAnimationControllerTest extends SysuiTestCase { targetScale, targetCenterX, targetCenterY, callback); advanceTimeBy(duration); }); + // wait for animation returns + waitForIdleSync(); } private void deleteWindowMagnificationWithoutAnimation() { getInstrumentation().runOnMainSync(() -> { mWindowMagnificationAnimationController.deleteWindowMagnification(null); }); + // wait for animation returns + waitForIdleSync(); } private void deleteWindowMagnificationAndWaitAnimating(long duration, @@ -843,6 +857,8 @@ public class WindowMagnificationAnimationControllerTest extends SysuiTestCase { mWindowMagnificationAnimationController.deleteWindowMagnification(callback); advanceTimeBy(duration); }); + // wait for animation returns + waitForIdleSync(); } private void verifyStartValue(ArgumentCaptor<Float> captor, float startValue) { diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerTest.java index 68879a54cfe4..5e5273b779c6 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerTest.java @@ -55,8 +55,6 @@ import android.os.Build; import android.os.UserHandle; import android.platform.test.annotations.DisableFlags; import android.platform.test.annotations.EnableFlags; -import android.platform.test.flag.junit.CheckFlagsRule; -import android.platform.test.flag.junit.DeviceFlagsValueProvider; import android.provider.Settings; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; @@ -120,10 +118,6 @@ public class MenuViewLayerTest extends SysuiTestCase { @Rule public MockitoRule mockito = MockitoJUnit.rule(); - @Rule - public final CheckFlagsRule mCheckFlagsRule = - DeviceFlagsValueProvider.createCheckFlagsRule(); - @Spy private SysuiTestableContext mSpyContext = getContext(); @Mock 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 7c626a141a4a..e0c6bbad5635 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 @@ -44,6 +44,8 @@ import com.android.systemui.shade.ShadeController import com.android.systemui.shade.ShadeViewController import com.android.systemui.statusbar.NotificationShadeWindowController import com.android.systemui.statusbar.StatusBarState +import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationListRepository +import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager import com.android.systemui.statusbar.policy.HeadsUpManager import com.android.systemui.util.concurrency.FakeExecutor @@ -56,6 +58,7 @@ import com.google.common.truth.Truth.assertThat import junit.framework.Assert.assertFalse 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 @@ -89,6 +92,9 @@ class BackActionInteractorTest : SysuiTestCase() { @Mock private lateinit var onBackInvokedDispatcher: WindowOnBackInvokedDispatcher @Mock private lateinit var iStatusBarService: IStatusBarService @Mock private lateinit var headsUpManager: HeadsUpManager + private val activeNotificationsRepository = ActiveNotificationListRepository() + private val activeNotificationsInteractor = + ActiveNotificationsInteractor(activeNotificationsRepository, StandardTestDispatcher()) private val keyguardRepository = FakeKeyguardRepository() private val windowRootViewVisibilityInteractor: WindowRootViewVisibilityInteractor by lazy { @@ -98,6 +104,7 @@ class BackActionInteractorTest : SysuiTestCase() { keyguardRepository, headsUpManager, powerInteractor, + activeNotificationsInteractor, ) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt index 0ee09390d03a..43f7c60721ee 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt @@ -16,6 +16,7 @@ package com.android.systemui.biometrics import android.app.admin.DevicePolicyManager +import android.content.pm.PackageManager import android.hardware.biometrics.BiometricAuthenticator import android.hardware.biometrics.BiometricConstants import android.hardware.biometrics.BiometricManager @@ -79,6 +80,8 @@ import org.mockito.Mockito.verify import org.mockito.junit.MockitoJUnit import org.mockito.Mockito.`when` as whenever +private const val OP_PACKAGE_NAME = "biometric.testapp" + @RunWith(AndroidJUnit4::class) @RunWithLooper(setAsMainLooper = true) @SmallTest @@ -109,6 +112,8 @@ open class AuthContainerViewTest : SysuiTestCase() { lateinit var authController: AuthController @Mock lateinit var selectedUserInteractor: SelectedUserInteractor + @Mock + private lateinit var packageManager: PackageManager private val testScope = TestScope(StandardTestDispatcher()) private val fakeExecutor = FakeExecutor(FakeSystemClock()) @@ -134,6 +139,7 @@ open class AuthContainerViewTest : SysuiTestCase() { private lateinit var udfpsOverlayInteractor: UdfpsOverlayInteractor private val credentialViewModel = CredentialViewModel(mContext, bpCredentialInteractor) + private val defaultLogoIcon = context.getDrawable(R.drawable.ic_android) private var authContainer: TestAuthContainerView? = null @@ -156,6 +162,9 @@ open class AuthContainerViewTest : SysuiTestCase() { selectedUserInteractor, testScope.backgroundScope, ) + // Set up default logo icon + whenever(packageManager.getApplicationIcon(OP_PACKAGE_NAME)).thenReturn(defaultLogoIcon) + context.setMockPackageManager(packageManager) } @After @@ -533,6 +542,7 @@ open class AuthContainerViewTest : SysuiTestCase() { mPromptInfo = PromptInfo().apply { this.authenticators = authenticators } + mOpPackageName = OP_PACKAGE_NAME }, testScope.backgroundScope, fingerprintProps, diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsBpViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsBpViewControllerTest.kt index 647dae6fd6c2..13306becf6d2 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsBpViewControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsBpViewControllerTest.kt @@ -20,6 +20,7 @@ import android.testing.TestableLooper import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.biometrics.domain.interactor.UdfpsOverlayInteractor import com.android.systemui.dump.DumpManager import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.shade.domain.interactor.ShadeInteractor @@ -44,6 +45,7 @@ class UdfpsBpViewControllerTest : SysuiTestCase() { @Mock lateinit var shadeInteractor: ShadeInteractor @Mock lateinit var systemUIDialogManager: SystemUIDialogManager @Mock lateinit var dumpManager: DumpManager + @Mock lateinit var udfpsOverlayInteractor: UdfpsOverlayInteractor private lateinit var udfpsBpViewController: UdfpsBpViewController @@ -55,7 +57,8 @@ class UdfpsBpViewControllerTest : SysuiTestCase() { statusBarStateController, shadeInteractor, systemUIDialogManager, - dumpManager + dumpManager, + udfpsOverlayInteractor, ) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/PromptRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/PromptRepositoryImplTest.kt index ec7ce634fd78..b39e09df9d2e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/PromptRepositoryImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/PromptRepositoryImplTest.kt @@ -43,6 +43,7 @@ import org.mockito.junit.MockitoJUnit private const val USER_ID = 9 private const val CHALLENGE = 90L +private const val OP_PACKAGE_NAME = "biometric.testapp" @OptIn(ExperimentalCoroutinesApi::class) @SmallTest @@ -102,7 +103,8 @@ class PromptRepositoryImplTest : SysuiTestCase() { PromptInfo().apply { isConfirmationRequested = case }, USER_ID, CHALLENGE, - PromptKind.Biometric() + PromptKind.Biometric(), + OP_PACKAGE_NAME ) assertThat(isConfirmationRequired).isEqualTo(case) @@ -120,7 +122,8 @@ class PromptRepositoryImplTest : SysuiTestCase() { PromptInfo().apply { isConfirmationRequested = case }, USER_ID, CHALLENGE, - PromptKind.Biometric() + PromptKind.Biometric(), + OP_PACKAGE_NAME ) assertThat(isConfirmationRequired).isTrue() @@ -133,17 +136,19 @@ class PromptRepositoryImplTest : SysuiTestCase() { val kind = PromptKind.Pin val promptInfo = PromptInfo() - repository.setPrompt(promptInfo, USER_ID, CHALLENGE, kind) + repository.setPrompt(promptInfo, USER_ID, CHALLENGE, kind, OP_PACKAGE_NAME) assertThat(repository.kind.value).isEqualTo(kind) assertThat(repository.userId.value).isEqualTo(USER_ID) assertThat(repository.challenge.value).isEqualTo(CHALLENGE) assertThat(repository.promptInfo.value).isSameInstanceAs(promptInfo) + assertThat(repository.opPackageName.value).isEqualTo(OP_PACKAGE_NAME) repository.unsetPrompt() assertThat(repository.promptInfo.value).isNull() assertThat(repository.userId.value).isNull() assertThat(repository.challenge.value).isNull() + assertThat(repository.opPackageName.value).isNull() } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/LogContextInteractorImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/LogContextInteractorImplTest.kt index 8f8004f1cbb8..b1e471af2e71 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/LogContextInteractorImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/LogContextInteractorImplTest.kt @@ -5,6 +5,7 @@ import android.hardware.biometrics.IBiometricContextListener import android.hardware.biometrics.IBiometricContextListener.FoldState import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.biometrics.AuthController import com.android.systemui.coroutines.collectLastValue import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractorFactory @@ -17,6 +18,7 @@ import com.android.systemui.unfold.updates.FOLD_UPDATE_FINISH_HALF_OPEN import com.android.systemui.unfold.updates.FOLD_UPDATE_START_CLOSING import com.android.systemui.unfold.updates.FOLD_UPDATE_START_OPENING import com.android.systemui.unfold.updates.FoldStateProvider +import com.android.systemui.user.domain.interactor.SelectedUserInteractor import com.android.systemui.util.mockito.withArgCaptor import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -42,7 +44,10 @@ class LogContextInteractorImplTest : SysuiTestCase() { private val testScope = TestScope() @Mock private lateinit var foldProvider: FoldStateProvider + @Mock private lateinit var authController: AuthController + @Mock private lateinit var selectedUserInteractor: SelectedUserInteractor + private lateinit var udfpsOverlayInteractor: UdfpsOverlayInteractor private lateinit var keyguardTransitionRepository: FakeKeyguardTransitionRepository private lateinit var interactor: LogContextInteractorImpl @@ -50,6 +55,13 @@ class LogContextInteractorImplTest : SysuiTestCase() { @Before fun setup() { keyguardTransitionRepository = FakeKeyguardTransitionRepository() + udfpsOverlayInteractor = + UdfpsOverlayInteractor( + context, + authController, + selectedUserInteractor, + testScope.backgroundScope, + ) interactor = LogContextInteractorImpl( testScope.backgroundScope, @@ -59,6 +71,7 @@ class LogContextInteractorImplTest : SysuiTestCase() { scope = testScope.backgroundScope, ) .keyguardTransitionInteractor, + udfpsOverlayInteractor, ) } @@ -162,6 +175,18 @@ class LogContextInteractorImplTest : SysuiTestCase() { } @Test + fun isHardwareIgnoringTouchesChanges() = + testScope.runTest { + val isHardwareIgnoringTouches by collectLastValue(interactor.isHardwareIgnoringTouches) + + udfpsOverlayInteractor.setHandleTouches(true) + assertThat(isHardwareIgnoringTouches).isFalse() + + udfpsOverlayInteractor.setHandleTouches(false) + assertThat(isHardwareIgnoringTouches).isTrue() + } + + @Test fun foldStateChanges() = testScope.runTest { val foldState = collectLastValue(interactor.foldState) @@ -195,6 +220,7 @@ class LogContextInteractorImplTest : SysuiTestCase() { var folded: Int? = null var displayState: Int? = null + var ignoreTouches: Boolean? = null val job = interactor.addBiometricContextListener( object : IBiometricContextListener.Stub() { @@ -205,12 +231,17 @@ class LogContextInteractorImplTest : SysuiTestCase() { override fun onDisplayStateChanged(newDisplayState: Int) { displayState = newDisplayState } + + override fun onHardwareIgnoreTouchesChanged(newIgnoreTouches: Boolean) { + ignoreTouches = newIgnoreTouches + } } ) runCurrent() assertThat(folded).isEqualTo(FoldState.FULLY_CLOSED) assertThat(displayState).isEqualTo(AuthenticateOptions.DISPLAY_STATE_AOD) + assertThat(ignoreTouches).isFalse() foldListener.onFoldUpdate(FOLD_UPDATE_START_OPENING) foldListener.onFoldUpdate(FOLD_UPDATE_FINISH_HALF_OPEN) @@ -220,6 +251,11 @@ class LogContextInteractorImplTest : SysuiTestCase() { assertThat(folded).isEqualTo(FoldState.HALF_OPENED) assertThat(displayState).isEqualTo(AuthenticateOptions.DISPLAY_STATE_LOCKSCREEN) + udfpsOverlayInteractor.setHandleTouches(false) + runCurrent() + + assertThat(ignoreTouches).isTrue() + job.cancel() // stale updates should be ignored diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/PromptCredentialInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/PromptCredentialInteractorTest.kt index dcefea28d4c8..8a46c0c6da9f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/PromptCredentialInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/PromptCredentialInteractorTest.kt @@ -30,6 +30,7 @@ import org.mockito.junit.MockitoJUnit private const val USER_ID = 22 private const val OPERATION_ID = 100L +private const val OP_PACKAGE_NAME = "biometric.testapp" @OptIn(ExperimentalCoroutinesApi::class) @SmallTest @@ -114,7 +115,8 @@ class PromptCredentialInteractorTest : SysuiTestCase() { }, kind = kind, userId = USER_ID, - challenge = OPERATION_ID + challenge = OPERATION_ID, + opPackageName = OP_PACKAGE_NAME ) assertThat(prompt?.title).isEqualTo(title) diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractorImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractorImplTest.kt index f15b738f3e95..52b42750847a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractorImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractorImplTest.kt @@ -51,6 +51,7 @@ private const val NEGATIVE_TEXT = "escape" private const val USER_ID = 8 private const val CHALLENGE = 999L +private const val OP_PACKAGE_NAME = "biometric.testapp" @OptIn(ExperimentalCoroutinesApi::class) @SmallTest @@ -113,13 +114,20 @@ class PromptSelectorInteractorImplTest : SysuiTestCase() { assertThat(currentPrompt).isNull() - interactor.useBiometricsForAuthentication(info, USER_ID, CHALLENGE, modalities) + interactor.useBiometricsForAuthentication( + info, + USER_ID, + CHALLENGE, + modalities, + OP_PACKAGE_NAME + ) assertThat(currentPrompt).isNotNull() assertThat(currentPrompt?.title).isEqualTo(TITLE) assertThat(currentPrompt?.description).isEqualTo(DESCRIPTION) assertThat(currentPrompt?.subtitle).isEqualTo(SUBTITLE) assertThat(currentPrompt?.negativeButtonText).isEqualTo(NEGATIVE_TEXT) + assertThat(currentPrompt?.opPackageName).isEqualTo(OP_PACKAGE_NAME) if (allowCredentialFallback) { assertThat(credentialKind).isSameInstanceAs(PromptKind.Password) @@ -167,7 +175,7 @@ class PromptSelectorInteractorImplTest : SysuiTestCase() { assertThat(currentPrompt).isNull() - interactor.useCredentialsForAuthentication(info, kind, USER_ID, CHALLENGE) + interactor.useCredentialsForAuthentication(info, kind, USER_ID, CHALLENGE, OP_PACKAGE_NAME) // not using biometrics, should be null with no fallback option assertThat(currentPrompt).isNull() diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/UdfpsOverlayInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/UdfpsOverlayInteractorTest.kt index 6a686726b959..c0e108ef75f8 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/UdfpsOverlayInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/UdfpsOverlayInteractorTest.kt @@ -68,7 +68,7 @@ class UdfpsOverlayInteractorTest : SysuiTestCase() { @Test fun testShouldInterceptTouch() = testScope.runTest { - createUdpfsOverlayInteractor() + createUdfpsOverlayInteractor() // When fingerprint enrolled and touch is within bounds verify(authController).addCallback(authControllerCallback.capture()) @@ -92,7 +92,7 @@ class UdfpsOverlayInteractorTest : SysuiTestCase() { @Test fun testUdfpsOverlayParamsChange() = testScope.runTest { - createUdpfsOverlayInteractor() + createUdfpsOverlayInteractor() val udfpsOverlayParams = collectLastValue(underTest.udfpsOverlayParams) runCurrent() @@ -105,7 +105,7 @@ class UdfpsOverlayInteractorTest : SysuiTestCase() { assertThat(udfpsOverlayParams()).isEqualTo(firstParams) } - private fun createUdpfsOverlayInteractor() { + private fun createUdfpsOverlayInteractor() { underTest = UdfpsOverlayInteractor( context, diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/model/BiometricPromptRequestTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/model/BiometricPromptRequestTest.kt index bd4973d65006..a46167a423f1 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/model/BiometricPromptRequestTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/model/BiometricPromptRequestTest.kt @@ -1,6 +1,7 @@ package com.android.systemui.biometrics.domain.model -import android.hardware.biometrics.PromptContentListItemBulletedText +import android.graphics.Bitmap +import android.hardware.biometrics.PromptContentItemBulletedText import android.hardware.biometrics.PromptVerticalListContentView import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase @@ -8,6 +9,7 @@ import com.android.systemui.biometrics.fingerprintSensorPropertiesInternal import com.android.systemui.biometrics.promptInfo import com.android.systemui.biometrics.shared.model.BiometricModalities import com.android.systemui.biometrics.shared.model.BiometricUserInfo +import com.android.systemui.res.R import com.google.common.truth.Truth.assertThat import org.junit.Test import org.junit.runner.RunWith @@ -15,6 +17,7 @@ import org.junit.runners.JUnit4 private const val USER_ID = 2 private const val OPERATION_ID = 8L +private const val OP_PACKAGE_NAME = "biometric.testapp" @SmallTest @RunWith(JUnit4::class) @@ -22,19 +25,22 @@ class BiometricPromptRequestTest : SysuiTestCase() { @Test fun biometricRequestFromPromptInfo() { + val logoRes = R.drawable.ic_cake val title = "what" val subtitle = "a" val description = "request" val contentView = PromptVerticalListContentView.Builder() .setDescription("content description") - .addListItem(PromptContentListItemBulletedText("content text")) + .addListItem(PromptContentItemBulletedText("content item 1")) + .addListItem(PromptContentItemBulletedText("content item 2"), 1) .build() val fpPros = fingerprintSensorPropertiesInternal().first() val request = BiometricPromptRequest.Biometric( promptInfo( + logoRes = logoRes, title = title, subtitle = subtitle, description = description, @@ -43,8 +49,10 @@ class BiometricPromptRequestTest : SysuiTestCase() { BiometricUserInfo(USER_ID), BiometricOperationInfo(OPERATION_ID), BiometricModalities(fingerprintProperties = fpPros), + OP_PACKAGE_NAME, ) + assertThat(request.logoRes).isEqualTo(logoRes) assertThat(request.title).isEqualTo(title) assertThat(request.subtitle).isEqualTo(subtitle) assertThat(request.description).isEqualTo(description) @@ -56,6 +64,23 @@ class BiometricPromptRequestTest : SysuiTestCase() { } @Test + fun biometricRequestLogoBitmapFromPromptInfo() { + val logoBitmap = Bitmap.createBitmap(400, 400, Bitmap.Config.ARGB_8888) + val fpPros = fingerprintSensorPropertiesInternal().first() + val request = + BiometricPromptRequest.Biometric( + promptInfo( + logoBitmap = logoBitmap, + ), + BiometricUserInfo(USER_ID), + BiometricOperationInfo(OPERATION_ID), + BiometricModalities(fingerprintProperties = fpPros), + OP_PACKAGE_NAME, + ) + assertThat(request.logoBitmap).isEqualTo(logoBitmap) + } + + @Test fun credentialRequestFromPromptInfo() { val title = "what" val subtitle = "a" 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 bf61c2e6d32c..3888f2b940b3 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 @@ -16,9 +16,12 @@ package com.android.systemui.biometrics.ui.viewmodel +import android.content.pm.PackageManager import android.content.res.Configuration +import android.graphics.Bitmap import android.graphics.Point -import android.hardware.biometrics.PromptContentListItemBulletedText +import android.graphics.drawable.BitmapDrawable +import android.hardware.biometrics.PromptContentItemBulletedText import android.hardware.biometrics.PromptContentView import android.hardware.biometrics.PromptInfo import android.hardware.biometrics.PromptVerticalListContentView @@ -76,6 +79,7 @@ import org.mockito.junit.MockitoJUnit private const val USER_ID = 4 private const val CHALLENGE = 2L private const val DELAY = 1000L +private const val OP_PACKAGE_NAME = "biometric.testapp" @OptIn(ExperimentalCoroutinesApi::class) @SmallTest @@ -88,9 +92,14 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa @Mock private lateinit var authController: AuthController @Mock private lateinit var selectedUserInteractor: SelectedUserInteractor @Mock private lateinit var udfpsUtils: UdfpsUtils + @Mock private lateinit var packageManager: PackageManager private val fakeExecutor = FakeExecutor(FakeSystemClock()) private val testScope = TestScope() + private val defaultLogoIcon = context.getDrawable(R.drawable.ic_android) + private val logoResFromApp = R.drawable.ic_cake + private val logoFromApp = context.getDrawable(logoResFromApp) + private val logoBitmapFromApp = Bitmap.createBitmap(400, 400, Bitmap.Config.RGB_565) private lateinit var fingerprintRepository: FakeFingerprintPropertyRepository private lateinit var promptRepository: FakePromptRepository @@ -140,7 +149,8 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa selector.resetPrompt() promptContentView = PromptVerticalListContentView.Builder() - .addListItem(PromptContentListItemBulletedText("test")) + .addListItem(PromptContentItemBulletedText("content item 1")) + .addListItem(PromptContentItemBulletedText("content item 2"), 1) .build() viewModel = @@ -152,6 +162,12 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa udfpsUtils ) iconViewModel = viewModel.iconViewModel + + // Set up default logo icon and app customized icon + whenever(packageManager.getApplicationIcon(OP_PACKAGE_NAME)).thenReturn(defaultLogoIcon) + context.setMockPackageManager(packageManager) + val resources = context.getOrCreateTestableResources() + resources.addOverride(logoResFromApp, logoFromApp) } @Test @@ -1226,6 +1242,26 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa assertThat(contentView).isNull() } + @Test + fun defaultLogoIfNoLogoSet() = runGenericTest { + val logo by collectLastValue(viewModel.logo) + assertThat(logo).isEqualTo(defaultLogoIcon) + } + + @Test + fun logoResSetByApp() = + runGenericTest(logoRes = logoResFromApp) { + val logo by collectLastValue(viewModel.logo) + assertThat(logo).isEqualTo(logoFromApp) + } + + @Test + fun logoBitmapSetByApp() = + runGenericTest(logoBitmap = logoBitmapFromApp) { + val logo by collectLastValue(viewModel.logo) + assertThat((logo as BitmapDrawable).bitmap).isEqualTo(logoBitmapFromApp) + } + /** Asserts that the selected buttons are visible now. */ private suspend fun TestScope.assertButtonsVisible( tryAgain: Boolean = false, @@ -1247,6 +1283,8 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa allowCredentialFallback: Boolean = false, description: String? = null, contentView: PromptContentView? = null, + logoRes: Int = -1, + logoBitmap: Bitmap? = null, block: suspend TestScope.() -> Unit ) { selector.initializePrompt( @@ -1256,6 +1294,8 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa face = testCase.face, descriptionFromApp = description, contentViewFromApp = contentView, + logoResFromApp = logoRes, + logoBitmapFromApp = logoBitmap, ) // put the view model in the initial authenticating state, unless explicitly skipped @@ -1433,9 +1473,13 @@ private fun PromptSelectorInteractor.initializePrompt( allowCredentialFallback: Boolean = false, descriptionFromApp: String? = null, contentViewFromApp: PromptContentView? = null, + logoResFromApp: Int = -1, + logoBitmapFromApp: Bitmap? = null, ) { val info = PromptInfo().apply { + logoRes = logoResFromApp + logoBitmap = logoBitmapFromApp title = "t" subtitle = "s" description = descriptionFromApp @@ -1444,11 +1488,13 @@ private fun PromptSelectorInteractor.initializePrompt( isDeviceCredentialAllowed = allowCredentialFallback isConfirmationRequested = requireConfirmation } + useBiometricsForAuthentication( info, USER_ID, CHALLENGE, BiometricModalities(fingerprintProperties = fingerprint, faceProperties = face), + OP_PACKAGE_NAME, ) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/data/repository/KeyguardBouncerRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/data/repository/KeyguardBouncerRepositoryTest.kt index 44c57f34fa1b..134c40da1033 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/data/repository/KeyguardBouncerRepositoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/data/repository/KeyguardBouncerRepositoryTest.kt @@ -3,8 +3,9 @@ package com.android.systemui.bouncer.data.repository import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.kosmos.testScope import com.android.systemui.log.table.TableLogBuffer -import com.android.systemui.scene.SceneTestUtils +import com.android.systemui.testKosmos import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.eq import com.android.systemui.util.time.SystemClock @@ -23,8 +24,8 @@ class KeyguardBouncerRepositoryTest : SysuiTestCase() { @Mock private lateinit var systemClock: SystemClock @Mock private lateinit var bouncerLogger: TableLogBuffer - private val utils = SceneTestUtils(this) - private val testScope = utils.testScope + private val kosmos = testKosmos() + private val testScope = kosmos.testScope lateinit var underTest: KeyguardBouncerRepository diff --git a/packages/SystemUI/tests/src/com/android/systemui/deviceentry/data/repository/DeviceEntryHapticsInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/deviceentry/data/repository/DeviceEntryHapticsInteractorTest.kt index d5c3641e75a9..0dfdeca60fcd 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/deviceentry/data/repository/DeviceEntryHapticsInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/deviceentry/data/repository/DeviceEntryHapticsInteractorTest.kt @@ -36,9 +36,9 @@ import com.android.systemui.power.data.repository.powerRepository import com.android.systemui.power.shared.model.WakeSleepReason import com.android.systemui.power.shared.model.WakefulnessState import com.android.systemui.testKosmos -import com.android.systemui.util.time.fakeSystemClock import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.advanceTimeBy import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Test @@ -72,8 +72,8 @@ class DeviceEntryHapticsInteractorTest : SysuiTestCase() { // It's been 10 seconds since the last power button wakeup setAwakeFromPowerButton() + advanceTimeBy(10000) runCurrent() - kosmos.fakeSystemClock.setUptimeMillis(kosmos.fakeSystemClock.uptimeMillis() + 10000) enterDeviceFromBiometricUnlock() assertThat(playSuccessHaptic).isNotNull() @@ -89,8 +89,8 @@ class DeviceEntryHapticsInteractorTest : SysuiTestCase() { // It's been 10 seconds since the last power button wakeup setAwakeFromPowerButton() + advanceTimeBy(10000) runCurrent() - kosmos.fakeSystemClock.setUptimeMillis(kosmos.fakeSystemClock.uptimeMillis() + 10000) enterDeviceFromBiometricUnlock() assertThat(playSuccessHaptic).isNull() @@ -106,8 +106,8 @@ class DeviceEntryHapticsInteractorTest : SysuiTestCase() { // It's only been 50ms since the last power button wakeup setAwakeFromPowerButton() + advanceTimeBy(50) runCurrent() - kosmos.fakeSystemClock.setUptimeMillis(kosmos.fakeSystemClock.uptimeMillis() + 50) enterDeviceFromBiometricUnlock() assertThat(playSuccessHaptic).isNull() 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 new file mode 100644 index 000000000000..d397fc202637 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/keyboard/stickykeys/ui/viewmodel/StickyKeysIndicatorViewModelTest.kt @@ -0,0 +1,211 @@ +/* + * 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.viewmodel + +import android.hardware.input.InputManager +import android.hardware.input.StickyModifierState +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.keyboard.data.repository.FakeKeyboardRepository +import com.android.systemui.keyboard.stickykeys.StickyKeysLogger +import com.android.systemui.keyboard.stickykeys.data.repository.StickyKeysRepositoryImpl +import com.android.systemui.keyboard.stickykeys.shared.model.Locked +import com.android.systemui.keyboard.stickykeys.shared.model.ModifierKey +import com.android.systemui.keyboard.stickykeys.shared.model.ModifierKey.ALT +import com.android.systemui.keyboard.stickykeys.shared.model.ModifierKey.ALT_GR +import com.android.systemui.keyboard.stickykeys.shared.model.ModifierKey.CTRL +import com.android.systemui.keyboard.stickykeys.shared.model.ModifierKey.META +import com.android.systemui.keyboard.stickykeys.shared.model.ModifierKey.SHIFT +import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.mock +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.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.mockito.ArgumentCaptor +import org.mockito.Mockito.verify +import org.mockito.Mockito.verifyZeroInteractions + +@SmallTest +@RunWith(JUnit4::class) +class StickyKeysIndicatorViewModelTest : SysuiTestCase() { + + private val dispatcher = StandardTestDispatcher() + private val testScope = TestScope(dispatcher) + private lateinit var viewModel: StickyKeysIndicatorViewModel + private val inputManager = mock<InputManager>() + private val keyboardRepository = FakeKeyboardRepository() + private val captor = + ArgumentCaptor.forClass(InputManager.StickyModifierStateListener::class.java) + + @Before + fun setup() { + val stickyKeysRepository = StickyKeysRepositoryImpl( + inputManager, + dispatcher, + mock<StickyKeysLogger>() + ) + viewModel = + StickyKeysIndicatorViewModel( + stickyKeysRepository = stickyKeysRepository, + keyboardRepository = keyboardRepository, + applicationScope = testScope.backgroundScope, + ) + } + + @Test + fun startsListeningToStickyKeysOnlyWhenKeyboardIsConnected() { + testScope.runTest { + collectLastValue(viewModel.indicatorContent) + runCurrent() + verifyZeroInteractions(inputManager) + + keyboardRepository.setIsAnyKeyboardConnected(true) + runCurrent() + + verify(inputManager) + .registerStickyModifierStateListener( + any(), + any(InputManager.StickyModifierStateListener::class.java) + ) + } + } + + @Test + fun stopsListeningToStickyKeysWhenKeyboardDisconnects() { + testScope.runTest { + collectLastValue(viewModel.indicatorContent) + keyboardRepository.setIsAnyKeyboardConnected(true) + runCurrent() + + keyboardRepository.setIsAnyKeyboardConnected(false) + runCurrent() + + verify(inputManager).unregisterStickyModifierStateListener(any()) + } + } + + @Test + fun emitsStickyKeysListWhenStickyKeyIsPressed() { + testScope.runTest { + val stickyKeys by collectLastValue(viewModel.indicatorContent) + keyboardRepository.setIsAnyKeyboardConnected(true) + + setStickyKeys(mapOf(ALT to false)) + + assertThat(stickyKeys).isEqualTo(mapOf(ALT to Locked(false))) + } + } + + @Test + fun emitsEmptyListWhenNoStickyKeysAreActive() { + testScope.runTest { + val stickyKeys by collectLastValue(viewModel.indicatorContent) + keyboardRepository.setIsAnyKeyboardConnected(true) + + setStickyKeys(emptyMap()) + + assertThat(stickyKeys).isEqualTo(emptyMap<ModifierKey, Locked>()) + } + } + + @Test + fun passesAllStickyKeysToDialog() { + testScope.runTest { + val stickyKeys by collectLastValue(viewModel.indicatorContent) + keyboardRepository.setIsAnyKeyboardConnected(true) + + setStickyKeys(mapOf( + ALT to false, + META to false, + SHIFT to false)) + + assertThat(stickyKeys).isEqualTo(mapOf( + ALT to Locked(false), + META to Locked(false), + SHIFT to Locked(false), + )) + } + } + + @Test + fun showsOnlyLockedStateIfKeyIsStickyAndLocked() { + testScope.runTest { + val stickyKeys by collectLastValue(viewModel.indicatorContent) + keyboardRepository.setIsAnyKeyboardConnected(true) + + setStickyKeys(mapOf( + ALT to false, + ALT to true)) + + assertThat(stickyKeys).isEqualTo(mapOf(ALT to Locked(true))) + } + } + + @Test + fun doesNotChangeOrderOfKeysIfTheyBecomeLocked() { + testScope.runTest { + val stickyKeys by collectLastValue(viewModel.indicatorContent) + keyboardRepository.setIsAnyKeyboardConnected(true) + + setStickyKeys(mapOf( + META to false, + SHIFT to false, // shift is sticky but not locked + CTRL to false)) + val previousShiftIndex = stickyKeys?.toList()?.indexOf(SHIFT to Locked(false)) + + setStickyKeys(mapOf( + SHIFT to false, + SHIFT to true, // shift is now locked + META to false, + CTRL to false)) + assertThat(stickyKeys?.toList()?.indexOf(SHIFT to Locked(true))) + .isEqualTo(previousShiftIndex) + } + } + + private fun TestScope.setStickyKeys(keys: Map<ModifierKey, Boolean>) { + runCurrent() + verify(inputManager).registerStickyModifierStateListener(any(), captor.capture()) + captor.value.onStickyModifierStateChanged(TestStickyModifierState(keys)) + runCurrent() + } + + private class TestStickyModifierState(private val keys: Map<ModifierKey, Boolean>) : + StickyModifierState() { + + private fun isOn(key: ModifierKey) = keys.any { it.key == key && !it.value } + private fun isLocked(key: ModifierKey) = keys.any { it.key == key && it.value } + + override fun isAltGrModifierLocked() = isLocked(ALT_GR) + override fun isAltGrModifierOn() = isOn(ALT_GR) + override fun isAltModifierLocked() = isLocked(ALT) + override fun isAltModifierOn() = isOn(ALT) + override fun isCtrlModifierLocked() = isLocked(CTRL) + override fun isCtrlModifierOn() = isOn(CTRL) + override fun isMetaModifierLocked() = isLocked(META) + override fun isMetaModifierOn() = isOn(META) + override fun isShiftModifierLocked() = isLocked(SHIFT) + override fun isShiftModifierOn() = isOn(SHIFT) + } +} 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 0ea4e9f8d416..8b6611f339c0 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 @@ -1342,14 +1342,8 @@ class KeyguardTransitionScenariosTest : SysuiTestCase() { runTransitionAndSetWakefulness(KeyguardState.LOCKSCREEN, KeyguardState.AOD) runCurrent() - // WHEN the keyguard is occluded and aod ends + // WHEN the keyguard is occluded keyguardRepository.setKeyguardOccluded(true) - keyguardRepository.setDozeTransitionModel( - DozeTransitionModel( - from = DozeStateModel.DOZE_AOD, - to = DozeStateModel.FINISH, - ) - ) runCurrent() val info = @@ -1366,6 +1360,30 @@ class KeyguardTransitionScenariosTest : SysuiTestCase() { } @Test + fun aodToPrimaryBouncer() = + testScope.runTest { + // GIVEN a prior transition has run to AOD + runTransitionAndSetWakefulness(KeyguardState.LOCKSCREEN, KeyguardState.AOD) + runCurrent() + + // WHEN the primary bouncer is set to show + bouncerRepository.setPrimaryShow(true) + runCurrent() + + val info = + withArgCaptor<TransitionInfo> { + verify(transitionRepository).startTransition(capture()) + } + // THEN a transition to OCCLUDED should occur + assertThat(info.ownerName).isEqualTo("FromAodTransitionInteractor") + assertThat(info.from).isEqualTo(KeyguardState.AOD) + assertThat(info.to).isEqualTo(KeyguardState.PRIMARY_BOUNCER) + assertThat(info.animator).isNotNull() + + coroutineContext.cancelChildren() + } + + @Test fun lockscreenToOccluded_fromCameraGesture() = testScope.runTest { // GIVEN a prior transition has run to LOCKSCREEN diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinderTest.kt index a4d217f1af79..5dd37ae46ee8 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinderTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinderTest.kt @@ -21,13 +21,17 @@ import androidx.constraintlayout.helper.widget.Layer import androidx.constraintlayout.widget.ConstraintLayout import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest +import com.android.keyguard.KeyguardClockSwitch.LARGE +import com.android.keyguard.KeyguardClockSwitch.SMALL import com.android.systemui.SysuiTestCase +import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel import com.android.systemui.plugins.clocks.ClockConfig import com.android.systemui.plugins.clocks.ClockController import com.android.systemui.plugins.clocks.ClockFaceController import com.android.systemui.plugins.clocks.ClockFaceLayout import com.android.systemui.util.mockito.whenever import kotlin.test.Test +import kotlinx.coroutines.flow.MutableStateFlow import org.junit.Before import org.junit.runner.RunWith import org.mockito.Mock @@ -48,30 +52,58 @@ class KeyguardClockViewBinderTest : SysuiTestCase() { @Mock private lateinit var smallClockView: View @Mock private lateinit var smallClockFaceLayout: ClockFaceLayout @Mock private lateinit var largeClockFaceLayout: ClockFaceLayout + @Mock private lateinit var clockViewModel: KeyguardClockViewModel + private val clockSize = MutableStateFlow(LARGE) + private val currentClock: MutableStateFlow<ClockController?> = MutableStateFlow(null) @Before fun setup() { MockitoAnnotations.initMocks(this) + whenever(clockViewModel.clockSize).thenReturn(clockSize) + whenever(clockViewModel.currentClock).thenReturn(currentClock) + whenever(clockViewModel.burnInLayer).thenReturn(burnInLayer) + } + + @Test + fun addClockViews_WeatherClock() { + setupWeatherClock() + KeyguardClockViewBinder.addClockViews(clock, rootView) + verify(rootView).addView(smallClockView) + verify(rootView).addView(largeClockView) } @Test fun addClockViews_nonWeatherClock() { setupNonWeatherClock() - KeyguardClockViewBinder.addClockViews(clock, rootView, burnInLayer) + KeyguardClockViewBinder.addClockViews(clock, rootView) verify(rootView).addView(smallClockView) verify(rootView).addView(largeClockView) - verify(burnInLayer).addView(smallClockView) + } + @Test + fun addClockViewsToBurnInLayer_LargeWeatherClock() { + setupWeatherClock() + clockSize.value = LARGE + KeyguardClockViewBinder.updateBurnInLayer(rootView, clockViewModel) + verify(burnInLayer).removeView(smallClockView) + verify(burnInLayer).addView(largeClockView) + } + + @Test + fun addClockViewsToBurnInLayer_LargeNonWeatherClock() { + setupNonWeatherClock() + clockSize.value = LARGE + KeyguardClockViewBinder.updateBurnInLayer(rootView, clockViewModel) + verify(burnInLayer).removeView(smallClockView) verify(burnInLayer, never()).addView(largeClockView) } @Test - fun addClockViews_WeatherClock() { - setupWeatherClock() - KeyguardClockViewBinder.addClockViews(clock, rootView, burnInLayer) - verify(rootView).addView(smallClockView) - verify(rootView).addView(largeClockView) + fun addClockViewsToBurnInLayer_SmallClock() { + setupNonWeatherClock() + clockSize.value = SMALL + KeyguardClockViewBinder.updateBurnInLayer(rootView, clockViewModel) verify(burnInLayer).addView(smallClockView) - verify(burnInLayer).addView(largeClockView) + verify(burnInLayer).removeView(largeClockView) } private fun setupWeatherClock() { @@ -99,5 +131,7 @@ class KeyguardClockViewBinderTest : SysuiTestCase() { whenever(clock.smallClock).thenReturn(smallClock) whenever(largeClock.layout).thenReturn(largeClockFaceLayout) whenever(smallClock.layout).thenReturn(smallClockFaceLayout) + whenever(clockViewModel.clock).thenReturn(clock) + currentClock.value = clock } } 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 070a0ccd1232..57b555989166 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 @@ -22,6 +22,7 @@ import android.content.res.Resources 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.res.R @@ -31,6 +32,8 @@ import com.android.systemui.util.mockito.eq import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat +import dagger.Lazy +import kotlinx.coroutines.flow.MutableStateFlow import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -46,6 +49,8 @@ class ClockSectionTest : SysuiTestCase() { @Mock private lateinit var keyguardClockInteractor: KeyguardClockInteractor @Mock private lateinit var keyguardClockViewModel: KeyguardClockViewModel @Mock private lateinit var splitShadeStateController: SplitShadeStateController + @Mock private lateinit var blueprintInteractor: Lazy<KeyguardBlueprintInteractor> + private val clockShouldBeCentered: MutableStateFlow<Boolean> = MutableStateFlow(true) private lateinit var underTest: ClockSection @@ -104,12 +109,15 @@ class ClockSectionTest : SysuiTestCase() { whenever(packageManager.getResourcesForApplication(anyString())).thenReturn(remoteResources) mContext.setMockPackageManager(packageManager) + whenever(keyguardClockViewModel.clockShouldBeCentered).thenReturn(clockShouldBeCentered) + underTest = ClockSection( keyguardClockInteractor, keyguardClockViewModel, mContext, splitShadeStateController, + blueprintInteractor ) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/SmartspaceSectionTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/SmartspaceSectionTest.kt index 28da957d31b0..deb3a83fcbee 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/SmartspaceSectionTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/SmartspaceSectionTest.kt @@ -26,6 +26,8 @@ import androidx.test.filters.SmallTest import com.android.systemui.Flags import com.android.systemui.SysuiTestCase import com.android.systemui.keyguard.KeyguardUnlockAnimationController +import com.android.systemui.keyguard.domain.interactor.KeyguardBlueprintInteractor +import com.android.systemui.keyguard.domain.interactor.KeyguardSmartspaceInteractor import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel import com.android.systemui.keyguard.ui.viewmodel.KeyguardSmartspaceViewModel import com.android.systemui.res.R @@ -34,7 +36,8 @@ import com.android.systemui.statusbar.lockscreen.LockscreenSmartspaceController import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat -import kotlinx.coroutines.flow.StateFlow +import dagger.Lazy +import kotlinx.coroutines.flow.MutableStateFlow import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -50,7 +53,8 @@ class SmartspaceSectionTest : SysuiTestCase() { @Mock private lateinit var keyguardSmartspaceViewModel: KeyguardSmartspaceViewModel @Mock private lateinit var lockscreenSmartspaceController: LockscreenSmartspaceController @Mock private lateinit var keyguardUnlockAnimationController: KeyguardUnlockAnimationController - @Mock private lateinit var hasCustomWeatherDataDisplay: StateFlow<Boolean> + @Mock private lateinit var keyguardSmartspaceInteractor: KeyguardSmartspaceInteractor + @Mock private lateinit var blueprintInteractor: Lazy<KeyguardBlueprintInteractor> private val smartspaceView = View(mContext).also { it.id = sharedR.id.bc_smartspace_view } private val weatherView = View(mContext).also { it.id = sharedR.id.weather_smartspace_view } @@ -58,17 +62,22 @@ class SmartspaceSectionTest : SysuiTestCase() { private lateinit var constraintLayout: ConstraintLayout private lateinit var constraintSet: ConstraintSet + private val clockShouldBeCentered = MutableStateFlow(false) + private val hasCustomWeatherDataDisplay = MutableStateFlow(false) + @Before fun setup() { MockitoAnnotations.initMocks(this) mSetFlagsRule.enableFlags(Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) underTest = SmartspaceSection( + mContext, keyguardClockViewModel, keyguardSmartspaceViewModel, - mContext, + keyguardSmartspaceInteractor, lockscreenSmartspaceController, keyguardUnlockAnimationController, + blueprintInteractor ) constraintLayout = ConstraintLayout(mContext) whenever(lockscreenSmartspaceController.buildAndConnectView(any())) @@ -78,6 +87,7 @@ class SmartspaceSectionTest : SysuiTestCase() { whenever(lockscreenSmartspaceController.buildAndConnectDateView(any())).thenReturn(dateView) whenever(keyguardClockViewModel.hasCustomWeatherDataDisplay) .thenReturn(hasCustomWeatherDataDisplay) + whenever(keyguardClockViewModel.clockShouldBeCentered).thenReturn(clockShouldBeCentered) constraintSet = ConstraintSet() } @@ -115,7 +125,7 @@ class SmartspaceSectionTest : SysuiTestCase() { fun testConstraintsWhenNotHasCustomWeatherDataDisplay() { whenever(keyguardSmartspaceViewModel.isSmartspaceEnabled).thenReturn(true) whenever(keyguardSmartspaceViewModel.isDateWeatherDecoupled).thenReturn(true) - whenever(keyguardClockViewModel.hasCustomWeatherDataDisplay.value).thenReturn(false) + hasCustomWeatherDataDisplay.value = false underTest.addViews(constraintLayout) underTest.applyConstraints(constraintSet) assertWeatherSmartspaceConstrains(constraintSet) @@ -129,7 +139,7 @@ class SmartspaceSectionTest : SysuiTestCase() { @Test fun testConstraintsWhenHasCustomWeatherDataDisplay() { - whenever(keyguardClockViewModel.hasCustomWeatherDataDisplay.value).thenReturn(true) + hasCustomWeatherDataDisplay.value = true underTest.addViews(constraintLayout) underTest.applyConstraints(constraintSet) assertWeatherSmartspaceConstrains(constraintSet) @@ -140,7 +150,7 @@ class SmartspaceSectionTest : SysuiTestCase() { @Test fun testNormalDateWeatherVisibility() { - whenever(keyguardClockViewModel.hasCustomWeatherDataDisplay.value).thenReturn(false) + hasCustomWeatherDataDisplay.value = false whenever(keyguardSmartspaceViewModel.isWeatherEnabled).thenReturn(true) underTest.addViews(constraintLayout) underTest.applyConstraints(constraintSet) @@ -153,7 +163,7 @@ class SmartspaceSectionTest : SysuiTestCase() { } @Test fun testCustomDateWeatherVisibility() { - whenever(keyguardClockViewModel.hasCustomWeatherDataDisplay.value).thenReturn(true) + hasCustomWeatherDataDisplay.value = true underTest.addViews(constraintLayout) underTest.applyConstraints(constraintSet) diff --git a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/FakeMediaProjectionManager.kt b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/FakeMediaProjectionManager.kt index 45f0a8c62125..44c411fdb1d1 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/FakeMediaProjectionManager.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/data/repository/FakeMediaProjectionManager.kt @@ -66,6 +66,11 @@ class FakeMediaProjectionManager { private const val DEFAULT_PACKAGE_NAME = "com.media.projection.test" private val DEFAULT_USER_HANDLE = UserHandle.getUserHandleForUid(UserHandle.myUserId()) - private val DEFAULT_INFO = MediaProjectionInfo(DEFAULT_PACKAGE_NAME, DEFAULT_USER_HANDLE) + private val DEFAULT_INFO = + MediaProjectionInfo( + DEFAULT_PACKAGE_NAME, + DEFAULT_USER_HANDLE, + /* launchCookie = */ null + ) } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/LeftRightArrowPressedListenerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/LeftRightArrowPressedListenerTest.kt new file mode 100644 index 000000000000..60eb3aec190f --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/LeftRightArrowPressedListenerTest.kt @@ -0,0 +1,107 @@ +/* + * 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 + +import android.testing.AndroidTestingRunner +import android.view.KeyEvent +import android.view.KeyEvent.KEYCODE_DPAD_LEFT +import android.view.View +import androidx.core.util.Consumer +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidTestingRunner::class) +@SmallTest +class LeftRightArrowPressedListenerTest : SysuiTestCase() { + + private lateinit var underTest: LeftRightArrowPressedListener + private val callback = + object : Consumer<Int> { + var lastValue: Int? = null + + override fun accept(keyCode: Int?) { + lastValue = keyCode + } + } + + private val view = View(context) + + @Before + fun setUp() { + underTest = LeftRightArrowPressedListener.createAndRegisterListenerForView(view) + underTest.setArrowKeyPressedListener(callback) + } + + @Test + fun shouldTriggerCallback_whenArrowUpReceived_afterArrowDownReceived() { + underTest.sendKey(KeyEvent.ACTION_DOWN, KEYCODE_DPAD_LEFT) + + underTest.sendKey(KeyEvent.ACTION_UP, KEYCODE_DPAD_LEFT) + + assertThat(callback.lastValue).isEqualTo(KEYCODE_DPAD_LEFT) + } + + @Test + fun shouldNotTriggerCallback_whenKeyUpReceived_ifKeyDownNotReceived() { + underTest.sendKey(KeyEvent.ACTION_UP, KEYCODE_DPAD_LEFT) + + assertThat(callback.lastValue).isNull() + } + + @Test + fun shouldNotTriggerCallback_whenKeyUpReceived_ifKeyDownWasRepeated() { + underTest.sendKeyWithRepeat(KeyEvent.ACTION_UP, KEYCODE_DPAD_LEFT, repeat = 2) + underTest.sendKey(KeyEvent.ACTION_UP, KEYCODE_DPAD_LEFT) + + assertThat(callback.lastValue).isNull() + } + + @Test + fun shouldNotTriggerCallback_whenKeyUpReceived_ifKeyDownReceivedBeforeLosingFocus() { + underTest.sendKey(KeyEvent.ACTION_DOWN, KEYCODE_DPAD_LEFT) + underTest.onFocusChange(view, hasFocus = false) + underTest.onFocusChange(view, hasFocus = true) + + underTest.sendKey(KeyEvent.ACTION_UP, KEYCODE_DPAD_LEFT) + + assertThat(callback.lastValue).isNull() + } + + private fun LeftRightArrowPressedListener.sendKey(action: Int, keyCode: Int) { + onKey(view, keyCode, KeyEvent(action, keyCode)) + } + + private fun LeftRightArrowPressedListener.sendKeyWithRepeat( + action: Int, + keyCode: Int, + repeat: Int + ) { + val keyEvent = + KeyEvent( + /* downTime= */ 0L, + /* eventTime= */ 0L, + /* action= */ action, + /* code= */ KEYCODE_DPAD_LEFT, + /* repeat= */ repeat + ) + onKey(view, keyCode, keyEvent) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/PagedTileLayoutTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/PagedTileLayoutTest.kt index db9e548e74c8..8ef3f57103a0 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/PagedTileLayoutTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/PagedTileLayoutTest.kt @@ -2,11 +2,13 @@ package com.android.systemui.qs import android.content.Context import android.testing.AndroidTestingRunner -import android.view.KeyEvent import android.view.View import android.widget.Scroller import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.qs.PageIndicator.PageScrollActionListener +import com.android.systemui.qs.PageIndicator.PageScrollActionListener.LEFT +import com.android.systemui.qs.PageIndicator.PageScrollActionListener.RIGHT import com.google.common.truth.Truth.assertThat import org.junit.Before import org.junit.Test @@ -22,7 +24,7 @@ import org.mockito.MockitoAnnotations class PagedTileLayoutTest : SysuiTestCase() { @Mock private lateinit var pageIndicator: PageIndicator - @Captor private lateinit var captor: ArgumentCaptor<View.OnKeyListener> + @Captor private lateinit var captor: ArgumentCaptor<PageScrollActionListener> private lateinit var pageTileLayout: TestPagedTileLayout private lateinit var scroller: Scroller @@ -32,7 +34,7 @@ class PagedTileLayoutTest : SysuiTestCase() { MockitoAnnotations.initMocks(this) pageTileLayout = TestPagedTileLayout(mContext) pageTileLayout.setPageIndicator(pageIndicator) - verify(pageIndicator).setOnKeyListener(captor.capture()) + verify(pageIndicator).setPageScrollActionListener(captor.capture()) setViewWidth(pageTileLayout, width = PAGE_WIDTH) scroller = pageTileLayout.mScroller } @@ -43,28 +45,27 @@ class PagedTileLayoutTest : SysuiTestCase() { } @Test - fun scrollsRight_afterRightArrowPressed_whenFocusOnPagerIndicator() { + fun scrollsRight_afterRightScrollActionTriggered() { pageTileLayout.currentPageIndex = 0 - sendUpEvent(KeyEvent.KEYCODE_DPAD_RIGHT) + sendScrollActionEvent(RIGHT) assertThat(scroller.isFinished).isFalse() // aka we're scrolling assertThat(scroller.finalX).isEqualTo(scroller.currX + PAGE_WIDTH) } @Test - fun scrollsLeft_afterLeftArrowPressed_whenFocusOnPagerIndicator() { + fun scrollsLeft_afterLeftScrollActionTriggered() { pageTileLayout.currentPageIndex = 1 // we won't scroll left if we're on the first page - sendUpEvent(KeyEvent.KEYCODE_DPAD_LEFT) + sendScrollActionEvent(LEFT) assertThat(scroller.isFinished).isFalse() // aka we're scrolling assertThat(scroller.finalX).isEqualTo(scroller.currX - PAGE_WIDTH) } - private fun sendUpEvent(keyCode: Int) { - val event = KeyEvent(KeyEvent.ACTION_UP, keyCode) - captor.value.onKey(pageIndicator, keyCode, event) + private fun sendScrollActionEvent(@PageScrollActionListener.Direction direction: Int) { + captor.value.onScrollActionTriggered(direction) } /** diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModelTest.kt index c7118066a276..1cb3bf6c7100 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModelTest.kt @@ -393,23 +393,23 @@ class FooterActionsViewModelTest : SysuiTestCase() { } @Test - fun backgroundAlpha_inSplitShade_followsExpansion_with_0_99_delay() { + fun backgroundAlpha_inSplitShade_followsExpansion_with_0_15_delay() { val underTest = utils.footerActionsViewModel() val floatTolerance = 0.01f underTest.onQuickSettingsExpansionChanged(0f, isInSplitShade = true) assertThat(underTest.backgroundAlpha.value).isEqualTo(0f) - underTest.onQuickSettingsExpansionChanged(0.5f, isInSplitShade = true) + underTest.onQuickSettingsExpansionChanged(0.1f, isInSplitShade = true) assertThat(underTest.backgroundAlpha.value).isEqualTo(0f) - underTest.onQuickSettingsExpansionChanged(0.9f, isInSplitShade = true) + underTest.onQuickSettingsExpansionChanged(0.14f, isInSplitShade = true) assertThat(underTest.backgroundAlpha.value).isEqualTo(0f) - underTest.onQuickSettingsExpansionChanged(0.991f, isInSplitShade = true) + underTest.onQuickSettingsExpansionChanged(0.235f, isInSplitShade = true) assertThat(underTest.backgroundAlpha.value).isWithin(floatTolerance).of(0.1f) - underTest.onQuickSettingsExpansionChanged(0.995f, isInSplitShade = true) + underTest.onQuickSettingsExpansionChanged(0.575f, isInSplitShade = true) assertThat(underTest.backgroundAlpha.value).isWithin(floatTolerance).of(0.5f) underTest.onQuickSettingsExpansionChanged(1f, isInSplitShade = true) diff --git a/packages/SystemUI/tests/src/com/android/systemui/scene/shared/flag/SceneContainerFlagsTest.kt b/packages/SystemUI/tests/src/com/android/systemui/scene/shared/flag/SceneContainerFlagsTest.kt index 994166172aff..543f6c91513e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/scene/shared/flag/SceneContainerFlagsTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/scene/shared/flag/SceneContainerFlagsTest.kt @@ -16,17 +16,13 @@ package com.android.systemui.scene.shared.flag +import android.platform.test.annotations.DisableFlags import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest +import com.android.systemui.Flags.FLAG_SCENE_CONTAINER import com.android.systemui.SysuiTestCase -import com.android.systemui.compose.ComposeFacade -import com.android.systemui.flags.Flags -import com.android.systemui.flags.setFlagValue -import com.android.systemui.keyguard.shared.KeyguardShadeMigrationNssl -import com.android.systemui.media.controls.util.MediaInSceneContainerFlag +import com.android.systemui.flags.EnableSceneContainer import com.google.common.truth.Truth -import org.junit.Assume -import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -34,45 +30,17 @@ import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) internal class SceneContainerFlagsTest : SysuiTestCase() { - @Before - fun setUp() { - // TODO(b/283300105): remove this reflection setting once the hard-coded - // Flags.SCENE_CONTAINER_ENABLED is no longer needed. - val field = Flags::class.java.getField("SCENE_CONTAINER_ENABLED") - field.isAccessible = true - field.set(null, true) // note: this does not work with multivalent tests - } - - private fun setAconfigFlagsEnabled(enabled: Boolean) { - listOf( - com.android.systemui.Flags.FLAG_SCENE_CONTAINER, - com.android.systemui.Flags.FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR, - KeyguardShadeMigrationNssl.FLAG_NAME, - MediaInSceneContainerFlag.FLAG_NAME, - ) - .forEach { flagName -> mSetFlagsRule.setFlagValue(flagName, enabled) } - } - @Test + @DisableFlags(FLAG_SCENE_CONTAINER) fun isNotEnabled_withoutAconfigFlags() { - setAconfigFlagsEnabled(false) Truth.assertThat(SceneContainerFlag.isEnabled).isEqualTo(false) Truth.assertThat(SceneContainerFlagsImpl().isEnabled()).isEqualTo(false) } @Test - fun isEnabled_withAconfigFlags_withCompose() { - Assume.assumeTrue(ComposeFacade.isComposeAvailable()) - setAconfigFlagsEnabled(true) + @EnableSceneContainer + fun isEnabled_withAconfigFlags() { Truth.assertThat(SceneContainerFlag.isEnabled).isEqualTo(true) Truth.assertThat(SceneContainerFlagsImpl().isEnabled()).isEqualTo(true) } - - @Test - fun isNotEnabled_withAconfigFlags_withoutCompose() { - Assume.assumeFalse(ComposeFacade.isComposeAvailable()) - setAconfigFlagsEnabled(true) - Truth.assertThat(SceneContainerFlag.isEnabled).isEqualTo(false) - Truth.assertThat(SceneContainerFlagsImpl().isEnabled()).isEqualTo(false) - } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java index 49579f6f46b4..b3e386e69905 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java @@ -113,6 +113,7 @@ import com.android.systemui.keyguard.ui.viewmodel.LockscreenToDreamingTransition import com.android.systemui.keyguard.ui.viewmodel.LockscreenToOccludedTransitionViewModel; import com.android.systemui.keyguard.ui.viewmodel.OccludedToLockscreenTransitionViewModel; import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToGoneTransitionViewModel; +import com.android.systemui.kosmos.KosmosJavaAdapter; import com.android.systemui.media.controls.pipeline.MediaDataManager; import com.android.systemui.media.controls.ui.KeyguardMediaController; import com.android.systemui.media.controls.ui.MediaHierarchyManager; @@ -125,7 +126,6 @@ import com.android.systemui.plugins.qs.QS; import com.android.systemui.power.domain.interactor.PowerInteractor; import com.android.systemui.qs.QSFragmentLegacy; import com.android.systemui.res.R; -import com.android.systemui.scene.SceneTestUtils; import com.android.systemui.screenrecord.RecordingController; import com.android.systemui.shade.data.repository.FakeShadeRepository; import com.android.systemui.shade.data.repository.ShadeAnimationRepository; @@ -355,8 +355,8 @@ public class NotificationPanelViewControllerBaseTest extends SysuiTestCase { protected FakeKeyguardRepository mFakeKeyguardRepository; protected KeyguardInteractor mKeyguardInteractor; protected ShadeAnimationInteractor mShadeAnimationInteractor; - protected SceneTestUtils mUtils = new SceneTestUtils(this); - protected TestScope mTestScope = mUtils.getTestScope(); + protected KosmosJavaAdapter mKosmos = new KosmosJavaAdapter(this); + protected TestScope mTestScope = mKosmos.getTestScope(); protected ShadeInteractor mShadeInteractor; protected PowerInteractor mPowerInteractor; protected NotificationPanelViewController.TouchHandler mTouchHandler; 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 791c080e9219..a894f877fe3c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java @@ -73,11 +73,11 @@ import com.android.systemui.keyguard.domain.interactor.GlanceableHubTransitions; import com.android.systemui.keyguard.domain.interactor.InWindowLauncherUnlockAnimationInteractor; import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor; import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor; +import com.android.systemui.kosmos.KosmosJavaAdapter; 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.scene.FakeWindowRootViewComponent; -import com.android.systemui.scene.SceneTestUtils; import com.android.systemui.scene.data.repository.SceneContainerRepository; import com.android.systemui.scene.domain.interactor.SceneInteractor; import com.android.systemui.scene.shared.flag.FakeSceneContainerFlags; @@ -131,7 +131,6 @@ public class NotificationShadeWindowControllerImplTest extends SysuiTestCase { @Spy private final NotificationShadeWindowView mNotificationShadeWindowView = spy( new NotificationShadeWindowView(mContext, null)); @Mock private IActivityManager mActivityManager; - @Mock private SysuiStatusBarStateController mStatusBarStateController; @Mock private ConfigurationController mConfigurationController; @Mock private KeyguardViewMediator mKeyguardViewMediator; @Mock private KeyguardBypassController mKeyguardBypassController; @@ -140,7 +139,6 @@ public class NotificationShadeWindowControllerImplTest extends SysuiTestCase { @Mock private DumpManager mDumpManager; @Mock private KeyguardSecurityModel mKeyguardSecurityModel; @Mock private KeyguardStateController mKeyguardStateController; - @Mock private ScreenOffAnimationController mScreenOffAnimationController; @Mock private AuthController mAuthController; @Mock private ShadeWindowLogger mShadeWindowLogger; @Mock private SelectedUserInteractor mSelectedUserInteractor; @@ -152,14 +150,16 @@ public class NotificationShadeWindowControllerImplTest extends SysuiTestCase { private final Executor mMainExecutor = MoreExecutors.directExecutor(); private final Executor mBackgroundExecutor = MoreExecutors.directExecutor(); - private final SceneTestUtils mUtils = new SceneTestUtils(this); - private final TestScope mTestScope = mUtils.getTestScope(); + private final KosmosJavaAdapter mKosmos = new KosmosJavaAdapter(this); + private final TestScope mTestScope = mKosmos.getTestScope(); private ShadeInteractor mShadeInteractor; private NotificationShadeWindowControllerImpl mNotificationShadeWindowController; private float mPreferredRefreshRate = -1; private FromLockscreenTransitionInteractor mFromLockscreenTransitionInteractor; private FromPrimaryBouncerTransitionInteractor mFromPrimaryBouncerTransitionInteractor; + private ScreenOffAnimationController mScreenOffAnimationController; + private SysuiStatusBarStateController mStatusBarStateController; @Before public void setUp() { @@ -178,17 +178,15 @@ public class NotificationShadeWindowControllerImplTest extends SysuiTestCase { FakeFeatureFlagsClassic featureFlags = new FakeFeatureFlagsClassic(); FakeShadeRepository shadeRepository = new FakeShadeRepository(); - PowerInteractor powerInteractor = mUtils.powerInteractor( - mUtils.getPowerRepository(), - mUtils.falsingCollector(), - mScreenOffAnimationController, - mStatusBarStateController); + mScreenOffAnimationController = mKosmos.getScreenOffAnimationController(); + mStatusBarStateController = spy(mKosmos.getStatusBarStateController()); + PowerInteractor powerInteractor = mKosmos.getPowerInteractor(); SceneInteractor sceneInteractor = new SceneInteractor( mTestScope.getBackgroundScope(), new SceneContainerRepository( mTestScope.getBackgroundScope(), - mUtils.fakeSceneContainerConfig()), + mKosmos.getFakeSceneContainerConfig()), powerInteractor, mock(SceneLogger.class)); @@ -221,8 +219,8 @@ public class NotificationShadeWindowControllerImplTest extends SysuiTestCase { keyguardTransitionRepository, keyguardTransitionInteractor, mTestScope.getBackgroundScope(), - mUtils.getTestDispatcher(), - mUtils.getTestDispatcher(), + mKosmos.getTestDispatcher(), + mKosmos.getTestDispatcher(), keyguardInteractor, featureFlags, shadeRepository, @@ -247,8 +245,8 @@ public class NotificationShadeWindowControllerImplTest extends SysuiTestCase { keyguardTransitionRepository, keyguardTransitionInteractor, mTestScope.getBackgroundScope(), - mUtils.getTestDispatcher(), - mUtils.getTestDispatcher(), + mKosmos.getTestDispatcher(), + mKosmos.getTestDispatcher(), keyguardInteractor, featureFlags, mKeyguardSecurityModel, diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationsQSContainerControllerLegacyTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationsQSContainerControllerLegacyTest.kt index ea3caa380cf2..697b05aa9add 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationsQSContainerControllerLegacyTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationsQSContainerControllerLegacyTest.kt @@ -26,6 +26,7 @@ import androidx.annotation.IdRes import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintSet import androidx.test.filters.SmallTest +import com.android.systemui.Flags.FLAG_CENTRALIZED_STATUS_BAR_DIMENS_REFACTOR import com.android.systemui.SysuiTestCase import com.android.systemui.flags.FakeFeatureFlags import com.android.systemui.flags.Flags @@ -167,10 +168,38 @@ class NotificationsQSContainerControllerLegacyTest : SysuiTestCase() { } @Test - fun testLargeScreen_updateResources_splitShadeHeightIsSet() { + fun testLargeScreen_updateResources_refactorFlagOff_splitShadeHeightIsSetBasedOnResource() { + val headerResourceHeight = 20 + val headerHelperHeight = 30 + mSetFlagsRule.disableFlags(FLAG_CENTRALIZED_STATUS_BAR_DIMENS_REFACTOR) + whenever(largeScreenHeaderHelper.getLargeScreenHeaderHeight()) + .thenReturn(headerHelperHeight) overrideResource(R.bool.config_use_large_screen_shade_header, true) overrideResource(R.dimen.qs_header_height, 10) - overrideResource(R.dimen.large_screen_shade_header_height, 20) + overrideResource(R.dimen.large_screen_shade_header_height, headerResourceHeight) + + // ensure the estimated height (would be 3 here) wouldn't impact this test case + overrideResource(R.dimen.large_screen_shade_header_min_height, 1) + overrideResource(R.dimen.new_qs_header_non_clickable_element_height, 1) + + underTest.updateResources() + + val captor = ArgumentCaptor.forClass(ConstraintSet::class.java) + verify(view).applyConstraints(capture(captor)) + assertThat(captor.value.getHeight(R.id.split_shade_status_bar)) + .isEqualTo(headerResourceHeight) + } + + @Test + fun testLargeScreen_updateResources_refactorFlagOn_splitShadeHeightIsSetBasedOnHelper() { + val headerResourceHeight = 20 + val headerHelperHeight = 30 + mSetFlagsRule.enableFlags(FLAG_CENTRALIZED_STATUS_BAR_DIMENS_REFACTOR) + whenever(largeScreenHeaderHelper.getLargeScreenHeaderHeight()) + .thenReturn(headerHelperHeight) + overrideResource(R.bool.config_use_large_screen_shade_header, true) + overrideResource(R.dimen.qs_header_height, 10) + overrideResource(R.dimen.large_screen_shade_header_height, headerResourceHeight) // ensure the estimated height (would be 3 here) wouldn't impact this test case overrideResource(R.dimen.large_screen_shade_header_min_height, 1) @@ -180,7 +209,8 @@ class NotificationsQSContainerControllerLegacyTest : SysuiTestCase() { val captor = ArgumentCaptor.forClass(ConstraintSet::class.java) verify(view).applyConstraints(capture(captor)) - assertThat(captor.value.getHeight(R.id.split_shade_status_bar)).isEqualTo(20) + assertThat(captor.value.getHeight(R.id.split_shade_status_bar)) + .isEqualTo(headerHelperHeight) } @Test @@ -416,10 +446,36 @@ class NotificationsQSContainerControllerLegacyTest : SysuiTestCase() { } @Test - fun testLargeScreenLayout_qsAndNotifsTopMarginIsOfHeaderHeight() { + fun testLargeScreenLayout_refactorFlagOff_qsAndNotifsTopMarginIsOfHeaderHeightResource() { + mSetFlagsRule.disableFlags(FLAG_CENTRALIZED_STATUS_BAR_DIMENS_REFACTOR) + setLargeScreen() + val largeScreenHeaderResourceHeight = 100 + val largeScreenHeaderHelperHeight = 200 + whenever(largeScreenHeaderHelper.getLargeScreenHeaderHeight()) + .thenReturn(largeScreenHeaderHelperHeight) + overrideResource(R.dimen.large_screen_shade_header_height, largeScreenHeaderResourceHeight) + + // ensure the estimated height (would be 30 here) wouldn't impact this test case + overrideResource(R.dimen.large_screen_shade_header_min_height, 10) + overrideResource(R.dimen.new_qs_header_non_clickable_element_height, 10) + + underTest.updateResources() + + assertThat(getConstraintSetLayout(R.id.qs_frame).topMargin) + .isEqualTo(largeScreenHeaderResourceHeight) + assertThat(getConstraintSetLayout(R.id.notification_stack_scroller).topMargin) + .isEqualTo(largeScreenHeaderResourceHeight) + } + + @Test + fun testLargeScreenLayout_refactorFlagOn_qsAndNotifsTopMarginIsOfHeaderHeightHelper() { + mSetFlagsRule.enableFlags(FLAG_CENTRALIZED_STATUS_BAR_DIMENS_REFACTOR) setLargeScreen() - val largeScreenHeaderHeight = 100 - overrideResource(R.dimen.large_screen_shade_header_height, largeScreenHeaderHeight) + val largeScreenHeaderResourceHeight = 100 + val largeScreenHeaderHelperHeight = 200 + whenever(largeScreenHeaderHelper.getLargeScreenHeaderHeight()) + .thenReturn(largeScreenHeaderHelperHeight) + overrideResource(R.dimen.large_screen_shade_header_height, largeScreenHeaderResourceHeight) // ensure the estimated height (would be 30 here) wouldn't impact this test case overrideResource(R.dimen.large_screen_shade_header_min_height, 10) @@ -428,9 +484,9 @@ class NotificationsQSContainerControllerLegacyTest : SysuiTestCase() { underTest.updateResources() assertThat(getConstraintSetLayout(R.id.qs_frame).topMargin) - .isEqualTo(largeScreenHeaderHeight) + .isEqualTo(largeScreenHeaderHelperHeight) assertThat(getConstraintSetLayout(R.id.notification_stack_scroller).topMargin) - .isEqualTo(largeScreenHeaderHeight) + .isEqualTo(largeScreenHeaderHelperHeight) } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationsQSContainerControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationsQSContainerControllerTest.kt index c1bc303f26ea..e66251a030a3 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationsQSContainerControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationsQSContainerControllerTest.kt @@ -26,6 +26,7 @@ import androidx.annotation.IdRes import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintSet import androidx.test.filters.SmallTest +import com.android.systemui.Flags.FLAG_CENTRALIZED_STATUS_BAR_DIMENS_REFACTOR import com.android.systemui.SysuiTestCase import com.android.systemui.flags.FakeFeatureFlags import com.android.systemui.flags.Flags @@ -166,10 +167,14 @@ class NotificationsQSContainerControllerTest : SysuiTestCase() { } @Test - fun testLargeScreen_updateResources_splitShadeHeightIsSet() { + fun testLargeScreen_updateResources_refactorFlagOff_splitShadeHeightIsSet_basedOnResource() { + mSetFlagsRule.disableFlags(FLAG_CENTRALIZED_STATUS_BAR_DIMENS_REFACTOR) + val helperHeight = 30 + val resourceHeight = 20 + whenever(largeScreenHeaderHelper.getLargeScreenHeaderHeight()).thenReturn(helperHeight) overrideResource(R.bool.config_use_large_screen_shade_header, true) overrideResource(R.dimen.qs_header_height, 10) - overrideResource(R.dimen.large_screen_shade_header_height, 20) + overrideResource(R.dimen.large_screen_shade_header_height, resourceHeight) // ensure the estimated height (would be 3 here) wouldn't impact this test case overrideResource(R.dimen.large_screen_shade_header_min_height, 1) @@ -179,7 +184,28 @@ class NotificationsQSContainerControllerTest : SysuiTestCase() { val captor = ArgumentCaptor.forClass(ConstraintSet::class.java) verify(view).applyConstraints(capture(captor)) - assertThat(captor.value.getHeight(R.id.split_shade_status_bar)).isEqualTo(20) + assertThat(captor.value.getHeight(R.id.split_shade_status_bar)).isEqualTo(resourceHeight) + } + + @Test + fun testLargeScreen_updateResources_refactorFlagOn_splitShadeHeightIsSet_basedOnHelper() { + mSetFlagsRule.enableFlags(FLAG_CENTRALIZED_STATUS_BAR_DIMENS_REFACTOR) + val helperHeight = 30 + val resourceHeight = 20 + whenever(largeScreenHeaderHelper.getLargeScreenHeaderHeight()).thenReturn(helperHeight) + overrideResource(R.bool.config_use_large_screen_shade_header, true) + overrideResource(R.dimen.qs_header_height, 10) + overrideResource(R.dimen.large_screen_shade_header_height, resourceHeight) + + // ensure the estimated height (would be 3 here) wouldn't impact this test case + overrideResource(R.dimen.large_screen_shade_header_min_height, 1) + overrideResource(R.dimen.new_qs_header_non_clickable_element_height, 1) + + underTest.updateResources() + + val captor = ArgumentCaptor.forClass(ConstraintSet::class.java) + verify(view).applyConstraints(capture(captor)) + assertThat(captor.value.getHeight(R.id.split_shade_status_bar)).isEqualTo(helperHeight) } @Test @@ -404,10 +430,34 @@ class NotificationsQSContainerControllerTest : SysuiTestCase() { } @Test - fun testLargeScreenLayout_qsAndNotifsTopMarginIsOfHeaderHeight() { + fun testLargeScreenLayout_refactorFlagOff_qsAndNotifsTopMarginIsOfHeaderResourceHeight() { + mSetFlagsRule.disableFlags(FLAG_CENTRALIZED_STATUS_BAR_DIMENS_REFACTOR) + setLargeScreen() + val largeScreenHeaderHelperHeight = 200 + val largeScreenHeaderResourceHeight = 100 + whenever(largeScreenHeaderHelper.getLargeScreenHeaderHeight()) + .thenReturn(largeScreenHeaderHelperHeight) + overrideResource(R.dimen.large_screen_shade_header_height, largeScreenHeaderResourceHeight) + + // ensure the estimated height (would be 30 here) wouldn't impact this test case + overrideResource(R.dimen.large_screen_shade_header_min_height, 10) + overrideResource(R.dimen.new_qs_header_non_clickable_element_height, 10) + + underTest.updateResources() + + assertThat(getConstraintSetLayout(R.id.qs_frame).topMargin) + .isEqualTo(largeScreenHeaderResourceHeight) + } + + @Test + fun testLargeScreenLayout_refactorFlagOn_qsAndNotifsTopMarginIsOfHeaderHelperHeight() { + mSetFlagsRule.enableFlags(FLAG_CENTRALIZED_STATUS_BAR_DIMENS_REFACTOR) setLargeScreen() - val largeScreenHeaderHeight = 100 - overrideResource(R.dimen.large_screen_shade_header_height, largeScreenHeaderHeight) + val largeScreenHeaderHelperHeight = 200 + val largeScreenHeaderResourceHeight = 100 + whenever(largeScreenHeaderHelper.getLargeScreenHeaderHeight()) + .thenReturn(largeScreenHeaderHelperHeight) + overrideResource(R.dimen.large_screen_shade_header_height, largeScreenHeaderResourceHeight) // ensure the estimated height (would be 30 here) wouldn't impact this test case overrideResource(R.dimen.large_screen_shade_header_min_height, 10) @@ -416,7 +466,7 @@ class NotificationsQSContainerControllerTest : SysuiTestCase() { underTest.updateResources() assertThat(getConstraintSetLayout(R.id.qs_frame).topMargin) - .isEqualTo(largeScreenHeaderHeight) + .isEqualTo(largeScreenHeaderHelperHeight) } @Test 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 a369f8280190..cbd4d2bfe377 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerBaseTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerBaseTest.java @@ -61,6 +61,7 @@ import com.android.systemui.keyguard.domain.interactor.GlanceableHubTransitions; import com.android.systemui.keyguard.domain.interactor.InWindowLauncherUnlockAnimationInteractor; import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor; import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor; +import com.android.systemui.kosmos.KosmosJavaAdapter; import com.android.systemui.media.controls.pipeline.MediaDataManager; import com.android.systemui.media.controls.ui.MediaHierarchyManager; import com.android.systemui.plugins.FalsingManager; @@ -68,7 +69,6 @@ import com.android.systemui.plugins.qs.QS; import com.android.systemui.power.domain.interactor.PowerInteractor; import com.android.systemui.qs.QSFragmentLegacy; import com.android.systemui.res.R; -import com.android.systemui.scene.SceneTestUtils; import com.android.systemui.scene.data.repository.SceneContainerRepository; import com.android.systemui.scene.domain.interactor.SceneInteractor; import com.android.systemui.scene.shared.flag.FakeSceneContainerFlags; @@ -85,7 +85,6 @@ import com.android.systemui.statusbar.NotificationRemoteInputManager; import com.android.systemui.statusbar.NotificationShadeDepthController; import com.android.systemui.statusbar.PulseExpansionHandler; import com.android.systemui.statusbar.QsFrameTranslateController; -import com.android.systemui.statusbar.StatusBarStateControllerImpl; import com.android.systemui.statusbar.SysuiStatusBarStateController; import com.android.systemui.statusbar.disableflags.data.repository.FakeDisableFlagsRepository; import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationListRepository; @@ -99,7 +98,6 @@ import com.android.systemui.statusbar.phone.KeyguardBypassController; import com.android.systemui.statusbar.phone.KeyguardStatusBarView; import com.android.systemui.statusbar.phone.LightBarController; import com.android.systemui.statusbar.phone.LockscreenGestureLogger; -import com.android.systemui.statusbar.phone.ScreenOffAnimationController; import com.android.systemui.statusbar.phone.ScrimController; import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager; import com.android.systemui.statusbar.phone.StatusBarTouchableRegionManager; @@ -133,8 +131,8 @@ public class QuickSettingsControllerBaseTest extends SysuiTestCase { protected QuickSettingsController mQsController; - protected SceneTestUtils mUtils = new SceneTestUtils(this); - protected TestScope mTestScope = mUtils.getTestScope(); + protected KosmosJavaAdapter mKosmos = new KosmosJavaAdapter(this); + protected TestScope mTestScope = mKosmos.getTestScope(); @Mock protected Resources mResources; @Mock protected KeyguardBottomAreaView mQsFrame; @@ -172,7 +170,6 @@ public class QuickSettingsControllerBaseTest extends SysuiTestCase { @Mock protected LockscreenGestureLogger mLockscreenGestureLogger; @Mock protected MetricsLogger mMetricsLogger; @Mock protected FeatureFlags mFeatureFlags; - @Mock protected InteractionJankMonitor mInteractionJankMonitor; @Mock protected ShadeLogger mShadeLogger; @Mock protected DumpManager mDumpManager; @Mock protected UiEventLogger mUiEventLogger; @@ -186,6 +183,7 @@ public class QuickSettingsControllerBaseTest extends SysuiTestCase { protected FakeKeyguardRepository mKeyguardRepository = new FakeKeyguardRepository(); protected FakeShadeRepository mShadeRepository = new FakeShadeRepository(); + protected InteractionJankMonitor mInteractionJankMonitor; protected SysuiStatusBarStateController mStatusBarStateController; protected ShadeInteractor mShadeInteractor; @@ -205,8 +203,8 @@ public class QuickSettingsControllerBaseTest extends SysuiTestCase { public void setup() { MockitoAnnotations.initMocks(this); when(mPanelViewControllerLazy.get()).thenReturn(mNotificationPanelViewController); - mStatusBarStateController = new StatusBarStateControllerImpl(mUiEventLogger, - mInteractionJankMonitor, mock(JavaAdapter.class), () -> mShadeInteractor); + mStatusBarStateController = mKosmos.getStatusBarStateController(); + mInteractionJankMonitor = mKosmos.getInteractionJankMonitor(); FakeDeviceProvisioningRepository deviceProvisioningRepository = new FakeDeviceProvisioningRepository(); @@ -214,17 +212,13 @@ public class QuickSettingsControllerBaseTest extends SysuiTestCase { FakeFeatureFlagsClassic featureFlags = new FakeFeatureFlagsClassic(); FakeConfigurationRepository configurationRepository = new FakeConfigurationRepository(); - PowerInteractor powerInteractor = mUtils.powerInteractor( - mUtils.getPowerRepository(), - mUtils.falsingCollector(), - mock(ScreenOffAnimationController.class), - mStatusBarStateController); + PowerInteractor powerInteractor = mKosmos.getPowerInteractor(); SceneInteractor sceneInteractor = new SceneInteractor( mTestScope.getBackgroundScope(), new SceneContainerRepository( mTestScope.getBackgroundScope(), - mUtils.fakeSceneContainerConfig()), + mKosmos.getFakeSceneContainerConfig()), powerInteractor, mock(SceneLogger.class)); @@ -256,8 +250,8 @@ public class QuickSettingsControllerBaseTest extends SysuiTestCase { keyguardTransitionRepository, keyguardTransitionInteractor, mTestScope.getBackgroundScope(), - mUtils.getTestDispatcher(), - mUtils.getTestDispatcher(), + mKosmos.getTestDispatcher(), + mKosmos.getTestDispatcher(), keyguardInteractor, featureFlags, mShadeRepository, @@ -282,8 +276,8 @@ public class QuickSettingsControllerBaseTest extends SysuiTestCase { keyguardTransitionRepository, keyguardTransitionInteractor, mTestScope.getBackgroundScope(), - mUtils.getTestDispatcher(), - mUtils.getTestDispatcher(), + mKosmos.getTestDispatcher(), + mKosmos.getTestDispatcher(), keyguardInteractor, featureFlags, mock(KeyguardSecurityModel.class), 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 215f8b1c462f..c4911a41b4a7 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/ShadeControllerImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/ShadeControllerImplTest.kt @@ -33,6 +33,8 @@ import com.android.systemui.scene.data.repository.WindowRootViewVisibilityReposi import com.android.systemui.scene.domain.interactor.WindowRootViewVisibilityInteractor import com.android.systemui.statusbar.CommandQueue import com.android.systemui.statusbar.NotificationShadeWindowController +import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationListRepository +import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor import com.android.systemui.statusbar.notification.row.NotificationGutsManager import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager import com.android.systemui.statusbar.policy.DeviceProvisionedController @@ -45,6 +47,7 @@ import com.android.systemui.util.mockito.whenever import com.android.systemui.util.time.FakeSystemClock import com.google.common.truth.Truth.assertThat import dagger.Lazy +import kotlinx.coroutines.test.StandardTestDispatcher import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -59,6 +62,8 @@ import org.mockito.MockitoAnnotations @SmallTest class ShadeControllerImplTest : SysuiTestCase() { private val executor = FakeExecutor(FakeSystemClock()) + private val testDispatcher = StandardTestDispatcher() + private val activeNotificationsRepository = ActiveNotificationListRepository() @Mock private lateinit var commandQueue: CommandQueue @Mock private lateinit var keyguardStateController: KeyguardStateController @@ -84,6 +89,7 @@ class ShadeControllerImplTest : SysuiTestCase() { FakeKeyguardRepository(), headsUpManager, PowerInteractorFactory.create().powerInteractor, + ActiveNotificationsInteractor(activeNotificationsRepository, testDispatcher) ) } 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 4a365b9ab084..05e866e85112 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt @@ -41,10 +41,12 @@ import com.android.systemui.keyguard.domain.interactor.GlanceableHubTransitions import com.android.systemui.keyguard.domain.interactor.InWindowLauncherUnlockAnimationInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor +import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.kosmos.testScope import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.power.data.repository.FakePowerRepository import com.android.systemui.power.domain.interactor.PowerInteractor -import com.android.systemui.scene.SceneTestUtils +import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.scene.shared.flag.FakeSceneContainerFlags import com.android.systemui.shade.LargeScreenHeaderHelper import com.android.systemui.shade.data.repository.FakeShadeRepository @@ -56,6 +58,7 @@ import com.android.systemui.statusbar.notification.stack.domain.interactor.Share import com.android.systemui.statusbar.policy.ResourcesSplitShadeStateController import com.android.systemui.statusbar.policy.data.repository.FakeDeviceProvisioningRepository import com.android.systemui.statusbar.policy.data.repository.FakeUserSetupRepository +import com.android.systemui.testKosmos import com.android.systemui.util.mockito.mock import kotlinx.coroutines.flow.emptyFlow import org.junit.Assert.assertEquals @@ -71,17 +74,17 @@ import org.mockito.ArgumentMatchers.eq import org.mockito.Mockito import org.mockito.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) @TestableLooper.RunWithLooper class StatusBarStateControllerImplTest : SysuiTestCase() { - private val utils = SceneTestUtils(this) - private val testScope = utils.testScope - private val testDispatcher = utils.testDispatcher + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + private val testDispatcher = kosmos.testDispatcher private lateinit var shadeInteractor: ShadeInteractor private lateinit var fromLockscreenTransitionInteractor: FromLockscreenTransitionInteractor private lateinit var fromPrimaryBouncerTransitionInteractor: @@ -131,7 +134,7 @@ class StatusBarStateControllerImplTest : SysuiTestCase() { FakeKeyguardBouncerRepository(), ConfigurationInteractor(configurationRepository), shadeRepository, - utils::sceneInteractor + { kosmos.sceneInteractor }, ) val keyguardTransitionInteractor = KeyguardTransitionInteractor( 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 65697b736cbd..36f643ab9cca 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.AndroidTestingRunner 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.Test 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 @@ class ConversationCoordinatorTest : SysuiTestCase() { 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 @@ class ConversationCoordinatorTest : SysuiTestCase() { 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 @@ class ConversationCoordinatorTest : SysuiTestCase() { 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 @@ class ConversationCoordinatorTest : SysuiTestCase() { @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 @@ class ConversationCoordinatorTest : SysuiTestCase() { @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/domain/interactor/ActiveNotificationsInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractorTest.kt index 4ab3cd49b297..9b641f014c01 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractorTest.kt @@ -50,6 +50,18 @@ class ActiveNotificationsInteractorTest : SysuiTestCase() { DaggerActiveNotificationsInteractorTest_TestComponent.factory().create(test = this) @Test + fun testAllNotificationsCount() = + testComponent.runTest { + val count by collectLastValue(underTest.allNotificationsCount) + + activeNotificationListRepository.setActiveNotifs(5) + runCurrent() + + assertThat(count).isEqualTo(5) + assertThat(underTest.allNotificationsCountValue).isEqualTo(5) + } + + @Test fun testAreAnyNotificationsPresent_isTrue() = testComponent.runTest { val areAnyNotificationsPresent by collectLastValue(underTest.areAnyNotificationsPresent) @@ -74,6 +86,18 @@ class ActiveNotificationsInteractorTest : SysuiTestCase() { } @Test + fun testActiveNotificationRanks_sizeMatches() { + testComponent.runTest { + val activeNotificationRanks by collectLastValue(underTest.activeNotificationRanks) + + activeNotificationListRepository.setActiveNotifs(5) + runCurrent() + + assertThat(activeNotificationRanks!!.size).isEqualTo(5) + } + } + + @Test fun testHasClearableNotifications_whenHasClearableAlertingNotifs() = testComponent.runTest { val hasClearable by collectLastValue(underTest.hasClearableNotifications) diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/domain/interactor/RenderNotificationsListInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/domain/interactor/RenderNotificationsListInteractorTest.kt index 6374d5e259fc..334776ca0bcc 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/domain/interactor/RenderNotificationsListInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/domain/interactor/RenderNotificationsListInteractorTest.kt @@ -15,10 +15,12 @@ package com.android.systemui.statusbar.notification.domain.interactor +import android.service.notification.StatusBarNotification import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue -import com.android.systemui.statusbar.notification.collection.ListEntry +import com.android.systemui.statusbar.RankingBuilder +import com.android.systemui.statusbar.notification.collection.GroupEntry import com.android.systemui.statusbar.notification.collection.NotificationEntry import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationListRepository import com.android.systemui.statusbar.notification.shared.byKey @@ -49,22 +51,101 @@ class RenderNotificationsListInteractorTest : SysuiTestCase() { testScope.runTest { val notifs by collectLastValue(notifsInteractor.topLevelRepresentativeNotifications) val keys = (1..50).shuffled().map { "$it" } - val entries = - keys.map { - mock<ListEntry> { - val mockRep = - mock<NotificationEntry> { - whenever(key).thenReturn(it) - whenever(sbn).thenReturn(mock()) - whenever(icons).thenReturn(mock()) - } - whenever(representativeEntry).thenReturn(mockRep) - } - } + val entries = keys.map { mockNotificationEntry(key = it) } underTest.setRenderedList(entries) assertThat(notifs) .comparingElementsUsing(byKey) .containsExactlyElementsIn(keys) .inOrder() } + + @Test + fun setRenderList_flatMapsRankings() = + testScope.runTest { + val ranks by collectLastValue(notifsInteractor.activeNotificationRanks) + + val single = mockNotificationEntry("single", 0) + val group = + mockGroupEntry( + key = "group", + summary = mockNotificationEntry("summary", 1), + children = + listOf( + mockNotificationEntry("child0", 2), + mockNotificationEntry("child1", 3), + ), + ) + + underTest.setRenderedList(listOf(single, group)) + + assertThat(ranks) + .containsExactlyEntriesIn( + mapOf( + "single" to 0, + "summary" to 1, + "child0" to 2, + "child1" to 3, + ) + ) + } + + @Test + fun setRenderList_singleItems_mapsRankings() = + testScope.runTest { + val actual by collectLastValue(notifsInteractor.activeNotificationRanks) + val expected = + (0..10).shuffled().mapIndexed { index, value -> "$value" to index }.toMap() + + val entries = expected.map { (key, rank) -> mockNotificationEntry(key, rank) } + + underTest.setRenderedList(entries) + + assertThat(actual).containsAtLeastEntriesIn(expected) + } + + @Test + fun setRenderList_groupWithNoSummary_flatMapsRankings() = + testScope.runTest { + val actual by collectLastValue(notifsInteractor.activeNotificationRanks) + val expected = + (0..10).shuffled().mapIndexed { index, value -> "$value" to index }.toMap() + + val group = + mockGroupEntry( + key = "group", + summary = null, + children = expected.map { (key, rank) -> mockNotificationEntry(key, rank) }, + ) + + underTest.setRenderedList(listOf(group)) + + assertThat(actual).containsAtLeastEntriesIn(expected) + } +} + +private fun mockGroupEntry( + key: String, + summary: NotificationEntry?, + children: List<NotificationEntry>, +): GroupEntry { + return mock<GroupEntry> { + whenever(this.key).thenReturn(key) + whenever(this.summary).thenReturn(summary) + whenever(this.children).thenReturn(children) + } +} + +private fun mockNotificationEntry(key: String, rank: Int = 0): NotificationEntry { + val mockSbn = + mock<StatusBarNotification>() { + whenever(notification).thenReturn(mock()) + whenever(packageName).thenReturn("com.android") + } + return mock<NotificationEntry> { + whenever(this.key).thenReturn(key) + whenever(this.icons).thenReturn(mock()) + whenever(this.representativeEntry).thenReturn(this) + whenever(this.ranking).thenReturn(RankingBuilder().setRank(rank).build()) + whenever(this.sbn).thenReturn(mockSbn) + } } 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 168e782d0481..ff02ef3d4e62 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 @@ -28,6 +28,8 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import static kotlinx.coroutines.test.TestCoroutineDispatchersKt.StandardTestDispatcher; + import android.app.Notification; import android.os.Handler; import android.os.Looper; @@ -56,6 +58,8 @@ import com.android.systemui.statusbar.notification.collection.NotifPipeline; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder; import com.android.systemui.statusbar.notification.collection.render.NotificationVisibilityProvider; +import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationListRepository; +import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor; import com.android.systemui.statusbar.notification.logging.nano.Notifications; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; import com.android.systemui.statusbar.notification.stack.NotificationListContainer; @@ -110,6 +114,11 @@ public class NotificationLoggerTest extends SysuiTestCase { private final FakeKeyguardRepository mKeyguardRepository = new FakeKeyguardRepository(); private final PowerInteractor mPowerInteractor = PowerInteractorFactory.create().getPowerInteractor(); + private final ActiveNotificationListRepository mActiveNotificationListRepository = + new ActiveNotificationListRepository(); + private final ActiveNotificationsInteractor mActiveNotificationsInteractor = + new ActiveNotificationsInteractor(mActiveNotificationListRepository, + StandardTestDispatcher(null, null)); private WindowRootViewVisibilityInteractor mWindowRootViewVisibilityInteractor; private final JavaAdapter mJavaAdapter = new JavaAdapter(mTestScope.getBackgroundScope()); @@ -123,7 +132,8 @@ public class NotificationLoggerTest extends SysuiTestCase { new WindowRootViewVisibilityRepository(mBarService, mUiBgExecutor), mKeyguardRepository, mHeadsUpManager, - mPowerInteractor); + mPowerInteractor, + mActiveNotificationsInteractor); mWindowRootViewVisibilityInteractor.setIsLockscreenOrShadeVisible(true); mEntry = new NotificationEntryBuilder() diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLoggerFake.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLoggerFake.java index dae0aa229dbf..d61fc05c699f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLoggerFake.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLoggerFake.java @@ -34,6 +34,11 @@ public class NotificationPanelLoggerFake implements NotificationPanelLogger { } @Override + public void logPanelShown(boolean isLockscreen, Notifications.NotificationList proto) { + mCalls.add(new CallRecord(isLockscreen, proto)); + } + + @Override public void logPanelShown(boolean isLockscreen, List<NotificationEntry> visibleNotifications) { mCalls.add(new CallRecord(isLockscreen, diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowControllerTest.kt index 9547af19b36b..8ac2a334818c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowControllerTest.kt @@ -40,12 +40,12 @@ import com.android.systemui.statusbar.notification.collection.provider.Notificat import com.android.systemui.statusbar.notification.collection.render.FakeNodeController import com.android.systemui.statusbar.notification.collection.render.GroupExpansionManager import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManager -import com.android.systemui.statusbar.notification.logging.NotificationLogger import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier import com.android.systemui.statusbar.notification.row.ExpandableNotificationRowController.BUBBLES_SETTING_URI import com.android.systemui.statusbar.notification.stack.NotificationChildrenContainer import com.android.systemui.statusbar.notification.stack.NotificationChildrenContainerLogger import com.android.systemui.statusbar.notification.stack.NotificationListContainer +import com.android.systemui.statusbar.notification.stack.ui.view.NotificationRowStatsLogger import com.android.systemui.statusbar.phone.KeyguardBypassController import com.android.systemui.statusbar.policy.HeadsUpManager import com.android.systemui.statusbar.policy.SmartReplyConstants @@ -92,7 +92,7 @@ class ExpandableNotificationRowControllerTest : SysuiTestCase() { private val groupMembershipManager: GroupMembershipManager = mock() private val groupExpansionManager: GroupExpansionManager = mock() private val rowContentBindStage: RowContentBindStage = mock() - private val notifLogger: NotificationLogger = mock() + private val notifLogger: NotificationRowStatsLogger = mock() private val headsUpManager: HeadsUpManager = mock() private val onExpandClickListener: ExpandableNotificationRow.OnExpandClickListener = mock() private val statusBarStateController: StatusBarStateController = mock() 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 8a730cfd7ddd..71613edb8737 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 @@ -42,6 +42,8 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import static kotlinx.coroutines.test.TestCoroutineDispatchersKt.StandardTestDispatcher; + import android.app.INotificationManager; import android.app.Notification; import android.app.NotificationChannel; @@ -85,6 +87,8 @@ import com.android.systemui.statusbar.notification.AssistantFeedbackController; import com.android.systemui.statusbar.notification.NotificationActivityStarter; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.collection.provider.HighPriorityProvider; +import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationListRepository; +import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor; import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier; import com.android.systemui.statusbar.notification.row.NotificationGutsManager.OnSettingsClickListener; import com.android.systemui.statusbar.notification.stack.NotificationListContainer; @@ -156,6 +160,12 @@ public class NotificationGutsManagerTest extends SysuiTestCase { @Mock private UserManager mUserManager; + private final ActiveNotificationListRepository mActiveNotificationListRepository = + new ActiveNotificationListRepository(); + private final ActiveNotificationsInteractor mActiveNotificationsInteractor = + new ActiveNotificationsInteractor(mActiveNotificationListRepository, + StandardTestDispatcher(null, null)); + private WindowRootViewVisibilityInteractor mWindowRootViewVisibilityInteractor; @Before @@ -171,7 +181,8 @@ public class NotificationGutsManagerTest extends SysuiTestCase { new WindowRootViewVisibilityRepository(mBarService, mExecutor), new FakeKeyguardRepository(), mHeadsUpManager, - PowerInteractorFactory.create().getPowerInteractor()); + PowerInteractorFactory.create().getPowerInteractor(), + mActiveNotificationsInteractor); mGutsManager = new NotificationGutsManager( mContext, diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/DisplaySwitchNotificationsHiderTrackerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/DisplaySwitchNotificationsHiderTrackerTest.kt new file mode 100644 index 000000000000..1dfcb38b247b --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/DisplaySwitchNotificationsHiderTrackerTest.kt @@ -0,0 +1,163 @@ +/* + * 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.stack + +import androidx.test.filters.SmallTest +import com.android.internal.util.LatencyTracker +import com.android.internal.util.LatencyTracker.ACTION_NOTIFICATIONS_HIDDEN_FOR_MEASURE +import com.android.internal.util.LatencyTracker.ACTION_NOTIFICATIONS_HIDDEN_FOR_MEASURE_WITH_SHADE_OPEN +import com.android.systemui.SysuiTestCase +import com.android.systemui.shade.domain.interactor.ShadeInteractor +import com.android.systemui.util.mockito.mock +import com.android.systemui.util.mockito.whenever +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.launch +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.mockito.Mockito.clearInvocations +import org.mockito.Mockito.never +import org.mockito.Mockito.verify + +@SmallTest +@OptIn(ExperimentalCoroutinesApi::class) +class DisplaySwitchNotificationsHiderTrackerTest : SysuiTestCase() { + + private val testScope = TestScope() + private val shadeInteractor = mock<ShadeInteractor>() + private val latencyTracker = mock<LatencyTracker>() + + private val shouldHideFlow = MutableStateFlow(false) + private val shadeExpandedFlow = MutableStateFlow(false) + + private val tracker = DisplaySwitchNotificationsHiderTracker(shadeInteractor, latencyTracker) + + @Before + fun setup() { + whenever(shadeInteractor.isAnyExpanded).thenReturn(shadeExpandedFlow) + } + + @Test + fun notificationsBecomeHidden_tracksHideActionStart() = testScope.runTest { + startTracking() + + shouldHideFlow.value = true + runCurrent() + + verify(latencyTracker).onActionStart(HIDE_NOTIFICATIONS_ACTION) + } + + @Test + fun notificationsBecomeVisibleAfterHidden_tracksHideActionEnd() = testScope.runTest { + startTracking() + + shouldHideFlow.value = true + runCurrent() + clearInvocations(latencyTracker) + shouldHideFlow.value = false + runCurrent() + + verify(latencyTracker).onActionEnd(HIDE_NOTIFICATIONS_ACTION) + } + + @Test + fun notificationsBecomeHiddenWhenShadeIsClosed_doesNotTrackHideWhenVisibleActionStart() = + testScope.runTest { + shouldHideFlow.value = false + shadeExpandedFlow.value = false + startTracking() + + shouldHideFlow.value = true + runCurrent() + + verify(latencyTracker, never()) + .onActionStart(HIDE_NOTIFICATIONS_WHEN_VISIBLE_ACTION) + } + + @Test + fun notificationsBecomeHiddenWhenShadeIsOpen_tracksHideWhenVisibleActionStart() = testScope.runTest { + shouldHideFlow.value = false + shadeExpandedFlow.value = false + startTracking() + + shouldHideFlow.value = true + shadeExpandedFlow.value = true + runCurrent() + + verify(latencyTracker).onActionStart(HIDE_NOTIFICATIONS_WHEN_VISIBLE_ACTION) + } + + @Test + fun shadeBecomesOpenWhenNotificationsHidden_tracksHideWhenVisibleActionStart() = + testScope.runTest { + shouldHideFlow.value = true + shadeExpandedFlow.value = false + startTracking() + + shadeExpandedFlow.value = true + runCurrent() + + verify(latencyTracker).onActionStart(HIDE_NOTIFICATIONS_WHEN_VISIBLE_ACTION) + } + + @Test + fun notificationsBecomeVisibleWhenShadeIsOpen_tracksHideWhenVisibleActionEnd() = testScope.runTest { + shouldHideFlow.value = false + shadeExpandedFlow.value = false + startTracking() + shouldHideFlow.value = true + shadeExpandedFlow.value = true + runCurrent() + clearInvocations(latencyTracker) + + shouldHideFlow.value = false + runCurrent() + + verify(latencyTracker).onActionEnd(HIDE_NOTIFICATIONS_WHEN_VISIBLE_ACTION) + } + + @Test + fun shadeBecomesClosedWhenNotificationsHidden_tracksHideWhenVisibleActionEnd() = testScope.runTest { + shouldHideFlow.value = false + shadeExpandedFlow.value = false + startTracking() + shouldHideFlow.value = true + shadeExpandedFlow.value = true + runCurrent() + clearInvocations(latencyTracker) + + shadeExpandedFlow.value = false + runCurrent() + + verify(latencyTracker).onActionEnd(HIDE_NOTIFICATIONS_WHEN_VISIBLE_ACTION) + } + + private fun TestScope.startTracking() { + backgroundScope.launch { tracker.trackNotificationHideTime(shouldHideFlow) } + backgroundScope.launch { tracker.trackNotificationHideTimeWhenVisible(shouldHideFlow) } + runCurrent() + clearInvocations(latencyTracker) + } + + private companion object { + const val HIDE_NOTIFICATIONS_ACTION = ACTION_NOTIFICATIONS_HIDDEN_FOR_MEASURE + const val HIDE_NOTIFICATIONS_WHEN_VISIBLE_ACTION = + ACTION_NOTIFICATIONS_HIDDEN_FOR_MEASURE_WITH_SHADE_OPEN + } +} 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 88662b60c7d1..89f826b2049d 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 @@ -66,6 +66,8 @@ import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin; import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin.OnMenuEventListener; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.power.domain.interactor.PowerInteractor; +import com.android.systemui.scene.shared.flag.SceneContainerFlags; +import com.android.systemui.scene.ui.view.WindowRootView; import com.android.systemui.shade.ShadeController; import com.android.systemui.statusbar.LockscreenShadeTransitionController; import com.android.systemui.statusbar.NotificationLockscreenUserManager; @@ -91,6 +93,7 @@ import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow import com.android.systemui.statusbar.notification.row.NotificationGutsManager; import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController.NotificationPanelEvent; import com.android.systemui.statusbar.notification.stack.NotificationSwipeHelper.NotificationCallback; +import com.android.systemui.statusbar.notification.stack.domain.interactor.NotificationStackAppearanceInteractor; import com.android.systemui.statusbar.notification.stack.ui.viewbinder.NotificationListViewBinder; import com.android.systemui.statusbar.phone.KeyguardBypassController; import com.android.systemui.statusbar.phone.ScrimController; @@ -112,6 +115,8 @@ import org.mockito.Captor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import javax.inject.Provider; + /** * Tests for {@link NotificationStackScrollLayoutController}. */ @@ -153,6 +158,9 @@ public class NotificationStackScrollLayoutControllerTest extends SysuiTestCase { @Mock private NotificationRemoteInputManager mRemoteInputManager; @Mock private VisibilityLocationProviderDelegator mVisibilityLocationProviderDelegator; @Mock private ShadeController mShadeController; + @Mock private SceneContainerFlags mSceneContainerFlags; + @Mock private Provider<WindowRootView> mWindowRootView; + @Mock private NotificationStackAppearanceInteractor mNotificationStackAppearanceInteractor; @Mock private InteractionJankMonitor mJankMonitor; private final StackStateLogger mStackLogger = new StackStateLogger(logcatLogBuffer(), logcatLogBuffer()); @@ -724,6 +732,9 @@ public class NotificationStackScrollLayoutControllerTest extends SysuiTestCase { mSeenNotificationsInteractor, mViewBinder, mShadeController, + mSceneContainerFlags, + mWindowRootView, + mNotificationStackAppearanceInteractor, mJankMonitor, mStackLogger, mLogger, diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java index a1721208b2f2..4afcc8c9da43 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java @@ -20,6 +20,7 @@ import static android.view.View.GONE; import static android.view.WindowInsets.Type.ime; import static com.android.systemui.Flags.FLAG_NEW_AOD_TRANSITION; +import static com.android.systemui.Flags.FLAG_SCENE_CONTAINER; import static com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.ROWS_ALL; import static com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.ROWS_GENTLE; import static com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.RUBBER_BAND_FACTOR_NORMAL; @@ -37,6 +38,7 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyFloat; import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.clearInvocations; @@ -51,6 +53,7 @@ import static org.mockito.Mockito.when; import android.graphics.Insets; import android.graphics.Rect; +import android.os.SystemClock; import android.platform.test.annotations.DisableFlags; import android.platform.test.annotations.EnableFlags; import android.testing.AndroidTestingRunner; @@ -74,6 +77,7 @@ import com.android.systemui.flags.FakeFeatureFlags; import com.android.systemui.flags.FeatureFlags; import com.android.systemui.flags.Flags; import com.android.systemui.res.R; +import com.android.systemui.scene.shared.flag.SceneContainerFlag; import com.android.systemui.shade.ShadeController; import com.android.systemui.shade.transition.LargeScreenShadeInterpolator; import com.android.systemui.statusbar.EmptyShadeView; @@ -93,11 +97,13 @@ import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager; import com.android.systemui.statusbar.policy.ResourcesSplitShadeStateController; import org.junit.Assert; +import org.junit.Assume; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; +import org.mockito.ArgumentMatcher; import org.mockito.Mock; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; @@ -947,6 +953,78 @@ public class NotificationStackScrollLayoutTest extends SysuiTestCase { verify(runnable).run(); } + @Test + public void testDispatchTouchEvent_sceneContainerDisabled() { + Assume.assumeFalse(SceneContainerFlag.isEnabled()); + + MotionEvent event = MotionEvent.obtain( + SystemClock.uptimeMillis(), + SystemClock.uptimeMillis(), + MotionEvent.ACTION_MOVE, + 0, + 0, + 0 + ); + + mStackScroller.dispatchTouchEvent(event); + + verify(mStackScrollLayoutController, never()).sendTouchToSceneFramework(any()); + } + + @Test + public void testDispatchTouchEvent_sceneContainerEnabled() { + Assume.assumeTrue(SceneContainerFlag.isEnabled()); + mStackScroller.setIsBeingDragged(true); + + MotionEvent moveEvent = MotionEvent.obtain( + SystemClock.uptimeMillis(), + SystemClock.uptimeMillis(), + MotionEvent.ACTION_MOVE, + 0, + 0, + 0 + ); + MotionEvent syntheticDownEvent = moveEvent.copy(); + syntheticDownEvent.setAction(MotionEvent.ACTION_DOWN); + mStackScroller.dispatchTouchEvent(moveEvent); + + verify(mStackScrollLayoutController).sendTouchToSceneFramework(argThat( + new MotionEventMatcher(syntheticDownEvent))); + + mStackScroller.dispatchTouchEvent(moveEvent); + + verify(mStackScrollLayoutController).sendTouchToSceneFramework(moveEvent); + } + + @Test + @EnableFlags(FLAG_SCENE_CONTAINER) + public void testDispatchTouchEvent_sceneContainerEnabled_actionUp() { + Assume.assumeTrue(SceneContainerFlag.isEnabled()); + mStackScroller.setIsBeingDragged(true); + + MotionEvent upEvent = MotionEvent.obtain( + SystemClock.uptimeMillis(), + SystemClock.uptimeMillis(), + MotionEvent.ACTION_UP, + 0, + 0, + 0 + ); + MotionEvent syntheticDownEvent = upEvent.copy(); + syntheticDownEvent.setAction(MotionEvent.ACTION_DOWN); + + mStackScroller.dispatchTouchEvent(upEvent); + + verify(mStackScrollLayoutController, atLeastOnce()).sendTouchToSceneFramework(argThat( + new MotionEventMatcher(syntheticDownEvent))); + + mStackScroller.dispatchTouchEvent(upEvent); + + verify(mStackScrollLayoutController, atLeastOnce()).sendTouchToSceneFramework(argThat( + new MotionEventMatcher(upEvent))); + assertFalse(mStackScroller.getIsBeingDragged()); + } + private void setBarStateForTest(int state) { // Can't inject this through the listener or we end up on the actual implementation // rather than the mock because the spy just coppied the anonymous inner /shruggie. @@ -993,4 +1071,21 @@ public class NotificationStackScrollLayoutTest extends SysuiTestCase { /* metaState= */0 ); } + + private static class MotionEventMatcher implements ArgumentMatcher<MotionEvent> { + private final MotionEvent mLeftEvent; + + MotionEventMatcher(MotionEvent leftEvent) { + mLeftEvent = leftEvent; + } + + @Override + public boolean matches(MotionEvent right) { + return mLeftEvent.getActionMasked() == right.getActionMasked() + && mLeftEvent.getDownTime() == right.getDownTime() + && mLeftEvent.getEventTime() == right.getEventTime() + && mLeftEvent.getX() == right.getX() + && mLeftEvent.getY() == right.getY(); + } + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationStatsLoggerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationStatsLoggerTest.kt new file mode 100644 index 000000000000..d7d1ffcc3339 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationStatsLoggerTest.kt @@ -0,0 +1,429 @@ +/* + * 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.stack.ui.view + +import android.service.notification.notificationListenerService +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.internal.statusbar.NotificationVisibility +import com.android.internal.statusbar.statusBarService +import com.android.systemui.SysuiTestCase +import com.android.systemui.kosmos.testScope +import com.android.systemui.statusbar.notification.data.model.activeNotificationModel +import com.android.systemui.statusbar.notification.logging.nano.Notifications +import com.android.systemui.statusbar.notification.logging.notificationPanelLogger +import com.android.systemui.statusbar.notification.stack.ExpandableViewState +import com.android.systemui.testKosmos +import com.android.systemui.util.mockito.argumentCaptor +import com.android.systemui.util.mockito.eq +import com.google.common.truth.Truth.assertThat +import java.util.concurrent.Callable +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito.clearInvocations +import org.mockito.Mockito.spy +import org.mockito.Mockito.verify +import org.mockito.Mockito.verifyZeroInteractions + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@RunWith(AndroidJUnit4::class) +class NotificationStatsLoggerTest : SysuiTestCase() { + + private val kosmos = testKosmos() + + private val testScope = kosmos.testScope + private val mockNotificationListenerService = kosmos.notificationListenerService + private val mockPanelLogger = kosmos.notificationPanelLogger + private val mockStatusBarService = kosmos.statusBarService + + private val underTest = kosmos.notificationStatsLogger + + private val visibilityArrayCaptor = argumentCaptor<Array<NotificationVisibility>>() + private val stringArrayCaptor = argumentCaptor<Array<String>>() + private val notificationListProtoCaptor = argumentCaptor<Notifications.NotificationList>() + + @Test + fun onNotificationListUpdated_itemsAdded_logsNewlyVisibleItems() = + testScope.runTest { + // WHEN new Notifications are added + // AND they're visible + val (ranks, locations) = fakeNotificationMaps("key0", "key1") + val callable = Callable { locations } + underTest.onNotificationListUpdated(callable, ranks) + runCurrent() + + // THEN visibility changes are reported + verify(mockStatusBarService) + .onNotificationVisibilityChanged(visibilityArrayCaptor.capture(), eq(emptyArray())) + verify(mockNotificationListenerService) + .setNotificationsShown(stringArrayCaptor.capture()) + val loggedVisibilities = visibilityArrayCaptor.value + val loggedKeys = stringArrayCaptor.value + assertThat(loggedVisibilities).hasLength(2) + assertThat(loggedKeys).hasLength(2) + assertThat(loggedVisibilities[0]).apply { + isKeyEqualTo("key0") + isRankEqualTo(0) + isVisible() + isInMainArea() + isCountEqualTo(2) + } + assertThat(loggedVisibilities[1]).apply { + isKeyEqualTo("key1") + isRankEqualTo(1) + isVisible() + isInMainArea() + isCountEqualTo(2) + } + assertThat(loggedKeys[0]).isEqualTo("key0") + assertThat(loggedKeys[1]).isEqualTo("key1") + } + + @Test + fun onNotificationListUpdated_itemsRemoved_logsNoLongerVisibleItems() = + testScope.runTest { + // GIVEN some visible Notifications are reported + val (ranks, locations) = fakeNotificationMaps("key0", "key1") + val callable = Callable { locations } + underTest.onNotificationListUpdated(callable, ranks) + runCurrent() + clearInvocations(mockStatusBarService, mockNotificationListenerService) + + // WHEN the same Notifications are removed + val emptyCallable = Callable { emptyMap<String, Int>() } + underTest.onNotificationListUpdated(emptyCallable, emptyMap()) + runCurrent() + + // THEN visibility changes are reported + verify(mockStatusBarService) + .onNotificationVisibilityChanged(eq(emptyArray()), visibilityArrayCaptor.capture()) + verifyZeroInteractions(mockNotificationListenerService) + val noLongerVisible = visibilityArrayCaptor.value + assertThat(noLongerVisible).hasLength(2) + assertThat(noLongerVisible[0]).apply { + isKeyEqualTo("key0") + isRankEqualTo(0) + notVisible() + isInMainArea() + isCountEqualTo(0) + } + assertThat(noLongerVisible[1]).apply { + isKeyEqualTo("key1") + isRankEqualTo(1) + notVisible() + isInMainArea() + isCountEqualTo(0) + } + } + + @Test + fun onNotificationListUpdated_itemsBecomeInvisible_logsNoLongerVisibleItems() = + testScope.runTest { + // GIVEN some visible Notifications are reported + val (ranks, locations) = fakeNotificationMaps("key0", "key1") + val callable = Callable { locations } + underTest.onNotificationListUpdated(callable, ranks) + runCurrent() + clearInvocations(mockStatusBarService, mockNotificationListenerService) + + // WHEN the same Notifications are becoming invisible + val emptyCallable = Callable { emptyMap<String, Int>() } + underTest.onNotificationListUpdated(emptyCallable, ranks) + runCurrent() + + // THEN visibility changes are reported + verify(mockStatusBarService) + .onNotificationVisibilityChanged(eq(emptyArray()), visibilityArrayCaptor.capture()) + verifyZeroInteractions(mockNotificationListenerService) + val noLongerVisible = visibilityArrayCaptor.value + assertThat(noLongerVisible).hasLength(2) + assertThat(noLongerVisible[0]).apply { + isKeyEqualTo("key0") + isRankEqualTo(0) + notVisible() + isInMainArea() + isCountEqualTo(2) + } + assertThat(noLongerVisible[1]).apply { + isKeyEqualTo("key1") + isRankEqualTo(1) + notVisible() + isInMainArea() + isCountEqualTo(2) + } + } + + @Test + fun onNotificationListUpdated_itemsChangedPositions_nothingLogged() = + testScope.runTest { + // GIVEN some visible Notifications are reported + val (ranks, locations) = fakeNotificationMaps("key0", "key1") + underTest.onNotificationListUpdated({ locations }, ranks) + runCurrent() + clearInvocations(mockStatusBarService, mockNotificationListenerService) + + // WHEN the reported Notifications are changing positions + val (newRanks, newLocations) = fakeNotificationMaps("key1", "key0") + underTest.onNotificationListUpdated({ newLocations }, newRanks) + runCurrent() + + // THEN no visibility changes are reported + verifyZeroInteractions(mockStatusBarService, mockNotificationListenerService) + } + + @Test + fun onNotificationListUpdated_calledTwice_usesTheNewCallable() = + testScope.runTest { + // GIVEN some visible Notifications are reported + val (ranks, locations) = fakeNotificationMaps("key0", "key1", "key2") + val callable = spy(Callable { locations }) + underTest.onNotificationListUpdated(callable, ranks) + runCurrent() + clearInvocations(callable) + + // WHEN a new update comes + val otherCallable = spy(Callable { locations }) + underTest.onNotificationListUpdated(otherCallable, ranks) + runCurrent() + + // THEN we call the new Callable + verifyZeroInteractions(callable) + verify(otherCallable).call() + } + + @Test + fun onLockscreenOrShadeNotInteractive_logsNoLongerVisibleItems() = + testScope.runTest { + // GIVEN some visible Notifications are reported + val (ranks, locations) = fakeNotificationMaps("key0", "key1") + val callable = Callable { locations } + underTest.onNotificationListUpdated(callable, ranks) + runCurrent() + clearInvocations(mockStatusBarService, mockNotificationListenerService) + + // WHEN the Shade becomes non interactive + underTest.onLockscreenOrShadeNotInteractive(emptyList()) + runCurrent() + + // THEN visibility changes are reported + verify(mockStatusBarService) + .onNotificationVisibilityChanged(eq(emptyArray()), visibilityArrayCaptor.capture()) + verifyZeroInteractions(mockNotificationListenerService) + val noLongerVisible = visibilityArrayCaptor.value + assertThat(noLongerVisible).hasLength(2) + assertThat(noLongerVisible[0]).apply { + isKeyEqualTo("key0") + isRankEqualTo(0) + notVisible() + isInMainArea() + isCountEqualTo(0) + } + assertThat(noLongerVisible[1]).apply { + isKeyEqualTo("key1") + isRankEqualTo(1) + notVisible() + isInMainArea() + isCountEqualTo(0) + } + } + + @Test + fun onLockscreenOrShadeInteractive_logsPanelShown() = + testScope.runTest { + // WHEN the Shade becomes interactive + underTest.onLockscreenOrShadeInteractive( + isOnLockScreen = true, + listOf( + activeNotificationModel( + key = "key0", + uid = 0, + packageName = "com.android.first" + ), + activeNotificationModel( + key = "key1", + uid = 1, + packageName = "com.android.second" + ), + ) + ) + runCurrent() + + // THEN the Panel shown event is reported + verify(mockPanelLogger).logPanelShown(eq(true), notificationListProtoCaptor.capture()) + val loggedNotifications = notificationListProtoCaptor.value.notifications + assertThat(loggedNotifications.size).isEqualTo(2) + with(loggedNotifications[0]) { + assertThat(uid).isEqualTo(0) + assertThat(packageName).isEqualTo("com.android.first") + } + with(loggedNotifications[1]) { + assertThat(uid).isEqualTo(1) + assertThat(packageName).isEqualTo("com.android.second") + } + } + + @Test + fun onNotificationExpanded_visibleLocation_expansionLogged() = + testScope.runTest { + // WHEN a Notification is expanded + underTest.onNotificationExpansionChanged( + key = "key", + isExpanded = true, + location = ExpandableViewState.LOCATION_MAIN_AREA, + isUserAction = true + ) + runCurrent() + + // THEN the Expand event is reported + verify(mockStatusBarService) + .onNotificationExpansionChanged( + /* key = */ "key", + /* userAction = */ true, + /* expanded = */ true, + NotificationVisibility.NotificationLocation.LOCATION_MAIN_AREA.ordinal + ) + } + + @Test + fun onNotificationExpanded_notVisibleLocation_nothingLogged() = + testScope.runTest { + // WHEN a NOT visible Notification is expanded + underTest.onNotificationExpansionChanged( + key = "key", + isExpanded = true, + location = ExpandableViewState.LOCATION_BOTTOM_STACK_HIDDEN, + isUserAction = true + ) + runCurrent() + + // No events are reported + verifyZeroInteractions(mockStatusBarService) + } + + @Test + fun onNotificationExpanded_notVisibleLocation_becomesVisible_expansionLogged() = + testScope.runTest { + // WHEN a NOT visible Notification is expanded + underTest.onNotificationExpansionChanged( + key = "key", + isExpanded = true, + location = ExpandableViewState.LOCATION_GONE, + isUserAction = true + ) + runCurrent() + + // AND it becomes visible + val (ranks, locations) = fakeNotificationMaps("key") + val callable = Callable { locations } + underTest.onNotificationListUpdated(callable, ranks) + runCurrent() + + // THEN the Expand event is reported + verify(mockStatusBarService) + .onNotificationExpansionChanged( + /* key = */ "key", + /* userAction = */ true, + /* expanded = */ true, + NotificationVisibility.NotificationLocation.LOCATION_MAIN_AREA.ordinal + ) + } + + @Test + fun onNotificationCollapsed_isFirstInteraction_nothingLogged() = + testScope.runTest { + // WHEN a Notification is collapsed, and it is the first interaction + underTest.onNotificationExpansionChanged( + key = "key", + isExpanded = false, + location = ExpandableViewState.LOCATION_MAIN_AREA, + isUserAction = false + ) + runCurrent() + + // THEN no events are reported, because we consider the Notification initially + // collapsed, so only expanded is logged in the first time. + verifyZeroInteractions(mockStatusBarService) + } + + @Test + fun onNotificationExpandedAndCollapsed_expansionChangesLogged() = + testScope.runTest { + // GIVEN a Notification is expanded + underTest.onNotificationExpansionChanged( + key = "key", + isExpanded = true, + location = ExpandableViewState.LOCATION_MAIN_AREA, + isUserAction = true + ) + runCurrent() + + // WHEN the Notification is collapsed + verify(mockStatusBarService) + .onNotificationExpansionChanged( + /* key = */ "key", + /* userAction = */ true, + /* expanded = */ true, + NotificationVisibility.NotificationLocation.LOCATION_MAIN_AREA.ordinal + ) + + // AND the Notification is expanded again + underTest.onNotificationExpansionChanged( + key = "key", + isExpanded = false, + location = ExpandableViewState.LOCATION_MAIN_AREA, + isUserAction = true + ) + runCurrent() + + // THEN the expansion changes are logged + verify(mockStatusBarService) + .onNotificationExpansionChanged( + /* key = */ "key", + /* userAction = */ true, + /* expanded = */ false, + NotificationVisibility.NotificationLocation.LOCATION_MAIN_AREA.ordinal + ) + } + + private fun fakeNotificationMaps( + vararg keys: String + ): Pair<Map<String, Int>, Map<String, Int>> { + val ranks: Map<String, Int> = keys.mapIndexed { index, key -> key to index }.toMap() + val locations: Map<String, Int> = + keys.associateWith { ExpandableViewState.LOCATION_MAIN_AREA } + + return Pair(ranks, locations) + } + + private fun assertThat(visibility: NotificationVisibility) = + NotificationVisibilitySubject(visibility) +} + +private class NotificationVisibilitySubject(private val visibility: NotificationVisibility) { + fun isKeyEqualTo(key: String) = assertThat(visibility.key).isEqualTo(key) + fun isRankEqualTo(rank: Int) = assertThat(visibility.rank).isEqualTo(rank) + fun isCountEqualTo(count: Int) = assertThat(visibility.count).isEqualTo(count) + fun isVisible() = assertThat(this.visibility.visible).isTrue() + fun notVisible() = assertThat(this.visibility.visible).isFalse() + fun isInMainArea() = + assertThat(this.visibility.location) + .isEqualTo(NotificationVisibility.NotificationLocation.LOCATION_MAIN_AREA) +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt index c17a8ef62a4b..4188c5d34d98 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt @@ -38,6 +38,7 @@ import com.android.systemui.res.R import com.android.systemui.runCurrent import com.android.systemui.runTest import com.android.systemui.shade.data.repository.FakeShadeRepository +import com.android.systemui.statusbar.notification.dagger.NotificationStatsLoggerModule import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationListRepository import com.android.systemui.statusbar.notification.data.repository.setActiveNotifs import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor @@ -71,6 +72,7 @@ class NotificationListViewModelTest : SysuiTestCase() { FooterViewModelModule::class, HeadlessSystemUserModeModule::class, UnfoldTransitionModule.Bindings::class, + NotificationStatsLoggerModule::class, ] ) interface TestComponent : SysUITestComponent<NotificationListViewModel> { diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationLoggerViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationLoggerViewModelTest.kt new file mode 100644 index 000000000000..e9d88ccb0cbc --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationLoggerViewModelTest.kt @@ -0,0 +1,147 @@ +/* + * 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.stack.ui.viewmodel + +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.keyguard.data.repository.fakeKeyguardRepository +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.powerInteractor +import com.android.systemui.scene.data.repository.windowRootViewVisibilityRepository +import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository +import com.android.systemui.statusbar.notification.data.repository.setActiveNotifs +import com.android.systemui.testKosmos +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +class NotificationLoggerViewModelTest : SysuiTestCase() { + + private val kosmos = testKosmos() + + private val testScope = kosmos.testScope + private val activeNotificationListRepository = kosmos.activeNotificationListRepository + private val keyguardRepository = kosmos.fakeKeyguardRepository + private val powerInteractor = kosmos.powerInteractor + private val windowRootViewVisibilityRepository = kosmos.windowRootViewVisibilityRepository + + private val underTest = kosmos.notificationListLoggerViewModel + + @Test + fun isLockscreenOrShadeInteractive_deviceActiveAndShadeIsInteractive_true() = + testScope.runTest { + powerInteractor.setAwakeForTest() + windowRootViewVisibilityRepository.setIsLockscreenOrShadeVisible(true) + + val actual by collectLastValue(underTest.isLockscreenOrShadeInteractive) + + assertThat(actual).isTrue() + } + + @Test + fun isLockscreenOrShadeInteractive_deviceIsAsleepAndShadeIsInteractive_false() = + testScope.runTest { + powerInteractor.setAsleepForTest() + windowRootViewVisibilityRepository.setIsLockscreenOrShadeVisible(true) + + val actual by collectLastValue(underTest.isLockscreenOrShadeInteractive) + + assertThat(actual).isFalse() + } + + @Test + fun isLockscreenOrShadeInteractive_deviceActiveAndShadeIsNotInteractive_false() = + testScope.runTest { + powerInteractor.setAwakeForTest() + windowRootViewVisibilityRepository.setIsLockscreenOrShadeVisible(false) + + val actual by collectLastValue(underTest.isLockscreenOrShadeInteractive) + + assertThat(actual).isFalse() + } + + @Test + fun activeNotifications_hasNotifications() = + testScope.runTest { + activeNotificationListRepository.setActiveNotifs(5) + + val notifs by collectLastValue(underTest.activeNotifications) + + assertThat(notifs).hasSize(5) + requireNotNull(notifs).forEachIndexed { i, notif -> + assertThat(notif.key).isEqualTo("$i") + } + } + + @Test + fun activeNotifications_isEmpty() = + testScope.runTest { + activeNotificationListRepository.setActiveNotifs(0) + + val notifications by collectLastValue(underTest.activeNotifications) + + assertThat(notifications).isEmpty() + } + + @Test + fun activeNotificationRanks_hasNotifications() = + testScope.runTest { + val keys = (0..4).map { "$it" } + activeNotificationListRepository.setActiveNotifs(5) + + val rankingsMap by collectLastValue(underTest.activeNotificationRanks) + + assertThat(rankingsMap).hasSize(5) + keys.forEachIndexed { rank, key -> assertThat(rankingsMap).containsEntry(key, rank) } + } + + @Test + fun activeNotificationRanks_isEmpty() = + testScope.runTest { + activeNotificationListRepository.setActiveNotifs(0) + + val rankingsMap by collectLastValue(underTest.activeNotificationRanks) + + assertThat(rankingsMap).isEmpty() + } + + @Test + fun isOnLockScreen_true() = + testScope.runTest { + keyguardRepository.setKeyguardShowing(true) + + val isOnLockScreen by collectLastValue(underTest.isOnLockScreen) + + assertThat(isOnLockScreen).isTrue() + } + @Test + fun isOnLockScreen_false() = + testScope.runTest { + keyguardRepository.setKeyguardShowing(false) + + val isOnLockScreen by collectLastValue(underTest.isOnLockScreen) + + assertThat(isOnLockScreen).isFalse() + } +} 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 20020f23b2df..a824bc4f803f 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 @@ -21,6 +21,7 @@ package com.android.systemui.statusbar.notification.stack.ui.viewmodel import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest +import com.android.systemui.Flags.FLAG_CENTRALIZED_STATUS_BAR_DIMENS_REFACTOR import com.android.systemui.SysuiTestCase import com.android.systemui.common.shared.model.NotificationContainerBounds import com.android.systemui.common.ui.data.repository.fakeConfigurationRepository @@ -39,8 +40,11 @@ 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.whenever import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.TestScope @@ -66,6 +70,7 @@ class SharedNotificationContainerViewModelTest : SysuiTestCase() { val keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository val shadeRepository = kosmos.shadeRepository val sharedNotificationContainerInteractor = kosmos.sharedNotificationContainerInteractor + val largeScreenHeaderHelper = kosmos.mockLargeScreenHeaderHelper val underTest = kosmos.sharedNotificationContainerViewModel @@ -101,8 +106,10 @@ class SharedNotificationContainerViewModelTest : SysuiTestCase() { } @Test - fun validatePaddingTopInSplitShade() = + fun validatePaddingTopInSplitShade_refactorFlagOff_usesLargeHeaderResource() = testScope.runTest { + mSetFlagsRule.disableFlags(FLAG_CENTRALIZED_STATUS_BAR_DIMENS_REFACTOR) + whenever(largeScreenHeaderHelper.getLargeScreenHeaderHeight()).thenReturn(5) overrideResource(R.bool.config_use_split_notification_shade, true) overrideResource(R.dimen.large_screen_shade_header_height, 10) overrideResource(R.dimen.keyguard_split_shade_top_margin, 50) @@ -115,6 +122,22 @@ class SharedNotificationContainerViewModelTest : SysuiTestCase() { } @Test + fun validatePaddingTopInSplitShade_refactorFlagOn_usesLargeHeaderHelper() = + testScope.runTest { + mSetFlagsRule.enableFlags(FLAG_CENTRALIZED_STATUS_BAR_DIMENS_REFACTOR) + whenever(largeScreenHeaderHelper.getLargeScreenHeaderHeight()).thenReturn(5) + overrideResource(R.bool.config_use_split_notification_shade, true) + overrideResource(R.dimen.large_screen_shade_header_height, 10) + overrideResource(R.dimen.keyguard_split_shade_top_margin, 50) + + val dimens by collectLastValue(underTest.configurationBasedDimensions) + + configurationRepository.onAnyConfigurationChange() + + assertThat(dimens!!.paddingTop).isEqualTo(40) + } + + @Test fun validatePaddingTop() = testScope.runTest { overrideResource(R.bool.config_use_split_notification_shade, false) @@ -153,17 +176,41 @@ class SharedNotificationContainerViewModelTest : SysuiTestCase() { } @Test - fun validateMarginTopWithLargeScreenHeader() = + fun validateMarginTopWithLargeScreenHeader_refactorFlagOff_usesResource() = testScope.runTest { + mSetFlagsRule.disableFlags(FLAG_CENTRALIZED_STATUS_BAR_DIMENS_REFACTOR) + val headerResourceHeight = 50 + val headerHelperHeight = 100 + whenever(largeScreenHeaderHelper.getLargeScreenHeaderHeight()) + .thenReturn(headerHelperHeight) overrideResource(R.bool.config_use_large_screen_shade_header, true) - overrideResource(R.dimen.large_screen_shade_header_height, 50) + overrideResource(R.dimen.large_screen_shade_header_height, headerResourceHeight) + overrideResource(R.dimen.notification_panel_margin_top, 0) + + val dimens by collectLastValue(underTest.configurationBasedDimensions) + + configurationRepository.onAnyConfigurationChange() + + assertThat(dimens!!.marginTop).isEqualTo(headerResourceHeight) + } + + @Test + fun validateMarginTopWithLargeScreenHeader_refactorFlagOn_usesHelper() = + testScope.runTest { + mSetFlagsRule.enableFlags(FLAG_CENTRALIZED_STATUS_BAR_DIMENS_REFACTOR) + val headerResourceHeight = 50 + val headerHelperHeight = 100 + whenever(largeScreenHeaderHelper.getLargeScreenHeaderHeight()) + .thenReturn(headerHelperHeight) + overrideResource(R.bool.config_use_large_screen_shade_header, true) + overrideResource(R.dimen.large_screen_shade_header_height, headerResourceHeight) overrideResource(R.dimen.notification_panel_margin_top, 0) val dimens by collectLastValue(underTest.configurationBasedDimensions) configurationRepository.onAnyConfigurationChange() - assertThat(dimens!!.marginTop).isEqualTo(50) + assertThat(dimens!!.marginTop).isEqualTo(headerHelperHeight) } @Test @@ -275,11 +322,13 @@ class SharedNotificationContainerViewModelTest : SysuiTestCase() { } @Test - fun boundsOnLockscreenInSplitShade() = + fun boundsOnLockscreenInSplitShade_refactorFlagOff_usesLargeHeaderResource() = testScope.runTest { + mSetFlagsRule.disableFlags(FLAG_CENTRALIZED_STATUS_BAR_DIMENS_REFACTOR) val bounds by collectLastValue(underTest.bounds) // When in split shade + whenever(largeScreenHeaderHelper.getLargeScreenHeaderHeight()).thenReturn(5) overrideResource(R.bool.config_use_split_notification_shade, true) overrideResource(R.dimen.large_screen_shade_header_height, 10) overrideResource(R.dimen.keyguard_split_shade_top_margin, 50) @@ -300,6 +349,33 @@ class SharedNotificationContainerViewModelTest : SysuiTestCase() { } @Test + fun boundsOnLockscreenInSplitShade_refactorFlagOn_usesLargeHeaderHelper() = + testScope.runTest { + mSetFlagsRule.enableFlags(FLAG_CENTRALIZED_STATUS_BAR_DIMENS_REFACTOR) + val bounds by collectLastValue(underTest.bounds) + + // When in split shade + whenever(largeScreenHeaderHelper.getLargeScreenHeaderHeight()).thenReturn(5) + overrideResource(R.bool.config_use_split_notification_shade, true) + overrideResource(R.dimen.large_screen_shade_header_height, 10) + overrideResource(R.dimen.keyguard_split_shade_top_margin, 50) + + configurationRepository.onAnyConfigurationChange() + runCurrent() + + // Start on lockscreen + showLockscreen() + + keyguardInteractor.setNotificationContainerBounds( + NotificationContainerBounds(top = 1f, bottom = 2f) + ) + runCurrent() + + // Top should be equal to bounds (1) + padding adjustment (40) + assertThat(bounds).isEqualTo(NotificationContainerBounds(top = 41f, bottom = 2f)) + } + + @Test fun boundsOnShade() = testScope.runTest { val bounds by collectLastValue(underTest.bounds) @@ -408,6 +484,46 @@ class SharedNotificationContainerViewModelTest : SysuiTestCase() { } @Test + fun translationYUpdatesOnKeyguard() = + testScope.runTest { + val translationY by collectLastValue(underTest.translationY) + + configurationRepository.setDimensionPixelSize( + R.dimen.keyguard_translate_distance_on_swipe_up, + -100 + ) + configurationRepository.onAnyConfigurationChange() + + // legacy expansion means the user is swiping up, usually for the bouncer + shadeRepository.setLegacyShadeExpansion(0.5f) + + showLockscreen() + + // The translation values are negative + assertThat(translationY).isLessThan(0f) + } + + @Test + fun translationYDoesNotUpdateWhenShadeIsExpanded() = + testScope.runTest { + val translationY by collectLastValue(underTest.translationY) + + configurationRepository.setDimensionPixelSize( + R.dimen.keyguard_translate_distance_on_swipe_up, + -100 + ) + configurationRepository.onAnyConfigurationChange() + + // legacy expansion means the user is swiping up, usually for the bouncer but also for + // shade collapsing + shadeRepository.setLegacyShadeExpansion(0.5f) + + showLockscreenWithShadeExpanded() + + assertThat(translationY).isEqualTo(0f) + } + + @Test fun updateBounds_fromKeyguardRoot() = testScope.runTest { val bounds by collectLastValue(underTest.bounds) diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardBypassControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardBypassControllerTest.kt index 91cbc3274900..7362e342f321 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardBypassControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardBypassControllerTest.kt @@ -24,9 +24,9 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.dump.DumpManager import com.android.systemui.flags.FakeFeatureFlags import com.android.systemui.flags.Flags +import com.android.systemui.kosmos.testScope import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.res.R -import com.android.systemui.scene.SceneTestUtils import com.android.systemui.shade.data.repository.FakeShadeRepository import com.android.systemui.statusbar.NotificationLockscreenUserManager import com.android.systemui.statusbar.policy.DevicePostureController @@ -35,6 +35,7 @@ import com.android.systemui.statusbar.policy.DevicePostureController.DEVICE_POST import com.android.systemui.statusbar.policy.DevicePostureController.DEVICE_POSTURE_OPENED import com.android.systemui.statusbar.policy.DevicePostureController.DEVICE_POSTURE_UNKNOWN import com.android.systemui.statusbar.policy.KeyguardStateController +import com.android.systemui.testKosmos import com.android.systemui.tuner.TunerService import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -60,8 +61,8 @@ import org.mockito.junit.MockitoJUnit @RunWith(AndroidTestingRunner::class) @TestableLooper.RunWithLooper class KeyguardBypassControllerTest : SysuiTestCase() { - private val utils = SceneTestUtils(this) - private val testScope = utils.testScope + private val kosmos = testKosmos() + private val testScope = kosmos.testScope private val featureFlags = FakeFeatureFlags() private val shadeRepository = FakeShadeRepository() diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardClockPositionAlgorithmTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardClockPositionAlgorithmTest.java index 4dc4798caa8c..bbf9a6b93201 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardClockPositionAlgorithmTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardClockPositionAlgorithmTest.java @@ -30,11 +30,13 @@ import android.testing.AndroidTestingRunner; import androidx.test.filters.SmallTest; +import com.android.systemui.Flags; import com.android.systemui.SysuiTestCase; import com.android.systemui.doze.util.BurnInHelperKt; import com.android.systemui.log.LogBuffer; import com.android.systemui.log.core.FakeLogBuffer; import com.android.systemui.res.R; +import com.android.systemui.shade.LargeScreenHeaderHelper; import org.junit.After; import org.junit.Before; @@ -79,6 +81,7 @@ public class KeyguardClockPositionAlgorithmTest extends SysuiTestCase { MockitoAnnotations.initMocks(this); mStaticMockSession = mockitoSession() .mockStatic(BurnInHelperKt.class) + .mockStatic(LargeScreenHeaderHelper.class) .startMocking(); LogBuffer logBuffer = FakeLogBuffer.Factory.Companion.create(); @@ -292,18 +295,44 @@ public class KeyguardClockPositionAlgorithmTest extends SysuiTestCase { } @Test - public void notifPaddingMakesUpToFullMarginInSplitShade() { + public void notifPaddingMakesUpToFullMarginInSplitShade_refactorFlagOff_usesResource() { + mSetFlagsRule.disableFlags(Flags.FLAG_CENTRALIZED_STATUS_BAR_DIMENS_REFACTOR); + int keyguardSplitShadeTopMargin = 100; + int largeScreenHeaderHeightResource = 70; when(mResources.getDimensionPixelSize(R.dimen.keyguard_split_shade_top_margin)) - .thenReturn(100); + .thenReturn(keyguardSplitShadeTopMargin); when(mResources.getDimensionPixelSize(R.dimen.large_screen_shade_header_height)) - .thenReturn(70); + .thenReturn(largeScreenHeaderHeightResource); mClockPositionAlgorithm.loadDimens(mContext, mResources); givenLockScreen(); mIsSplitShade = true; // WHEN the position algorithm is run positionClock(); - // THEN the notif padding makes up lacking margin (margin - header height = 30). - assertThat(mClockPosition.stackScrollerPadding).isEqualTo(30); + // THEN the notif padding makes up lacking margin (margin - header height). + int expectedPadding = keyguardSplitShadeTopMargin - largeScreenHeaderHeightResource; + assertThat(mClockPosition.stackScrollerPadding).isEqualTo(expectedPadding); + } + + @Test + public void notifPaddingMakesUpToFullMarginInSplitShade_refactorFlagOn_usesHelper() { + mSetFlagsRule.enableFlags(Flags.FLAG_CENTRALIZED_STATUS_BAR_DIMENS_REFACTOR); + int keyguardSplitShadeTopMargin = 100; + int largeScreenHeaderHeightHelper = 50; + int largeScreenHeaderHeightResource = 70; + when(LargeScreenHeaderHelper.getLargeScreenHeaderHeight(mContext)) + .thenReturn(largeScreenHeaderHeightHelper); + when(mResources.getDimensionPixelSize(R.dimen.keyguard_split_shade_top_margin)) + .thenReturn(keyguardSplitShadeTopMargin); + when(mResources.getDimensionPixelSize(R.dimen.large_screen_shade_header_height)) + .thenReturn(largeScreenHeaderHeightResource); + mClockPositionAlgorithm.loadDimens(mContext, mResources); + givenLockScreen(); + mIsSplitShade = true; + // WHEN the position algorithm is run + positionClock(); + // THEN the notif padding makes up lacking margin (margin - header height). + int expectedPadding = keyguardSplitShadeTopMargin - largeScreenHeaderHeightHelper; + assertThat(mClockPosition.stackScrollerPadding).isEqualTo(expectedPadding); } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.java index 5102b4f7a817..2d120cd9b13f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.java @@ -60,10 +60,10 @@ import com.android.systemui.flags.FakeFeatureFlagsClassic; import com.android.systemui.flags.Flags; import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository; import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor; +import com.android.systemui.kosmos.KosmosJavaAdapter; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.power.domain.interactor.PowerInteractorFactory; import com.android.systemui.res.R; -import com.android.systemui.scene.SceneTestUtils; import com.android.systemui.shade.ShadeViewStateProvider; import com.android.systemui.shade.data.repository.FakeShadeRepository; import com.android.systemui.statusbar.CommandQueue; @@ -149,7 +149,7 @@ public class KeyguardStatusBarViewControllerTest extends SysuiTestCase { private FakeExecutor mFakeExecutor = new FakeExecutor(new FakeSystemClock()); private final TestScope mTestScope = TestScopeProvider.getTestScope(); private final FakeKeyguardRepository mKeyguardRepository = new FakeKeyguardRepository(); - private final SceneTestUtils mSceneTestUtils = new SceneTestUtils(this); + private final KosmosJavaAdapter mKosmos = new KosmosJavaAdapter(this); private KeyguardInteractor mKeyguardInteractor; private KeyguardStatusBarViewModel mViewModel; @@ -166,11 +166,11 @@ public class KeyguardStatusBarViewControllerTest extends SysuiTestCase { mKeyguardRepository, mCommandQueue, PowerInteractorFactory.create().getPowerInteractor(), - mSceneTestUtils.getSceneContainerFlags(), + mKosmos.getFakeSceneContainerFlags(), new FakeKeyguardBouncerRepository(), new ConfigurationInteractor(new FakeConfigurationRepository()), new FakeShadeRepository(), - () -> mSceneTestUtils.sceneInteractor()); + () -> mKosmos.getSceneInteractor()); mViewModel = new KeyguardStatusBarViewModel( mTestScope.getBackgroundScope(), diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModelTest.kt index 9419d638b1c7..ca0e526bbc30 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModelTest.kt @@ -25,20 +25,22 @@ import com.android.systemui.coroutines.collectLastValue import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.keyguard.shared.model.StatusBarState +import com.android.systemui.kosmos.testScope import com.android.systemui.power.domain.interactor.PowerInteractorFactory -import com.android.systemui.scene.SceneTestUtils +import com.android.systemui.scene.domain.interactor.sceneInteractor +import com.android.systemui.scene.shared.flag.fakeSceneContainerFlags import com.android.systemui.shade.data.repository.FakeShadeRepository import com.android.systemui.statusbar.CommandQueue import com.android.systemui.statusbar.data.repository.FakeKeyguardStatusBarRepository import com.android.systemui.statusbar.domain.interactor.KeyguardStatusBarInteractor import com.android.systemui.statusbar.policy.BatteryController +import com.android.systemui.testKosmos import com.android.systemui.util.mockito.argumentCaptor import com.android.systemui.util.mockito.capture import com.android.systemui.util.mockito.mock import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Test @@ -47,20 +49,20 @@ import org.mockito.Mockito.verify @SmallTest @OptIn(ExperimentalCoroutinesApi::class) class KeyguardStatusBarViewModelTest : SysuiTestCase() { - private val testScope = TestScope() - private val sceneTestUtils = SceneTestUtils(this) + private val kosmos = testKosmos() + private val testScope = kosmos.testScope private val keyguardRepository = FakeKeyguardRepository() private val keyguardInteractor = KeyguardInteractor( keyguardRepository, mock<CommandQueue>(), PowerInteractorFactory.create().powerInteractor, - sceneTestUtils.sceneContainerFlags, + kosmos.fakeSceneContainerFlags, FakeKeyguardBouncerRepository(), ConfigurationInteractor(FakeConfigurationRepository()), FakeShadeRepository(), ) { - sceneTestUtils.sceneInteractor() + kosmos.sceneInteractor } private val keyguardStatusBarInteractor = KeyguardStatusBarInteractor( diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractorTest.kt index fb5375a1ab83..b6a033a7c5f6 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractorTest.kt @@ -40,13 +40,18 @@ import com.android.systemui.animation.Expandable import com.android.systemui.common.shared.model.Text import com.android.systemui.coroutines.collectLastValue import com.android.systemui.flags.Flags +import com.android.systemui.flags.fakeFeatureFlagsClassic import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository import com.android.systemui.keyguard.domain.interactor.KeyguardInteractorFactory +import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.kosmos.testScope import com.android.systemui.plugins.ActivityStarter import com.android.systemui.qs.user.UserSwitchDialogController import com.android.systemui.res.R -import com.android.systemui.scene.SceneTestUtils import com.android.systemui.statusbar.policy.DeviceProvisionedController +import com.android.systemui.telephony.data.repository.fakeTelephonyRepository +import com.android.systemui.telephony.domain.interactor.telephonyInteractor +import com.android.systemui.testKosmos import com.android.systemui.user.data.model.UserSwitcherSettingsModel import com.android.systemui.user.data.repository.FakeUserRepository import com.android.systemui.user.data.source.UserRecord @@ -98,8 +103,8 @@ class UserSwitcherInteractorTest : SysuiTestCase() { @Mock private lateinit var resetOrExitSessionReceiver: GuestResetOrExitSessionReceiver @Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor - private val utils = SceneTestUtils(this) - private val testScope = utils.testScope + private val kosmos = testKosmos() + private val testScope = kosmos.testScope private lateinit var spyContext: Context private lateinit var userRepository: FakeUserRepository private lateinit var keyguardReply: KeyguardInteractorFactory.WithDependencies @@ -121,16 +126,17 @@ class UserSwitcherInteractorTest : SysuiTestCase() { SUPERVISED_USER_CREATION_APP_PACKAGE, ) - utils.featureFlags.set(Flags.FULL_SCREEN_USER_SWITCHER, false) + kosmos.fakeFeatureFlagsClassic.set(Flags.FULL_SCREEN_USER_SWITCHER, false) mSetFlagsRule.enableFlags(AConfigFlags.FLAG_SWITCH_USER_ON_BG) spyContext = spy(context) - keyguardReply = KeyguardInteractorFactory.create(featureFlags = utils.featureFlags) + keyguardReply = + KeyguardInteractorFactory.create(featureFlags = kosmos.fakeFeatureFlagsClassic) keyguardRepository = keyguardReply.repository userRepository = FakeUserRepository() refreshUsersScheduler = RefreshUsersScheduler( applicationScope = testScope.backgroundScope, - mainDispatcher = utils.testDispatcher, + mainDispatcher = kosmos.testDispatcher, repository = userRepository, ) } @@ -363,7 +369,7 @@ class UserSwitcherInteractorTest : SysuiTestCase() { fun actions_deviceUnlocked_fullScreen() { createUserInteractor() testScope.runTest { - utils.featureFlags.set(Flags.FULL_SCREEN_USER_SWITCHER, true) + kosmos.fakeFeatureFlagsClassic.set(Flags.FULL_SCREEN_USER_SWITCHER, true) val userInfos = createUserInfos(count = 2, includeGuest = false) userRepository.setUserInfos(userInfos) @@ -447,7 +453,7 @@ class UserSwitcherInteractorTest : SysuiTestCase() { fun actions_deviceLockedAddFromLockscreenSet_fullList_fullScreen() { createUserInteractor() testScope.runTest { - utils.featureFlags.set(Flags.FULL_SCREEN_USER_SWITCHER, true) + kosmos.fakeFeatureFlagsClassic.set(Flags.FULL_SCREEN_USER_SWITCHER, true) val userInfos = createUserInfos(count = 2, includeGuest = false) userRepository.setUserInfos(userInfos) userRepository.setSelectedUserInfo(userInfos[0]) @@ -640,7 +646,7 @@ class UserSwitcherInteractorTest : SysuiTestCase() { val refreshUsersCallCount = userRepository.refreshUsersCallCount - utils.telephonyRepository.setCallState(1) + kosmos.fakeTelephonyRepository.setCallState(1) runCurrent() assertThat(userRepository.refreshUsersCallCount).isEqualTo(refreshUsersCallCount + 1) @@ -792,7 +798,7 @@ class UserSwitcherInteractorTest : SysuiTestCase() { fun userRecordsFullScreen() { createUserInteractor() testScope.runTest { - utils.featureFlags.set(Flags.FULL_SCREEN_USER_SWITCHER, true) + kosmos.fakeFeatureFlagsClassic.set(Flags.FULL_SCREEN_USER_SWITCHER, true) val userInfos = createUserInfos(count = 3, includeGuest = false) userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true)) userRepository.setUserInfos(userInfos) @@ -901,7 +907,7 @@ class UserSwitcherInteractorTest : SysuiTestCase() { fun showUserSwitcher_fullScreenEnabled_launchesFullScreenDialog() { createUserInteractor() testScope.runTest { - utils.featureFlags.set(Flags.FULL_SCREEN_USER_SWITCHER, true) + kosmos.fakeFeatureFlagsClassic.set(Flags.FULL_SCREEN_USER_SWITCHER, true) val expandable = mock<Expandable>() underTest.showUserSwitcher(expandable) @@ -1116,19 +1122,19 @@ class UserSwitcherInteractorTest : SysuiTestCase() { manager = manager, headlessSystemUserMode = headlessSystemUserMode, applicationScope = testScope.backgroundScope, - telephonyInteractor = utils.telephonyInteractor(), + telephonyInteractor = kosmos.telephonyInteractor, broadcastDispatcher = fakeBroadcastDispatcher, keyguardUpdateMonitor = keyguardUpdateMonitor, - backgroundDispatcher = utils.testDispatcher, - mainDispatcher = utils.testDispatcher, + backgroundDispatcher = kosmos.testDispatcher, + mainDispatcher = kosmos.testDispatcher, activityManager = activityManager, refreshUsersScheduler = refreshUsersScheduler, guestUserInteractor = GuestUserInteractor( applicationContext = spyContext, applicationScope = testScope.backgroundScope, - mainDispatcher = utils.testDispatcher, - backgroundDispatcher = utils.testDispatcher, + mainDispatcher = kosmos.testDispatcher, + backgroundDispatcher = kosmos.testDispatcher, manager = manager, repository = userRepository, deviceProvisionedController = deviceProvisionedController, @@ -1139,7 +1145,7 @@ class UserSwitcherInteractorTest : SysuiTestCase() { resetOrExitSessionReceiver = resetOrExitSessionReceiver, ), uiEventLogger = uiEventLogger, - featureFlags = utils.featureFlags, + featureFlags = kosmos.fakeFeatureFlagsClassic, userRestrictionChecker = mock(), ) } 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 8920d4df2a91..1ed045ff6546 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java @@ -117,11 +117,11 @@ import com.android.systemui.keyguard.domain.interactor.GlanceableHubTransitions; import com.android.systemui.keyguard.domain.interactor.InWindowLauncherUnlockAnimationInteractor; import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor; import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor; +import com.android.systemui.kosmos.KosmosJavaAdapter; import com.android.systemui.model.SysUiState; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.power.domain.interactor.PowerInteractor; import com.android.systemui.scene.FakeWindowRootViewComponent; -import com.android.systemui.scene.SceneTestUtils; import com.android.systemui.scene.data.repository.SceneContainerRepository; import com.android.systemui.scene.domain.interactor.SceneInteractor; import com.android.systemui.scene.shared.flag.FakeSceneContainerFlags; @@ -356,8 +356,8 @@ public class BubblesTest extends SysuiTestCase { @Mock private LargeScreenHeaderHelper mLargeScreenHeaderHelper; - private final SceneTestUtils mUtils = new SceneTestUtils(this); - private final TestScope mTestScope = mUtils.getTestScope(); + private final KosmosJavaAdapter mKosmos = new KosmosJavaAdapter(this); + private final TestScope mTestScope = mKosmos.getTestScope(); private ShadeInteractor mShadeInteractor; private ShellTaskOrganizer mShellTaskOrganizer; private TaskViewTransitions mTaskViewTransitions; @@ -408,8 +408,8 @@ public class BubblesTest extends SysuiTestCase { FakeConfigurationRepository configurationRepository = new FakeConfigurationRepository(); PowerInteractor powerInteractor = new PowerInteractor( - mUtils.getPowerRepository(), - mUtils.falsingCollector(), + mKosmos.getPowerRepository(), + mKosmos.getFalsingCollector(), mock(ScreenOffAnimationController.class), mStatusBarStateController); @@ -417,7 +417,7 @@ public class BubblesTest extends SysuiTestCase { mTestScope.getBackgroundScope(), new SceneContainerRepository( mTestScope.getBackgroundScope(), - mUtils.fakeSceneContainerConfig()), + mKosmos.getFakeSceneContainerConfig()), powerInteractor, mock(SceneLogger.class)); @@ -449,8 +449,8 @@ public class BubblesTest extends SysuiTestCase { keyguardTransitionRepository, keyguardTransitionInteractor, mTestScope.getBackgroundScope(), - mUtils.getTestDispatcher(), - mUtils.getTestDispatcher(), + mKosmos.getTestDispatcher(), + mKosmos.getTestDispatcher(), keyguardInteractor, featureFlags, shadeRepository, @@ -475,8 +475,8 @@ public class BubblesTest extends SysuiTestCase { keyguardTransitionRepository, keyguardTransitionInteractor, mTestScope.getBackgroundScope(), - mUtils.getTestDispatcher(), - mUtils.getTestDispatcher(), + mKosmos.getTestDispatcher(), + mKosmos.getTestDispatcher(), keyguardInteractor, featureFlags, mock(KeyguardSecurityModel.class), diff --git a/packages/SystemUI/tests/utils/src/android/content/ContextKosmos.kt b/packages/SystemUI/tests/utils/src/android/content/ContextKosmos.kt index f96c5080bb95..5e254bf4f879 100644 --- a/packages/SystemUI/tests/utils/src/android/content/ContextKosmos.kt +++ b/packages/SystemUI/tests/utils/src/android/content/ContextKosmos.kt @@ -16,9 +16,7 @@ package android.content -import com.android.systemui.SysuiTestableContext import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.testCase -val Kosmos.testableContext: SysuiTestableContext by Kosmos.Fixture { testCase.context } -var Kosmos.applicationContext: Context by Kosmos.Fixture { testableContext } +var Kosmos.applicationContext: Context by Kosmos.Fixture { testCase.context } diff --git a/packages/SystemUI/tests/utils/src/android/service/notification/NotificationListenerServiceKosmos.kt b/packages/SystemUI/tests/utils/src/android/service/notification/NotificationListenerServiceKosmos.kt new file mode 100644 index 000000000000..bff0d0e2ec81 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/android/service/notification/NotificationListenerServiceKosmos.kt @@ -0,0 +1,24 @@ +/* + * 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.service.notification + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.Kosmos.Fixture +import com.android.systemui.util.mockito.mock + +val Kosmos.notificationListenerService by Fixture { mockNotificationListenerService } +val Kosmos.mockNotificationListenerService by Fixture { mock<NotificationListenerService>() } diff --git a/packages/SystemUI/tests/utils/src/android/telephony/TelephonyManagerKosmos.kt b/packages/SystemUI/tests/utils/src/android/telephony/TelephonyManagerKosmos.kt new file mode 100644 index 000000000000..a4ee7026fdff --- /dev/null +++ b/packages/SystemUI/tests/utils/src/android/telephony/TelephonyManagerKosmos.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 android.telephony + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.Kosmos.Fixture +import com.android.systemui.util.mockito.mock +import com.android.systemui.util.mockito.whenever +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.ArgumentMatchers.anyString + +val Kosmos.telephonyManager by Fixture { + mock<TelephonyManager> { + whenever(createForSubscriptionId(anyInt())).thenReturn(this) + whenever(supplyIccLockPin(anyString())) + .thenReturn(PinResult(PinResult.PIN_RESULT_TYPE_SUCCESS, 3)) + } +} diff --git a/packages/SystemUI/tests/utils/src/android/view/accessibility/AccessibilityManagerKosmos.kt b/packages/SystemUI/tests/utils/src/android/view/accessibility/AccessibilityManagerKosmos.kt index a11bf6a5e79c..d095f4264cf6 100644 --- a/packages/SystemUI/tests/utils/src/android/view/accessibility/AccessibilityManagerKosmos.kt +++ b/packages/SystemUI/tests/utils/src/android/view/accessibility/AccessibilityManagerKosmos.kt @@ -18,6 +18,11 @@ package android.view.accessibility import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.Kosmos.Fixture +import com.android.systemui.statusbar.policy.AccessibilityManagerWrapper import com.android.systemui.util.mockito.mock var Kosmos.accessibilityManager by Fixture { mock<AccessibilityManager>() } + +var Kosmos.accessibilityManagerWrapper by Fixture { + AccessibilityManagerWrapper(accessibilityManager) +} diff --git a/packages/SystemUI/tests/utils/src/com/android/app/ActivityTaskManagerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/app/ActivityTaskManagerKosmos.kt new file mode 100644 index 000000000000..fa3e8f96923a --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/app/ActivityTaskManagerKosmos.kt @@ -0,0 +1,24 @@ +/* + * 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.app + +import android.app.ActivityTaskManager +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.Kosmos.Fixture +import com.android.systemui.util.mockito.mock + +val Kosmos.activityTaskManager by Fixture { mock<ActivityTaskManager>() } diff --git a/packages/SystemUI/tests/utils/src/com/android/internal/logging/MetricsLoggerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/internal/logging/MetricsLoggerKosmos.kt index a1815c527b35..22dff0aeb8a4 100644 --- a/packages/SystemUI/tests/utils/src/com/android/internal/logging/MetricsLoggerKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/internal/logging/MetricsLoggerKosmos.kt @@ -16,8 +16,12 @@ package com.android.internal.logging +import com.android.internal.logging.testing.FakeMetricsLogger +import com.android.internal.util.LatencyTracker import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.Kosmos.Fixture import com.android.systemui.util.mockito.mock -var Kosmos.metricsLogger by Fixture { mock<MetricsLogger>() } +val Kosmos.fakeMetricsLogger by Fixture { FakeMetricsLogger() } +val Kosmos.metricsLogger by Fixture<MetricsLogger> { fakeMetricsLogger } +val Kosmos.latencyTracker by Fixture { mock<LatencyTracker>() } diff --git a/packages/SystemUI/tests/utils/src/com/android/internal/util/EmergencyAffordanceManagerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/internal/util/EmergencyAffordanceManagerKosmos.kt new file mode 100644 index 000000000000..313343729fd9 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/internal/util/EmergencyAffordanceManagerKosmos.kt @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.util + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.Kosmos.Fixture +import com.android.systemui.util.mockito.mock + +val Kosmos.emergencyAffordanceManager by Fixture { mock<EmergencyAffordanceManager>() } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/SysuiTestCase.java b/packages/SystemUI/tests/utils/src/com/android/systemui/SysuiTestCase.java index d23dae9c762c..af7f4c894c7c 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/SysuiTestCase.java +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/SysuiTestCase.java @@ -39,6 +39,7 @@ import androidx.test.InstrumentationRegistry; import androidx.test.uiautomator.UiDevice; import com.android.systemui.broadcast.FakeBroadcastDispatcher; +import com.android.systemui.flags.SceneContainerRule; import org.junit.After; import org.junit.AfterClass; @@ -68,6 +69,9 @@ public abstract class SysuiTestCase { @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(DEVICE_DEFAULT); + @Rule(order = 10) + public final SceneContainerRule mSceneContainerRule = new SceneContainerRule(); + @Rule public SysuiTestableContext mContext = createTestableContext(); diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/TestMocksModule.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/TestMocksModule.kt index fdb9b30e546e..b18859dc4b25 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/TestMocksModule.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/TestMocksModule.kt @@ -18,9 +18,11 @@ package com.android.systemui import android.app.ActivityManager import android.app.admin.DevicePolicyManager import android.os.UserManager +import android.service.notification.NotificationListenerService import android.util.DisplayMetrics import android.view.LayoutInflater import com.android.internal.logging.MetricsLogger +import com.android.internal.statusbar.IStatusBarService import com.android.keyguard.KeyguardSecurityModel import com.android.keyguard.KeyguardUpdateMonitor import com.android.keyguard.KeyguardViewController @@ -49,9 +51,11 @@ import com.android.systemui.statusbar.NotificationShadeDepthController import com.android.systemui.statusbar.SysuiStatusBarStateController import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinator import com.android.systemui.statusbar.notification.collection.NotifCollection +import com.android.systemui.statusbar.notification.logging.NotificationPanelLogger import com.android.systemui.statusbar.notification.stack.AmbientState import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController import com.android.systemui.statusbar.notification.stack.NotificationStackSizeCalculator +import com.android.systemui.statusbar.notification.stack.ui.view.NotificationStatsLogger import com.android.systemui.statusbar.phone.DozeParameters import com.android.systemui.statusbar.phone.KeyguardBypassController import com.android.systemui.statusbar.phone.LSShadeTransitionLogger @@ -59,6 +63,7 @@ import com.android.systemui.statusbar.phone.ScreenOffAnimationController import com.android.systemui.statusbar.phone.ScrimController import com.android.systemui.statusbar.phone.SystemUIDialogManager import com.android.systemui.statusbar.policy.DeviceProvisionedController +import com.android.systemui.statusbar.policy.HeadsUpManager import com.android.systemui.statusbar.policy.ZenModeController import com.android.systemui.statusbar.window.StatusBarWindowController import com.android.systemui.unfold.UnfoldTransitionProgressProvider @@ -80,6 +85,7 @@ data class TestMocksModule( @get:Provides val deviceProvisionedController: DeviceProvisionedController = mock(), @get:Provides val dozeParameters: DozeParameters = mock(), @get:Provides val dumpManager: DumpManager = mock(), + @get:Provides val headsUpManager: HeadsUpManager = mock(), @get:Provides val guestResumeSessionReceiver: GuestResumeSessionReceiver = mock(), @get:Provides val keyguardBypassController: KeyguardBypassController = mock(), @get:Provides val keyguardSecurityModel: KeyguardSecurityModel = mock(), @@ -89,8 +95,10 @@ data class TestMocksModule( val lockscreenShadeTransitionController: LockscreenShadeTransitionController = mock(), @get:Provides val mediaHierarchyManager: MediaHierarchyManager = mock(), @get:Provides val notifCollection: NotifCollection = mock(), + @get:Provides val notificationListLogger: NotificationStatsLogger = mock(), @get:Provides val notificationListener: NotificationListener = mock(), @get:Provides val notificationLockscreenUserManager: NotificationLockscreenUserManager = mock(), + @get:Provides val notificationPanelLogger: NotificationPanelLogger = mock(), @get:Provides val notificationMediaManager: NotificationMediaManager = mock(), @get:Provides val notificationShadeDepthController: NotificationShadeDepthController = mock(), @get:Provides @@ -129,6 +137,10 @@ data class TestMocksModule( @get:Provides val displayMetrics: DisplayMetrics = mock(), @get:Provides val metricsLogger: MetricsLogger = mock(), @get:Provides val userManager: UserManager = mock(), + + // system server mocks + @get:Provides val mockStatusBarService: IStatusBarService = mock(), + @get:Provides val mockNotificationListenerService: NotificationListenerService = mock(), ) { @Module interface Bindings { diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakePromptRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakePromptRepository.kt index 42ec8fed0127..f192de23fecc 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakePromptRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakePromptRepository.kt @@ -26,12 +26,24 @@ class FakePromptRepository : PromptRepository { private val _isConfirmationRequired = MutableStateFlow(false) override val isConfirmationRequired = _isConfirmationRequired.asStateFlow() + private val _opPackageName: MutableStateFlow<String?> = MutableStateFlow(null) + override val opPackageName = _opPackageName.asStateFlow() + override fun setPrompt( promptInfo: PromptInfo, userId: Int, gatekeeperChallenge: Long?, kind: PromptKind, - ) = setPrompt(promptInfo, userId, gatekeeperChallenge, kind, forceConfirmation = false) + opPackageName: String, + ) = + setPrompt( + promptInfo, + userId, + gatekeeperChallenge, + kind, + forceConfirmation = false, + opPackageName = opPackageName + ) fun setPrompt( promptInfo: PromptInfo, @@ -39,12 +51,14 @@ class FakePromptRepository : PromptRepository { gatekeeperChallenge: Long?, kind: PromptKind, forceConfirmation: Boolean = false, + opPackageName: String? = null, ) { _promptInfo.value = promptInfo _userId.value = userId _challenge.value = gatekeeperChallenge _kind.value = kind _isConfirmationRequired.value = promptInfo.isConfirmationRequested || forceConfirmation + _opPackageName.value = opPackageName } override fun unsetPrompt() { diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/data/repository/BouncerRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/data/repository/BouncerRepositoryKosmos.kt new file mode 100644 index 000000000000..c0f8638e3d32 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/data/repository/BouncerRepositoryKosmos.kt @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.bouncer.data.repository + +import com.android.systemui.flags.featureFlagsClassic +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.Kosmos.Fixture + +val Kosmos.bouncerRepository by Fixture { + BouncerRepository( + flags = featureFlagsClassic, + ) +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/data/repository/EmergencyServicesRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/data/repository/EmergencyServicesRepositoryKosmos.kt new file mode 100644 index 000000000000..88517092934a --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/data/repository/EmergencyServicesRepositoryKosmos.kt @@ -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.systemui.bouncer.data.repository + +import android.content.res.mainResources +import com.android.systemui.common.ui.data.repository.configurationRepository +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.Kosmos.Fixture +import com.android.systemui.kosmos.testScope + +val Kosmos.emergencyServicesRepository by Fixture { + EmergencyServicesRepository( + applicationScope = testScope.backgroundScope, + resources = mainResources, + configurationRepository = configurationRepository, + ) +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/data/repository/SimBouncerRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/data/repository/SimBouncerRepositoryKosmos.kt new file mode 100644 index 000000000000..7af39dfa46b0 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/data/repository/SimBouncerRepositoryKosmos.kt @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.bouncer.data.repository + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.Kosmos.Fixture + +val Kosmos.fakeSimBouncerRepository by Fixture { FakeSimBouncerRepository() } + +val Kosmos.simBouncerRepository by Fixture<SimBouncerRepository> { fakeSimBouncerRepository } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/AlternateBouncerInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/AlternateBouncerInteractorKosmos.kt index 86a4509b4d62..c4fc30de3d06 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/AlternateBouncerInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/AlternateBouncerInteractorKosmos.kt @@ -25,7 +25,7 @@ import com.android.systemui.kosmos.testScope import com.android.systemui.plugins.statusbar.statusBarStateController import com.android.systemui.statusbar.policy.KeyguardStateControllerImpl import com.android.systemui.util.mockito.mock -import com.android.systemui.util.time.fakeSystemClock +import com.android.systemui.util.time.systemClock var Kosmos.alternateBouncerInteractor by Kosmos.Fixture { @@ -35,7 +35,7 @@ var Kosmos.alternateBouncerInteractor by bouncerRepository = keyguardBouncerRepository, fingerprintPropertyRepository = fingerprintPropertyRepository, biometricSettingsRepository = biometricSettingsRepository, - systemClock = fakeSystemClock, + systemClock = systemClock, keyguardUpdateMonitor = keyguardUpdateMonitor, scope = testScope.backgroundScope, ) diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/BouncerActionButtonInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/BouncerActionButtonInteractorKosmos.kt new file mode 100644 index 000000000000..5ced578ad974 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/BouncerActionButtonInteractorKosmos.kt @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.bouncer.domain.interactor + +import android.content.Intent +import android.content.applicationContext +import com.android.app.activityTaskManager +import com.android.internal.logging.metricsLogger +import com.android.internal.util.emergencyAffordanceManager +import com.android.systemui.authentication.domain.interactor.authenticationInteractor +import com.android.systemui.bouncer.data.repository.emergencyServicesRepository +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.Kosmos.Fixture +import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.statusbar.pipeline.mobile.data.repository.mobileConnectionsRepository +import com.android.systemui.telephony.domain.interactor.telephonyInteractor +import com.android.systemui.user.domain.interactor.selectedUserInteractor +import com.android.systemui.util.mockito.mock +import com.android.telecom.telecomManager + +val Kosmos.bouncerActionButtonInteractor by Fixture { + BouncerActionButtonInteractor( + applicationContext = applicationContext, + backgroundDispatcher = testDispatcher, + repository = emergencyServicesRepository, + mobileConnectionsRepository = mobileConnectionsRepository, + telephonyInteractor = telephonyInteractor, + authenticationInteractor = authenticationInteractor, + selectedUserInteractor = selectedUserInteractor, + activityTaskManager = activityTaskManager, + telecomManager = telecomManager, + emergencyAffordanceManager = emergencyAffordanceManager, + emergencyDialerIntentFactory = + object : EmergencyDialerIntentFactory { + override fun invoke(): Intent = Intent() + }, + metricsLogger = metricsLogger, + dozeLogger = mock(), + ) +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorKosmos.kt new file mode 100644 index 000000000000..27803b22de29 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorKosmos.kt @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.bouncer.domain.interactor + +import android.content.applicationContext +import com.android.systemui.authentication.domain.interactor.authenticationInteractor +import com.android.systemui.bouncer.data.repository.bouncerRepository +import com.android.systemui.classifier.domain.interactor.falsingInteractor +import com.android.systemui.deviceentry.domain.interactor.deviceEntryFaceAuthInteractor +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.Kosmos.Fixture +import com.android.systemui.kosmos.testScope +import com.android.systemui.power.domain.interactor.powerInteractor + +val Kosmos.bouncerInteractor by Fixture { + BouncerInteractor( + applicationScope = testScope.backgroundScope, + applicationContext = applicationContext, + repository = bouncerRepository, + authenticationInteractor = authenticationInteractor, + deviceEntryFaceAuthInteractor = deviceEntryFaceAuthInteractor, + falsingInteractor = falsingInteractor, + powerInteractor = powerInteractor, + simBouncerInteractor = simBouncerInteractor, + ) +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/SimBouncerInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/SimBouncerInteractorKosmos.kt new file mode 100644 index 000000000000..8ed9f45bd1ba --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/SimBouncerInteractorKosmos.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.bouncer.domain.interactor + +import android.content.Context +import android.content.applicationContext +import android.content.res.mainResources +import android.telephony.euicc.EuiccManager +import android.telephony.telephonyManager +import com.android.keyguard.keyguardUpdateMonitor +import com.android.systemui.bouncer.data.repository.simBouncerRepository +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.Kosmos.Fixture +import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.kosmos.testScope +import com.android.systemui.statusbar.pipeline.mobile.data.repository.mobileConnectionsRepository + +val Kosmos.simBouncerInteractor by Fixture { + SimBouncerInteractor( + applicationContext = applicationContext, + backgroundDispatcher = testDispatcher, + applicationScope = testScope.backgroundScope, + repository = simBouncerRepository, + telephonyManager = telephonyManager, + resources = mainResources, + keyguardUpdateMonitor = keyguardUpdateMonitor, + euiccManager = applicationContext.getSystemService(Context.EUICC_SERVICE) as EuiccManager, + mobileConnectionsRepository = mobileConnectionsRepository, + ) +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelKosmos.kt new file mode 100644 index 000000000000..d91c5974815c --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelKosmos.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.bouncer.ui.viewmodel + +import android.content.applicationContext +import com.android.systemui.authentication.domain.interactor.authenticationInteractor +import com.android.systemui.bouncer.domain.interactor.bouncerActionButtonInteractor +import com.android.systemui.bouncer.domain.interactor.bouncerInteractor +import com.android.systemui.bouncer.domain.interactor.simBouncerInteractor +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.Kosmos.Fixture +import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.kosmos.testScope +import com.android.systemui.scene.shared.flag.sceneContainerFlags +import com.android.systemui.user.ui.viewmodel.userSwitcherViewModel +import com.android.systemui.util.mockito.mock +import com.android.systemui.util.time.systemClock + +val Kosmos.bouncerViewModel by Fixture { + BouncerViewModel( + applicationContext = applicationContext, + applicationScope = testScope.backgroundScope, + mainDispatcher = testDispatcher, + bouncerInteractor = bouncerInteractor, + simBouncerInteractor = simBouncerInteractor, + authenticationInteractor = authenticationInteractor, + flags = sceneContainerFlags, + selectedUser = userSwitcherViewModel.selectedUser, + users = userSwitcherViewModel.users, + userSwitcherMenu = userSwitcherViewModel.menu, + actionButton = bouncerActionButtonInteractor.actionButton, + clock = systemClock, + devicePolicyManager = mock(), + ) +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/classifier/domain/interactor/FalsingInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/classifier/domain/interactor/FalsingInteractorKosmos.kt new file mode 100644 index 000000000000..8fee5b2b305c --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/classifier/domain/interactor/FalsingInteractorKosmos.kt @@ -0,0 +1,27 @@ +/* + * 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.classifier.domain.interactor + +import com.android.systemui.classifier.falsingCollector +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.Kosmos.Fixture + +val Kosmos.falsingInteractor by Fixture { + FalsingInteractor( + collector = falsingCollector, + ) +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/CommunalMediaRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/CommunalMediaRepositoryKosmos.kt index 7946446cb976..07681064a97d 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/CommunalMediaRepositoryKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/CommunalMediaRepositoryKosmos.kt @@ -18,6 +18,7 @@ package com.android.systemui.communal.data.repository import com.android.systemui.kosmos.Kosmos -var Kosmos.communalMediaRepository: CommunalMediaRepository by - Kosmos.Fixture { fakeCommunalMediaRepository } val Kosmos.fakeCommunalMediaRepository by Kosmos.Fixture { FakeCommunalMediaRepository() } + +val Kosmos.communalMediaRepository by + Kosmos.Fixture<CommunalMediaRepository> { fakeCommunalMediaRepository } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/CommunalPrefsRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/CommunalPrefsRepositoryKosmos.kt new file mode 100644 index 000000000000..79107cc818ba --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/CommunalPrefsRepositoryKosmos.kt @@ -0,0 +1,24 @@ +/* + * 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.data.repository + +import com.android.systemui.kosmos.Kosmos + +var Kosmos.communalPrefsRepository: CommunalPrefsRepository by + Kosmos.Fixture { fakeCommunalPrefsRepository } +val Kosmos.fakeCommunalPrefsRepository by Kosmos.Fixture { FakeCommunalPrefsRepository() } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/CommunalRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/CommunalRepositoryKosmos.kt index be56d2b20ded..1f5af5c38491 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/CommunalRepositoryKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/CommunalRepositoryKosmos.kt @@ -17,6 +17,8 @@ package com.android.systemui.communal.data.repository import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.Kosmos.Fixture -var Kosmos.communalRepository: CommunalRepository by Kosmos.Fixture { fakeCommunalRepository } -val Kosmos.fakeCommunalRepository by Kosmos.Fixture { FakeCommunalRepository() } +val Kosmos.fakeCommunalRepository by Fixture { FakeCommunalRepository() } + +val Kosmos.communalRepository by Fixture<CommunalRepository> { fakeCommunalRepository } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryKosmos.kt index 5a17f2f857cf..c225e3c8b9dc 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryKosmos.kt @@ -17,9 +17,14 @@ package com.android.systemui.communal.data.repository import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.Kosmos.Fixture import com.android.systemui.kosmos.applicationCoroutineScope -var Kosmos.communalWidgetRepository: CommunalWidgetRepository by - Kosmos.Fixture { fakeCommunalWidgetRepository } -val Kosmos.fakeCommunalWidgetRepository by - Kosmos.Fixture { FakeCommunalWidgetRepository(applicationCoroutineScope) } +val Kosmos.fakeCommunalWidgetRepository by Fixture { + FakeCommunalWidgetRepository( + coroutineScope = applicationCoroutineScope, + ) +} + +val Kosmos.communalWidgetRepository by + Fixture<CommunalWidgetRepository> { fakeCommunalWidgetRepository } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalPrefsRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalPrefsRepository.kt new file mode 100644 index 000000000000..d3ed58bf5be0 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalPrefsRepository.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.communal.data.repository + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow + +/** Fake implementation of [CommunalPrefsRepository] */ +class FakeCommunalPrefsRepository : CommunalPrefsRepository { + private val _isCtaDismissed = MutableStateFlow(false) + override val isCtaDismissed: Flow<Boolean> = _isCtaDismissed.asStateFlow() + + override suspend fun setCtaDismissedForCurrentUser() { + _isCtaDismissed.value = true + } +} 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 e82cae45c8f0..c85c27e277b4 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 @@ -9,7 +9,6 @@ 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.flatMapLatest import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.stateIn @@ -53,12 +52,4 @@ class FakeCommunalRepository( fun setIsCommunalHubShowing(isCommunalHubShowing: Boolean) { _isCommunalHubShowing.value = isCommunalHubShowing } - - private val _isCtaTileInViewModeVisible: MutableStateFlow<Boolean> = MutableStateFlow(true) - override val isCtaTileInViewModeVisible: Flow<Boolean> = - _isCtaTileInViewModeVisible.asStateFlow() - - override fun setCtaTileInViewModeVisibility(isVisible: Boolean) { - _isCtaTileInViewModeVisible.value = isVisible - } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorFactory.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorFactory.kt index 95ff889177b8..126bd6f03cca 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorFactory.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorFactory.kt @@ -17,11 +17,12 @@ package com.android.systemui.communal.domain.interactor -import android.appwidget.AppWidgetHost import com.android.systemui.communal.data.repository.FakeCommunalMediaRepository +import com.android.systemui.communal.data.repository.FakeCommunalPrefsRepository import com.android.systemui.communal.data.repository.FakeCommunalRepository import com.android.systemui.communal.data.repository.FakeCommunalTutorialRepository import com.android.systemui.communal.data.repository.FakeCommunalWidgetRepository +import com.android.systemui.communal.widgets.CommunalAppWidgetHost import com.android.systemui.communal.widgets.EditWidgetsActivityStarter import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor @@ -42,7 +43,8 @@ object CommunalInteractorFactory { mediaRepository: FakeCommunalMediaRepository = FakeCommunalMediaRepository(), smartspaceRepository: FakeSmartspaceRepository = FakeSmartspaceRepository(), tutorialRepository: FakeCommunalTutorialRepository = FakeCommunalTutorialRepository(), - appWidgetHost: AppWidgetHost = mock(), + communalPrefsRepository: FakeCommunalPrefsRepository = FakeCommunalPrefsRepository(), + appWidgetHost: CommunalAppWidgetHost = mock(), editWidgetsActivityStarter: EditWidgetsActivityStarter = mock(), ): WithDependencies { val withDeps = @@ -55,6 +57,7 @@ object CommunalInteractorFactory { testScope, communalRepository, widgetRepository, + communalPrefsRepository, mediaRepository, smartspaceRepository, tutorialRepository, @@ -66,6 +69,7 @@ object CommunalInteractorFactory { CommunalInteractor( communalRepository, widgetRepository, + communalPrefsRepository, mediaRepository, smartspaceRepository, withDeps.keyguardInteractor, @@ -79,13 +83,14 @@ object CommunalInteractorFactory { val testScope: TestScope, val communalRepository: FakeCommunalRepository, val widgetRepository: FakeCommunalWidgetRepository, + val communalPrefsRepository: FakeCommunalPrefsRepository, val mediaRepository: FakeCommunalMediaRepository, val smartspaceRepository: FakeSmartspaceRepository, val tutorialRepository: FakeCommunalTutorialRepository, val keyguardRepository: FakeKeyguardRepository, val keyguardInteractor: KeyguardInteractor, val tutorialInteractor: CommunalTutorialInteractor, - val appWidgetHost: AppWidgetHost, + val appWidgetHost: CommunalAppWidgetHost, val editWidgetsActivityStarter: EditWidgetsActivityStarter, val communalInteractor: CommunalInteractor, ) 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 new file mode 100644 index 000000000000..7cbbaabe9dfa --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorKosmos.kt @@ -0,0 +1,40 @@ +/* + * 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.domain.interactor + +import com.android.systemui.communal.data.repository.communalMediaRepository +import com.android.systemui.communal.data.repository.communalPrefsRepository +import com.android.systemui.communal.data.repository.communalRepository +import com.android.systemui.communal.data.repository.communalWidgetRepository +import com.android.systemui.keyguard.domain.interactor.keyguardInteractor +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.Kosmos.Fixture +import com.android.systemui.smartspace.data.repository.smartspaceRepository +import com.android.systemui.util.mockito.mock + +val Kosmos.communalInteractor by Fixture { + CommunalInteractor( + communalRepository = communalRepository, + widgetRepository = communalWidgetRepository, + mediaRepository = communalMediaRepository, + communalPrefsRepository = communalPrefsRepository, + smartspaceRepository = smartspaceRepository, + appWidgetHost = mock(), + keyguardInteractor = keyguardInteractor, + editWidgetsActivityStarter = mock(), + ) +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractorKosmos.kt index 6bf527df4026..de58ae5e9452 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractorKosmos.kt @@ -24,7 +24,7 @@ import com.android.systemui.keyevent.domain.interactor.keyEventInteractor import com.android.systemui.keyguard.data.repository.biometricSettingsRepository import com.android.systemui.kosmos.Kosmos import com.android.systemui.power.domain.interactor.powerInteractor -import com.android.systemui.util.time.fakeSystemClock +import com.android.systemui.util.time.systemClock import kotlinx.coroutines.ExperimentalCoroutinesApi val Kosmos.deviceEntryHapticsInteractor by @@ -37,7 +37,7 @@ val Kosmos.deviceEntryHapticsInteractor by biometricSettingsRepository = biometricSettingsRepository, keyEventInteractor = keyEventInteractor, powerInteractor = powerInteractor, - systemClock = fakeSystemClock, + systemClock = systemClock, logger = biometricUnlockLogger, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/flags/EnableSceneContainer.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/flags/EnableSceneContainer.kt new file mode 100644 index 000000000000..97f84c67f473 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/flags/EnableSceneContainer.kt @@ -0,0 +1,37 @@ +/* + * 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.flags + +import android.platform.test.annotations.EnableFlags +import com.android.systemui.Flags.FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR +import com.android.systemui.Flags.FLAG_KEYGUARD_SHADE_MIGRATION_NSSL +import com.android.systemui.Flags.FLAG_MEDIA_IN_SCENE_CONTAINER +import com.android.systemui.Flags.FLAG_SCENE_CONTAINER + +/** + * This includes @[EnableFlags] to work with [SetFlagsRule] to enable all aconfig flags required by + * that feature. It is also picked up by [SceneContainerRule] to set non-aconfig prerequisites. + */ +@EnableFlags( + FLAG_SCENE_CONTAINER, + FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR, + FLAG_KEYGUARD_SHADE_MIGRATION_NSSL, + FLAG_MEDIA_IN_SCENE_CONTAINER, +) +@Retention(AnnotationRetention.RUNTIME) +@Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS) +annotation class EnableSceneContainer diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/flags/FeatureFlagsClassicKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/flags/FeatureFlagsClassicKosmos.kt index abadaf754c30..7b36a29ad607 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/flags/FeatureFlagsClassicKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/flags/FeatureFlagsClassicKosmos.kt @@ -30,7 +30,13 @@ var Kosmos.featureFlagsClassic: FeatureFlagsClassic by Kosmos.Fixture { fakeFeat * Fixture supplying a shared [FakeFeatureFlagsClassic] instance. Can be accessed in tests in order * to override flag values. */ -val Kosmos.fakeFeatureFlagsClassic by Kosmos.Fixture { FakeFeatureFlagsClassic() } +val Kosmos.fakeFeatureFlagsClassic by + Kosmos.Fixture { + FakeFeatureFlagsClassic().apply { + set(Flags.FULL_SCREEN_USER_SWITCHER, false) + set(Flags.NSSL_DEBUG_LINES, false) + } + } /** * Fixture supplying a real [FeatureFlagsClassicRelease] instance, for use by tests that want to diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/flags/SceneContainerRule.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/flags/SceneContainerRule.kt new file mode 100644 index 000000000000..3faa6eb8f5f2 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/flags/SceneContainerRule.kt @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.flags + +import android.util.Log +import com.android.systemui.compose.ComposeFacade +import com.android.systemui.scene.shared.flag.SceneContainerFlag +import org.junit.Assert +import org.junit.Assume +import org.junit.rules.TestRule +import org.junit.runner.Description +import org.junit.runners.model.Statement + +/** + * Should always be used with [SetFlagsRule] and should be ordered after it. + * + * Used to ensure tests annotated with [EnableSceneContainer] can actually get `true` from + * [SceneContainerFlag.isEnabled]. + */ +class SceneContainerRule : TestRule { + override fun apply(base: Statement?, description: Description?): Statement { + return object : Statement() { + @Throws(Throwable::class) + override fun evaluate() { + val initialEnabledValue = Flags.SCENE_CONTAINER_ENABLED + val hasAnnotation = + description?.testClass?.getAnnotation(EnableSceneContainer::class.java) != + null || description?.getAnnotation(EnableSceneContainer::class.java) != null + if (hasAnnotation) { + Assume.assumeTrue( + "Compose must be available for @EnableSceneContainer test", + ComposeFacade.isComposeAvailable() + ) + Assume.assumeTrue( + "Couldn't set Flags.SCENE_CONTAINER_ENABLED for @EnableSceneContainer test", + trySetSceneContainerEnabled(true) + ) + Assert.assertTrue( + "SceneContainerFlag.isEnabled is false:" + + "\n * Did you forget to add a new aconfig flag dependency in" + + " @EnableSceneContainer?" + + "\n * Did you forget to use SetFlagsRule with an earlier order?", + SceneContainerFlag.isEnabled + ) + } + try { + base?.evaluate() + } finally { + if (hasAnnotation) { + trySetSceneContainerEnabled(initialEnabledValue) + } + } + } + } + } + + companion object { + fun trySetSceneContainerEnabled(enabled: Boolean): Boolean { + if (Flags.SCENE_CONTAINER_ENABLED == enabled) { + return true + } + return try { + // TODO(b/283300105): remove this reflection setting once the hard-coded + // Flags.SCENE_CONTAINER_ENABLED is no longer needed. + val field = Flags::class.java.getField("SCENE_CONTAINER_ENABLED") + field.isAccessible = true + field.set(null, enabled) // note: this does not work with multivalent tests + true + } catch (t: Throwable) { + Log.e("SceneContainerRule", "Unable to set SCENE_CONTAINER_ENABLED=$enabled", t) + false + } + } + } +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/jank/InteractionJankMonitorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/jank/InteractionJankMonitorKosmos.kt new file mode 100644 index 000000000000..5c5016daf029 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/jank/InteractionJankMonitorKosmos.kt @@ -0,0 +1,24 @@ +/* + * 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.jank + +import com.android.internal.jank.InteractionJankMonitor +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.Kosmos.Fixture +import com.android.systemui.util.mockito.mock + +val Kosmos.interactionJankMonitor by Fixture<InteractionJankMonitor> { mock() } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/KeyguardBlueprintRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/KeyguardBlueprintRepositoryKosmos.kt new file mode 100644 index 000000000000..19cd9502862f --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/KeyguardBlueprintRepositoryKosmos.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.keyguard.data.repository + +import com.android.systemui.common.ui.data.repository.configurationRepository +import com.android.systemui.keyguard.shared.model.KeyguardBlueprint +import com.android.systemui.keyguard.shared.model.KeyguardSection +import com.android.systemui.keyguard.ui.view.layout.blueprints.DefaultKeyguardBlueprint.Companion.DEFAULT +import com.android.systemui.kosmos.Kosmos + +val Kosmos.keyguardBlueprintRepository by + Kosmos.Fixture { + KeyguardBlueprintRepository( + configurationRepository = configurationRepository, + blueprints = setOf(defaultBlueprint), + ) + } + +private val defaultBlueprint = + object : KeyguardBlueprint { + override val id: String + get() = DEFAULT + override val sections: List<KeyguardSection> + get() = listOf() + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/CommunalInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/CommunalInteractorKosmos.kt index 59f0ec3cd3a5..ffca83bc6f08 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/CommunalInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/CommunalInteractorKosmos.kt @@ -16,11 +16,12 @@ package com.android.systemui.keyguard.domain.interactor -import android.appwidget.AppWidgetHost import com.android.systemui.communal.data.repository.communalMediaRepository +import com.android.systemui.communal.data.repository.communalPrefsRepository import com.android.systemui.communal.data.repository.communalRepository import com.android.systemui.communal.data.repository.communalWidgetRepository import com.android.systemui.communal.domain.interactor.CommunalInteractor +import com.android.systemui.communal.widgets.CommunalAppWidgetHost import com.android.systemui.communal.widgets.EditWidgetsActivityStarter import com.android.systemui.kosmos.Kosmos import com.android.systemui.smartspace.data.repository.smartspaceRepository @@ -32,9 +33,10 @@ val Kosmos.communalInteractor by communalRepository = communalRepository, widgetRepository = communalWidgetRepository, mediaRepository = communalMediaRepository, + communalPrefsRepository = communalPrefsRepository, smartspaceRepository = smartspaceRepository, keyguardInteractor = keyguardInteractor, - appWidgetHost = mock(AppWidgetHost::class.java), + appWidgetHost = mock(CommunalAppWidgetHost::class.java), editWidgetsActivityStarter = mock(EditWidgetsActivityStarter::class.java), ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractorKosmos.kt new file mode 100644 index 000000000000..d9a3192ce821 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractorKosmos.kt @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyguard.domain.interactor + +import android.content.applicationContext +import com.android.systemui.keyguard.data.repository.keyguardBlueprintRepository +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.applicationCoroutineScope +import com.android.systemui.statusbar.policy.splitShadeStateController + +val Kosmos.keyguardBlueprintInteractor by + Kosmos.Fixture { + KeyguardBlueprintInteractor( + keyguardBlueprintRepository = keyguardBlueprintRepository, + applicationScope = applicationCoroutineScope, + context = applicationContext, + splitShadeStateController = splitShadeStateController, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardLongPressInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardLongPressInteractorKosmos.kt new file mode 100644 index 000000000000..638a6a38595c --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardLongPressInteractorKosmos.kt @@ -0,0 +1,40 @@ +/* + * 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.domain.interactor + +import android.content.applicationContext +import android.view.accessibility.accessibilityManagerWrapper +import com.android.systemui.broadcast.broadcastDispatcher +import com.android.systemui.flags.featureFlagsClassic +import com.android.systemui.keyguard.data.repository.keyguardRepository +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.applicationCoroutineScope +import com.android.systemui.qs.uiEventLogger + +val Kosmos.keyguardLongPressInteractor by + Kosmos.Fixture { + KeyguardLongPressInteractor( + appContext = applicationContext, + scope = applicationCoroutineScope, + transitionInteractor = keyguardTransitionInteractor, + repository = keyguardRepository, + logger = uiEventLogger, + featureFlags = featureFlagsClassic, + broadcastDispatcher = broadcastDispatcher, + accessibilityManager = accessibilityManagerWrapper, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardLongPressViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardLongPressViewModelKosmos.kt new file mode 100644 index 000000000000..3c9846acf28c --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardLongPressViewModelKosmos.kt @@ -0,0 +1,27 @@ +/* + * 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 com.android.systemui.keyguard.domain.interactor.keyguardLongPressInteractor +import com.android.systemui.kosmos.Kosmos + +val Kosmos.keyguardLongPressViewModel by + Kosmos.Fixture { + KeyguardLongPressViewModel( + interactor = keyguardLongPressInteractor, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelKosmos.kt new file mode 100644 index 000000000000..96de4bae63d4 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelKosmos.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.keyguard.ui.viewmodel + +import com.android.systemui.biometrics.authController +import com.android.systemui.keyguard.domain.interactor.keyguardBlueprintInteractor +import com.android.systemui.keyguard.domain.interactor.keyguardClockInteractor +import com.android.systemui.kosmos.Kosmos + +val Kosmos.lockscreenContentViewModel by + Kosmos.Fixture { + LockscreenContentViewModel( + clockInteractor = keyguardClockInteractor, + interactor = keyguardBlueprintInteractor, + authController = authController, + longPress = keyguardLongPressViewModel, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt new file mode 100644 index 000000000000..24670b12193a --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.kosmos + +import android.content.applicationContext +import com.android.systemui.SysuiTestCase +import com.android.systemui.bouncer.data.repository.bouncerRepository +import com.android.systemui.bouncer.domain.interactor.simBouncerInteractor +import com.android.systemui.classifier.falsingCollector +import com.android.systemui.common.ui.data.repository.fakeConfigurationRepository +import com.android.systemui.common.ui.domain.interactor.configurationInteractor +import com.android.systemui.communal.data.repository.fakeCommunalRepository +import com.android.systemui.flags.fakeFeatureFlagsClassic +import com.android.systemui.jank.interactionJankMonitor +import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository +import com.android.systemui.plugins.statusbar.statusBarStateController +import com.android.systemui.power.data.repository.fakePowerRepository +import com.android.systemui.power.domain.interactor.powerInteractor +import com.android.systemui.scene.domain.interactor.sceneInteractor +import com.android.systemui.scene.sceneContainerConfig +import com.android.systemui.scene.shared.flag.fakeSceneContainerFlags +import com.android.systemui.statusbar.phone.screenOffAnimationController +import com.android.systemui.statusbar.pipeline.mobile.data.repository.fakeMobileConnectionsRepository +import com.android.systemui.util.time.systemClock + +/** Helper for using [Kosmos] from Java. */ +@Deprecated("Please convert your test to Kotlin and use [Kosmos] directly.") +class KosmosJavaAdapter( + testCase: SysuiTestCase, +) { + + private val kosmos = Kosmos() + + val testDispatcher by lazy { kosmos.testDispatcher } + val testScope by lazy { kosmos.testScope } + val fakeFeatureFlags by lazy { kosmos.fakeFeatureFlagsClassic } + val fakeSceneContainerFlags by lazy { kosmos.fakeSceneContainerFlags } + val configurationRepository by lazy { kosmos.fakeConfigurationRepository } + val configurationInteractor by lazy { kosmos.configurationInteractor } + val bouncerRepository by lazy { kosmos.bouncerRepository } + val communalRepository by lazy { kosmos.fakeCommunalRepository } + val keyguardRepository by lazy { kosmos.fakeKeyguardRepository } + val powerRepository by lazy { kosmos.fakePowerRepository } + val clock by lazy { kosmos.systemClock } + val mobileConnectionsRepository by lazy { kosmos.fakeMobileConnectionsRepository } + val simBouncerInteractor by lazy { kosmos.simBouncerInteractor } + val statusBarStateController by lazy { kosmos.statusBarStateController } + val interactionJankMonitor by lazy { kosmos.interactionJankMonitor } + val screenOffAnimationController by lazy { kosmos.screenOffAnimationController } + val fakeSceneContainerConfig by lazy { kosmos.sceneContainerConfig } + val sceneInteractor by lazy { kosmos.sceneInteractor } + val falsingCollector by lazy { kosmos.falsingCollector } + val powerInteractor by lazy { kosmos.powerInteractor } + + init { + kosmos.applicationContext = testCase.context + kosmos.testCase = testCase + } +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/plugins/statusbar/StatusBarStateControllerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/plugins/statusbar/StatusBarStateControllerKosmos.kt index cac2646a58f2..73b7c50eb8be 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/plugins/statusbar/StatusBarStateControllerKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/plugins/statusbar/StatusBarStateControllerKosmos.kt @@ -16,7 +16,20 @@ package com.android.systemui.plugins.statusbar +import com.android.systemui.jank.interactionJankMonitor import com.android.systemui.kosmos.Kosmos +import com.android.systemui.qs.uiEventLogger +import com.android.systemui.shade.domain.interactor.shadeInteractor +import com.android.systemui.statusbar.StatusBarStateControllerImpl import com.android.systemui.util.mockito.mock -var Kosmos.statusBarStateController by Kosmos.Fixture { mock<StatusBarStateController>() } +var Kosmos.statusBarStateController by + Kosmos.Fixture { + StatusBarStateControllerImpl( + uiEventLogger, + interactionJankMonitor, + mock(), + ) { + shadeInteractor + } + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneTestUtils.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneTestUtils.kt deleted file mode 100644 index 09ab6557c663..000000000000 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneTestUtils.kt +++ /dev/null @@ -1,423 +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.scene - -import android.app.ActivityTaskManager -import android.app.admin.DevicePolicyManager -import android.content.Context -import android.content.Intent -import android.content.pm.UserInfo -import android.graphics.Bitmap -import android.graphics.drawable.BitmapDrawable -import android.telecom.TelecomManager -import android.telephony.PinResult -import android.telephony.PinResult.PIN_RESULT_TYPE_SUCCESS -import android.telephony.TelephonyManager -import android.telephony.euicc.EuiccManager -import com.android.internal.logging.MetricsLogger -import com.android.internal.util.EmergencyAffordanceManager -import com.android.systemui.SysuiTestCase -import com.android.systemui.authentication.data.repository.AuthenticationRepository -import com.android.systemui.authentication.data.repository.FakeAuthenticationRepository -import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor -import com.android.systemui.bouncer.data.repository.BouncerRepository -import com.android.systemui.bouncer.data.repository.EmergencyServicesRepository -import com.android.systemui.bouncer.data.repository.FakeKeyguardBouncerRepository -import com.android.systemui.bouncer.data.repository.FakeSimBouncerRepository -import com.android.systemui.bouncer.domain.interactor.BouncerActionButtonInteractor -import com.android.systemui.bouncer.domain.interactor.BouncerInteractor -import com.android.systemui.bouncer.domain.interactor.EmergencyDialerIntentFactory -import com.android.systemui.bouncer.domain.interactor.SimBouncerInteractor -import com.android.systemui.bouncer.ui.viewmodel.BouncerViewModel -import com.android.systemui.classifier.FalsingCollector -import com.android.systemui.classifier.FalsingCollectorFake -import com.android.systemui.classifier.domain.interactor.FalsingInteractor -import com.android.systemui.common.shared.model.Text -import com.android.systemui.common.ui.data.repository.FakeConfigurationRepository -import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor -import com.android.systemui.communal.data.repository.FakeCommunalRepository -import com.android.systemui.communal.domain.interactor.CommunalInteractor -import com.android.systemui.communal.domain.interactor.CommunalInteractorFactory -import com.android.systemui.deviceentry.data.repository.DeviceEntryFaceAuthRepository -import com.android.systemui.deviceentry.data.repository.DeviceEntryRepository -import com.android.systemui.deviceentry.data.repository.FakeDeviceEntryRepository -import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFaceAuthInteractor -import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor -import com.android.systemui.doze.DozeLogger -import com.android.systemui.flags.FakeFeatureFlagsClassic -import com.android.systemui.flags.Flags -import com.android.systemui.keyguard.data.repository.FakeCommandQueue -import com.android.systemui.keyguard.data.repository.FakeDeviceEntryFaceAuthRepository -import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository -import com.android.systemui.keyguard.data.repository.FakeTrustRepository -import com.android.systemui.keyguard.data.repository.KeyguardRepository -import com.android.systemui.keyguard.data.repository.TrustRepository -import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor -import com.android.systemui.keyguard.ui.viewmodel.KeyguardRootViewModel -import com.android.systemui.kosmos.Kosmos -import com.android.systemui.kosmos.testDispatcher -import com.android.systemui.kosmos.testScope -import com.android.systemui.plugins.statusbar.StatusBarStateController -import com.android.systemui.power.data.repository.FakePowerRepository -import com.android.systemui.power.data.repository.PowerRepository -import com.android.systemui.power.domain.interactor.PowerInteractor -import com.android.systemui.power.domain.interactor.PowerInteractorFactory -import com.android.systemui.scene.data.repository.SceneContainerRepository -import com.android.systemui.scene.domain.interactor.SceneInteractor -import com.android.systemui.scene.shared.flag.FakeSceneContainerFlags -import com.android.systemui.scene.shared.model.SceneContainerConfig -import com.android.systemui.scene.shared.model.SceneKey -import com.android.systemui.shade.data.repository.FakeShadeRepository -import com.android.systemui.statusbar.notification.stack.data.repository.NotificationStackAppearanceRepository -import com.android.systemui.statusbar.notification.stack.domain.interactor.NotificationStackAppearanceInteractor -import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationsPlaceholderViewModel -import com.android.systemui.statusbar.phone.ScreenOffAnimationController -import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionsRepository -import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionsRepository -import com.android.systemui.telephony.data.repository.FakeTelephonyRepository -import com.android.systemui.telephony.data.repository.TelephonyRepository -import com.android.systemui.telephony.domain.interactor.TelephonyInteractor -import com.android.systemui.user.data.repository.FakeUserRepository -import com.android.systemui.user.domain.interactor.SelectedUserInteractor -import com.android.systemui.user.ui.viewmodel.UserActionViewModel -import com.android.systemui.user.ui.viewmodel.UserViewModel -import com.android.systemui.util.mockito.mock -import com.android.systemui.util.mockito.whenever -import com.android.systemui.util.time.SystemClock -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.test.currentTime -import org.mockito.ArgumentMatchers.anyInt -import org.mockito.ArgumentMatchers.anyString - -/** - * Utilities for creating scene container framework related repositories, interactors, and - * view-models for tests. - */ -@OptIn(ExperimentalCoroutinesApi::class) -class SceneTestUtils( - private val context: Context, -) { - constructor(test: SysuiTestCase) : this(context = test.context) - - val kosmos = Kosmos() - val testDispatcher = kosmos.testDispatcher - val testScope = kosmos.testScope - val featureFlags = - FakeFeatureFlagsClassic().apply { - set(Flags.FULL_SCREEN_USER_SWITCHER, false) - set(Flags.NSSL_DEBUG_LINES, false) - } - val sceneContainerFlags = FakeSceneContainerFlags().apply { enabled = true } - val deviceEntryRepository: FakeDeviceEntryRepository by lazy { FakeDeviceEntryRepository() } - val authenticationRepository: FakeAuthenticationRepository by lazy { - FakeAuthenticationRepository( - currentTime = { testScope.currentTime }, - ) - } - val configurationRepository: FakeConfigurationRepository by lazy { - FakeConfigurationRepository() - } - val configurationInteractor: ConfigurationInteractor by lazy { - ConfigurationInteractor(configurationRepository) - } - private val emergencyServicesRepository: EmergencyServicesRepository by lazy { - EmergencyServicesRepository( - applicationScope = applicationScope(), - resources = context.resources, - configurationRepository = configurationRepository, - ) - } - val telephonyRepository: FakeTelephonyRepository by lazy { FakeTelephonyRepository() } - val bouncerRepository = BouncerRepository(featureFlags) - val communalRepository: FakeCommunalRepository by lazy { FakeCommunalRepository() } - val keyguardRepository: FakeKeyguardRepository by lazy { FakeKeyguardRepository() } - val powerRepository: FakePowerRepository by lazy { FakePowerRepository() } - val simBouncerRepository: FakeSimBouncerRepository by lazy { FakeSimBouncerRepository() } - - val clock: SystemClock = mock { - whenever(elapsedRealtime()).thenAnswer { testScope.currentTime } - } - val telephonyManager: TelephonyManager = mock { - whenever(createForSubscriptionId(anyInt())).thenReturn(this) - whenever(supplyIccLockPin(anyString())).thenReturn(PinResult(PIN_RESULT_TYPE_SUCCESS, 3)) - } - val devicePolicyManager: DevicePolicyManager = mock {} - val mobileConnectionsRepository: FakeMobileConnectionsRepository by lazy { - FakeMobileConnectionsRepository(mock(), mock()) - } - - val simBouncerInteractor = - SimBouncerInteractor( - applicationContext = context, - backgroundDispatcher = testDispatcher, - applicationScope = applicationScope(), - repository = simBouncerRepository, - telephonyManager = telephonyManager, - resources = context.resources, - keyguardUpdateMonitor = mock(), - euiccManager = context.getSystemService(Context.EUICC_SERVICE) as EuiccManager, - mobileConnectionsRepository = mobileConnectionsRepository, - ) - - val userRepository: FakeUserRepository by lazy { - FakeUserRepository().apply { - val users = listOf(UserInfo(/* id= */ 0, "name", /* flags= */ 0)) - setUserInfos(users) - runBlocking { setSelectedUserInfo(users.first()) } - } - } - - private val falsingCollectorFake: FalsingCollector by lazy { FalsingCollectorFake() } - private var falsingInteractor: FalsingInteractor? = null - private var powerInteractor: PowerInteractor? = null - - fun fakeSceneContainerRepository( - containerConfig: SceneContainerConfig = fakeSceneContainerConfig(), - ): SceneContainerRepository { - return SceneContainerRepository(applicationScope(), containerConfig) - } - - fun fakeSceneKeys(): List<SceneKey> { - return kosmos.sceneKeys - } - - fun fakeSceneContainerConfig(): SceneContainerConfig { - return kosmos.sceneContainerConfig - } - - @JvmOverloads - fun sceneInteractor( - repository: SceneContainerRepository = fakeSceneContainerRepository() - ): SceneInteractor { - return SceneInteractor( - applicationScope = applicationScope(), - repository = repository, - powerInteractor = powerInteractor(), - logger = mock(), - ) - } - - fun deviceEntryInteractor( - repository: DeviceEntryRepository = deviceEntryRepository, - authenticationInteractor: AuthenticationInteractor, - sceneInteractor: SceneInteractor, - faceAuthRepository: DeviceEntryFaceAuthRepository = FakeDeviceEntryFaceAuthRepository(), - trustRepository: TrustRepository = FakeTrustRepository(), - ): DeviceEntryInteractor { - return DeviceEntryInteractor( - applicationScope = applicationScope(), - repository = repository, - authenticationInteractor = authenticationInteractor, - sceneInteractor = sceneInteractor, - deviceEntryFaceAuthRepository = faceAuthRepository, - trustRepository = trustRepository, - flags = FakeSceneContainerFlags(enabled = true) - ) - } - - fun authenticationInteractor( - repository: AuthenticationRepository = authenticationRepository, - ): AuthenticationInteractor { - return AuthenticationInteractor( - applicationScope = applicationScope(), - repository = repository, - selectedUserInteractor = selectedUserInteractor(), - ) - } - - fun keyguardInteractor( - repository: KeyguardRepository = keyguardRepository - ): KeyguardInteractor { - return KeyguardInteractor( - repository = repository, - commandQueue = FakeCommandQueue(), - sceneContainerFlags = sceneContainerFlags, - bouncerRepository = FakeKeyguardBouncerRepository(), - configurationInteractor = configurationInteractor, - shadeRepository = FakeShadeRepository(), - sceneInteractorProvider = { sceneInteractor() }, - powerInteractor = PowerInteractorFactory.create().powerInteractor, - ) - } - - fun communalInteractor(): CommunalInteractor { - return CommunalInteractorFactory.create( - communalRepository = communalRepository, - ) - .communalInteractor - } - - fun bouncerInteractor( - authenticationInteractor: AuthenticationInteractor, - deviceEntryFaceAuthInteractor: DeviceEntryFaceAuthInteractor = mock(), - ): BouncerInteractor { - return BouncerInteractor( - applicationScope = applicationScope(), - applicationContext = context, - repository = bouncerRepository, - authenticationInteractor = authenticationInteractor, - deviceEntryFaceAuthInteractor = deviceEntryFaceAuthInteractor, - falsingInteractor = falsingInteractor(), - powerInteractor = powerInteractor(), - simBouncerInteractor = simBouncerInteractor, - ) - } - - fun keyguardRootViewModel(): KeyguardRootViewModel = mock() - - fun notificationsPlaceholderViewModel(): NotificationsPlaceholderViewModel { - return NotificationsPlaceholderViewModel( - interactor = - NotificationStackAppearanceInteractor( - repository = NotificationStackAppearanceRepository(), - ), - flags = sceneContainerFlags, - featureFlags = featureFlags, - ) - } - - fun bouncerViewModel( - bouncerInteractor: BouncerInteractor, - authenticationInteractor: AuthenticationInteractor, - actionButtonInteractor: BouncerActionButtonInteractor = bouncerActionButtonInteractor(), - users: List<UserViewModel> = createUsers(), - ): BouncerViewModel { - return BouncerViewModel( - applicationContext = context, - applicationScope = applicationScope(), - mainDispatcher = testDispatcher, - bouncerInteractor = bouncerInteractor, - simBouncerInteractor = simBouncerInteractor, - authenticationInteractor = authenticationInteractor, - flags = sceneContainerFlags, - selectedUser = flowOf(users.first { it.isSelectionMarkerVisible }), - users = flowOf(users), - userSwitcherMenu = flowOf(createMenuActions()), - actionButton = actionButtonInteractor.actionButton, - clock = clock, - devicePolicyManager = devicePolicyManager, - ) - } - - fun telephonyInteractor( - repository: TelephonyRepository = telephonyRepository, - ): TelephonyInteractor { - return TelephonyInteractor(repository = repository) - } - - fun falsingInteractor(collector: FalsingCollector = falsingCollector()): FalsingInteractor { - return falsingInteractor ?: FalsingInteractor(collector).also { falsingInteractor = it } - } - - fun falsingCollector(): FalsingCollector { - return falsingCollectorFake - } - - fun powerInteractor( - repository: PowerRepository = powerRepository, - falsingCollector: FalsingCollector = falsingCollector(), - screenOffAnimationController: ScreenOffAnimationController = mock(), - statusBarStateController: StatusBarStateController = mock(), - ): PowerInteractor { - return powerInteractor - ?: PowerInteractor( - repository = repository, - falsingCollector = falsingCollector, - screenOffAnimationController = screenOffAnimationController, - statusBarStateController = statusBarStateController, - ) - .also { powerInteractor = it } - } - - private fun applicationScope(): CoroutineScope { - return testScope.backgroundScope - } - - private fun createUsers( - count: Int = 3, - selectedIndex: Int = 0, - ): List<UserViewModel> { - check(selectedIndex in 0 until count) - - return buildList { - repeat(count) { index -> - add( - UserViewModel( - viewKey = index, - name = Text.Loaded("name_$index"), - image = BitmapDrawable(Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888)), - isSelectionMarkerVisible = index == selectedIndex, - alpha = 1f, - onClicked = {}, - ) - ) - } - } - } - - private fun createMenuActions(): List<UserActionViewModel> { - return buildList { - repeat(3) { index -> - add( - UserActionViewModel( - viewKey = index.toLong(), - iconResourceId = 0, - textResourceId = 0, - onClicked = {}, - ) - ) - } - } - } - - fun selectedUserInteractor(): SelectedUserInteractor { - return SelectedUserInteractor(userRepository) - } - - fun bouncerActionButtonInteractor( - mobileConnectionsRepository: MobileConnectionsRepository = mock(), - activityTaskManager: ActivityTaskManager = mock(), - telecomManager: TelecomManager? = null, - emergencyAffordanceManager: EmergencyAffordanceManager = - EmergencyAffordanceManager(context), - emergencyDialerIntentFactory: EmergencyDialerIntentFactory = - object : EmergencyDialerIntentFactory { - override fun invoke(): Intent = Intent() - }, - metricsLogger: MetricsLogger = mock(), - dozeLogger: DozeLogger = mock(), - ): BouncerActionButtonInteractor { - return BouncerActionButtonInteractor( - applicationContext = context, - backgroundDispatcher = testDispatcher, - repository = emergencyServicesRepository, - mobileConnectionsRepository = mobileConnectionsRepository, - telephonyInteractor = telephonyInteractor(), - authenticationInteractor = authenticationInteractor(), - selectedUserInteractor = selectedUserInteractor(), - activityTaskManager = activityTaskManager, - telecomManager = telecomManager, - emergencyAffordanceManager = emergencyAffordanceManager, - emergencyDialerIntentFactory = emergencyDialerIntentFactory, - metricsLogger = metricsLogger, - dozeLogger = dozeLogger, - ) - } -} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/data/repository/SceneContainerRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/data/repository/SceneContainerRepositoryKosmos.kt index 7c4e160f6d05..e19941cfbaa0 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/data/repository/SceneContainerRepositoryKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/data/repository/SceneContainerRepositoryKosmos.kt @@ -18,7 +18,7 @@ package com.android.systemui.scene.data.repository import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.applicationCoroutineScope -import com.android.systemui.scene.shared.model.sceneContainerConfig +import com.android.systemui.scene.sceneContainerConfig val Kosmos.sceneContainerRepository by Kosmos.Fixture { SceneContainerRepository(applicationCoroutineScope, sceneContainerConfig) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/flag/SceneContainerFlagsKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/flag/SceneContainerFlagsKosmos.kt index c2cdbed21abe..979d8e76f3ee 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/flag/SceneContainerFlagsKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/flag/SceneContainerFlagsKosmos.kt @@ -18,4 +18,5 @@ package com.android.systemui.scene.shared.flag import com.android.systemui.kosmos.Kosmos -var Kosmos.sceneContainerFlags by Kosmos.Fixture { FakeSceneContainerFlags() } +var Kosmos.fakeSceneContainerFlags by Kosmos.Fixture { FakeSceneContainerFlags() } +val Kosmos.sceneContainerFlags by Kosmos.Fixture<SceneContainerFlags> { fakeSceneContainerFlags } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/model/FakeSceneContainerConfigModule.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/model/FakeSceneContainerConfigModule.kt index b4fc948cd2e0..8811b8db1cc0 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/model/FakeSceneContainerConfigModule.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/model/FakeSceneContainerConfigModule.kt @@ -30,6 +30,7 @@ data class FakeSceneContainerConfigModule( SceneKey.Lockscreen, SceneKey.Bouncer, SceneKey.Gone, + SceneKey.Communal, ), initialSceneKey = SceneKey.Lockscreen, ), diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/smartspace/data/repository/SmartspaceRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/smartspace/data/repository/SmartspaceRepositoryKosmos.kt index e671d4527be1..0e4c923a3078 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/smartspace/data/repository/SmartspaceRepositoryKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/smartspace/data/repository/SmartspaceRepositoryKosmos.kt @@ -17,6 +17,8 @@ package com.android.systemui.smartspace.data.repository import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.Kosmos.Fixture -var Kosmos.smartspaceRepository: SmartspaceRepository by Kosmos.Fixture { fakeSmartspaceRepository } -val Kosmos.fakeSmartspaceRepository by Kosmos.Fixture { FakeSmartspaceRepository() } +val Kosmos.fakeSmartspaceRepository by Fixture { FakeSmartspaceRepository() } + +val Kosmos.smartspaceRepository by Fixture<SmartspaceRepository> { fakeSmartspaceRepository } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/LockscreenShadeScrimTransitionControllerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/LockscreenShadeScrimTransitionControllerKosmos.kt index 93a7adf620d2..83854033e89f 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/LockscreenShadeScrimTransitionControllerKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/LockscreenShadeScrimTransitionControllerKosmos.kt @@ -16,7 +16,7 @@ package com.android.systemui.statusbar -import android.content.testableContext +import android.content.applicationContext import com.android.systemui.dump.dumpManager import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.Kosmos.Fixture @@ -27,7 +27,7 @@ import com.android.systemui.statusbar.policy.splitShadeStateController val Kosmos.lockscreenShadeScrimTransitionController by Fixture { LockscreenShadeScrimTransitionController( scrimController = scrimController, - context = testableContext, + context = applicationContext, configurationController = configurationController, dumpManager = dumpManager, splitShadeStateController = splitShadeStateController, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/LockscreenShadeTransitionControllerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/LockscreenShadeTransitionControllerKosmos.kt index 2752cc23f88b..1c6ce7987cd5 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/LockscreenShadeTransitionControllerKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/LockscreenShadeTransitionControllerKosmos.kt @@ -16,7 +16,7 @@ package com.android.systemui.statusbar -import android.content.testableContext +import android.content.applicationContext import com.android.systemui.classifier.falsingCollector import com.android.systemui.classifier.falsingManager import com.android.systemui.dump.dumpManager @@ -47,7 +47,7 @@ val Kosmos.lockscreenShadeTransitionController by Fixture { scrimTransitionController = lockscreenShadeScrimTransitionController, keyguardTransitionControllerFactory = lockscreenShadeKeyguardTransitionControllerFactory, depthController = notificationShadeDepthController, - context = testableContext, + context = applicationContext, splitShadeOverScrollerFactory = splitShadeLockScreenOverScrollerFactory, singleShadeOverScrollerFactory = singleShadeLockScreenOverScrollerFactory, activityStarter = activityStarter, 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 index da956ec67696..da956ec67696 100644 --- 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 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 index dda7fadde2d7..4efcada96a14 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 @@ public class GroupEntryBuilder { 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 @@ public class GroupEntryBuilder { return this; } + /** Get the group's internal children list. */ public static List<NotificationEntry> getRawChildren(GroupEntry groupEntry) { return groupEntry.getRawChildren(); } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotifCollectionKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/NotifCollectionKosmos.kt index d98f49684999..9b27a9fa818f 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotifCollectionKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/NotifCollectionKosmos.kt @@ -14,11 +14,11 @@ * limitations under the License. */ -package com.android.systemui.statusbar.notification.stack.ui.viewbinder +package com.android.systemui.statusbar.notification.collection import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.Kosmos.Fixture -import com.android.systemui.statusbar.notification.collection.NotifCollection import com.android.systemui.util.mockito.mock -var Kosmos.notifCollection by Fixture { mock<NotifCollection>() } +val Kosmos.notifCollection by Fixture { mockNotifCollection } +val Kosmos.mockNotifCollection by Fixture { mock<NotifCollection>() } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/model/ActiveNotificationModelBuilder.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/model/ActiveNotificationModelBuilder.kt index 9851b0ef9e94..9c5c48670ff4 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/model/ActiveNotificationModelBuilder.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/model/ActiveNotificationModelBuilder.kt @@ -18,6 +18,7 @@ package com.android.systemui.statusbar.notification.data.model import android.graphics.drawable.Icon import com.android.systemui.statusbar.notification.shared.ActiveNotificationModel +import com.android.systemui.statusbar.notification.stack.BUCKET_UNKNOWN /** Simple ActiveNotificationModel builder for use in tests. */ fun activeNotificationModel( @@ -32,6 +33,11 @@ fun activeNotificationModel( aodIcon: Icon? = null, shelfIcon: Icon? = null, statusBarIcon: Icon? = null, + uid: Int = 0, + instanceId: Int? = null, + isGroupSummary: Boolean = false, + packageName: String = "pkg", + bucket: Int = BUCKET_UNKNOWN, ) = ActiveNotificationModel( key = key, @@ -45,4 +51,9 @@ fun activeNotificationModel( aodIcon = aodIcon, shelfIcon = shelfIcon, statusBarIcon = statusBarIcon, + uid = uid, + packageName = packageName, + instanceId = instanceId, + isGroupSummary = isGroupSummary, + bucket = bucket, ) diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/repository/ActiveNotificationListRepositoryExt.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/repository/ActiveNotificationListRepositoryExt.kt index cb1ba206d110..b40e1e7ab33b 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/repository/ActiveNotificationListRepositoryExt.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/repository/ActiveNotificationListRepositoryExt.kt @@ -20,11 +20,20 @@ import com.android.systemui.statusbar.notification.data.model.activeNotification /** * Make the repository hold [count] active notifications for testing. The keys of the notifications - * are "0", "1", ..., (count - 1).toString(). + * are "0", "1", ..., (count - 1).toString(). The ranks are the same values in Int. */ fun ActiveNotificationListRepository.setActiveNotifs(count: Int) { this.activeNotifications.value = ActiveNotificationsStore.Builder() - .apply { repeat(count) { i -> addEntry(activeNotificationModel(key = i.toString())) } } + .apply { + val rankingsMap = mutableMapOf<String, Int>() + repeat(count) { i -> + val key = "$i" + addEntry(activeNotificationModel(key = key)) + rankingsMap[key] = i + } + + setRankingsMap(rankingsMap) + } .build() } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerViewBinderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerViewBinderKosmos.kt index 67fecb4d8bcd..acc455f74946 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerViewBinderKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerViewBinderKosmos.kt @@ -19,8 +19,8 @@ package com.android.systemui.statusbar.notification.icon.ui.viewbinder import com.android.systemui.common.ui.configurationState import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.Kosmos.Fixture +import com.android.systemui.statusbar.notification.collection.notifCollection import com.android.systemui.statusbar.notification.icon.ui.viewmodel.notificationIconContainerShelfViewModel -import com.android.systemui.statusbar.notification.stack.ui.viewbinder.notifCollection import com.android.systemui.statusbar.ui.systemBarUtilsState val Kosmos.notificationIconContainerShelfViewBinder by Fixture { diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLogger.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLogger.kt new file mode 100644 index 000000000000..30fc2f3af987 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/logging/NotificationPanelLogger.kt @@ -0,0 +1,24 @@ +/* + * 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.logging + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.Kosmos.Fixture +import com.android.systemui.util.mockito.mock + +val Kosmos.notificationPanelLogger by Fixture { mockNotificationPanelLogger } +val Kosmos.mockNotificationPanelLogger by Fixture { mock<NotificationPanelLogger>() } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/AmbientStateKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/AmbientStateKosmos.kt index 83ac330ee3b4..7f6f698c2932 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/AmbientStateKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/AmbientStateKosmos.kt @@ -16,7 +16,7 @@ package com.android.systemui.statusbar.notification.stack -import android.content.testableContext +import android.content.applicationContext import com.android.systemui.dump.dumpManager import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.Kosmos.Fixture @@ -27,7 +27,7 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi @OptIn(ExperimentalCoroutinesApi::class) val Kosmos.ambientState by Fixture { AmbientState( - /*context=*/ testableContext, + /*context=*/ applicationContext, /*dumpManager=*/ dumpManager, /*sectionProvider=*/ stackScrollAlgorithmSectionProvider, /*bypassController=*/ stackScrollAlgorithmBypassController, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/DisplaySwitchNotficationsHiderTrackerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/DisplaySwitchNotficationsHiderTrackerKosmos.kt new file mode 100644 index 000000000000..b12f5af03431 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/DisplaySwitchNotficationsHiderTrackerKosmos.kt @@ -0,0 +1,26 @@ +/* + * 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.stack + +import com.android.internal.logging.latencyTracker +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.Kosmos.Fixture +import com.android.systemui.shade.domain.interactor.shadeInteractor + +val Kosmos.displaySwitchNotificationsHiderTracker by Fixture { + DisplaySwitchNotificationsHiderTracker(shadeInteractor, latencyTracker) +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationStatsLoggerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationStatsLoggerKosmos.kt new file mode 100644 index 000000000000..de52155dce79 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationStatsLoggerKosmos.kt @@ -0,0 +1,35 @@ +/* + * 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.stack.ui.view + +import android.service.notification.notificationListenerService +import com.android.internal.statusbar.statusBarService +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.Kosmos.Fixture +import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.kosmos.testScope +import com.android.systemui.statusbar.notification.logging.notificationPanelLogger + +val Kosmos.notificationStatsLogger by Fixture { + NotificationStatsLoggerImpl( + applicationScope = testScope, + bgDispatcher = testDispatcher, + statusBarService = statusBarService, + notificationListenerService = notificationListenerService, + notificationPanelLogger = notificationPanelLogger, + ) +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinderKosmos.kt index 04716b9c48a3..748d04de1540 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinderKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinderKosmos.kt @@ -23,8 +23,11 @@ import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.Kosmos.Fixture import com.android.systemui.kosmos.testDispatcher import com.android.systemui.statusbar.notification.icon.ui.viewbinder.notificationIconContainerShelfViewBinder +import com.android.systemui.statusbar.notification.stack.ui.view.notificationStatsLogger +import com.android.systemui.statusbar.notification.stack.displaySwitchNotificationsHiderTracker import com.android.systemui.statusbar.notification.stack.ui.viewmodel.notificationListViewModel import com.android.systemui.statusbar.phone.notificationIconAreaController +import java.util.Optional val Kosmos.notificationListViewBinder by Fixture { NotificationListViewBinder( @@ -34,6 +37,8 @@ val Kosmos.notificationListViewBinder by Fixture { falsingManager = falsingManager, iconAreaController = notificationIconAreaController, metricsLogger = metricsLogger, + hiderTracker = displaySwitchNotificationsHiderTracker, nicBinder = notificationIconContainerShelfViewBinder, + loggerOptional = Optional.of(notificationStatsLogger), ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListLoggerViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListLoggerViewModelKosmos.kt new file mode 100644 index 000000000000..08bda463261d --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListLoggerViewModelKosmos.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.statusbar.notification.stack.ui.viewmodel + +import com.android.systemui.keyguard.domain.interactor.keyguardInteractor +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.Kosmos.Fixture +import com.android.systemui.statusbar.notification.domain.interactor.activeNotificationsInteractor + +val Kosmos.notificationListLoggerViewModel by Fixture { + NotificationLoggerViewModel( + keyguardInteractor = keyguardInteractor, + windowRootViewVisibilityInteractor = windowRootViewVisibilityInteractor, + activeNotificationsInteractor = activeNotificationsInteractor, + ) +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelKosmos.kt index f5a4c034d836..998e579b14fc 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelKosmos.kt @@ -33,6 +33,7 @@ val Kosmos.notificationListViewModel by Fixture { shelf = notificationShelfViewModel, hideListViewModel = hideListViewModel, footer = Optional.of(footerViewModel), + logger = Optional.of(notificationListLoggerViewModel), activeNotificationsInteractor = activeNotificationsInteractor, keyguardTransitionInteractor = keyguardTransitionInteractor, seenNotificationsInteractor = seenNotificationsInteractor, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModelKosmos.kt index 0dbade76979c..d7e948eefc95 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModelKosmos.kt @@ -20,11 +20,13 @@ import com.android.systemui.flags.featureFlagsClassic import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.Kosmos.Fixture import com.android.systemui.scene.shared.flag.sceneContainerFlags +import com.android.systemui.shade.domain.interactor.shadeInteractor import com.android.systemui.statusbar.notification.stack.domain.interactor.notificationStackAppearanceInteractor val Kosmos.notificationsPlaceholderViewModel by Fixture { NotificationsPlaceholderViewModel( interactor = notificationStackAppearanceInteractor, + shadeInteractor = shadeInteractor, flags = sceneContainerFlags, featureFlags = featureFlagsClassic, ) 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 e4313bb168b8..d80ee758269f 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 @@ -19,18 +19,20 @@ package com.android.systemui.statusbar.notification.stack.ui.viewmodel import com.android.systemui.keyguard.data.repository.keyguardRepository import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.Kosmos.Fixture -import com.android.systemui.kosmos.testScope +import com.android.systemui.kosmos.applicationCoroutineScope 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.statusbar.notification.domain.interactor.activeNotificationsInteractor import com.android.systemui.statusbar.policy.headsUpManager val Kosmos.windowRootViewVisibilityInteractor by Fixture { WindowRootViewVisibilityInteractor( - scope = testScope, + scope = applicationCoroutineScope, windowRootViewVisibilityRepository = windowRootViewVisibilityRepository, keyguardRepository = keyguardRepository, headsUpManager = headsUpManager, powerInteractor = powerInteractor, + activeNotificationsInteractor = activeNotificationsInteractor, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepositoryKosmos.kt new file mode 100644 index 000000000000..9d62ea5d2b0b --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepositoryKosmos.kt @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.pipeline.mobile.data.repository + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.Kosmos.Fixture +import com.android.systemui.util.mockito.mock + +val Kosmos.fakeMobileConnectionsRepository by Fixture { + FakeMobileConnectionsRepository(tableLogBuffer = mock()) +} + +val Kosmos.mobileConnectionsRepository by + Fixture<MobileConnectionsRepository> { fakeMobileConnectionsRepository } diff --git a/packages/CredentialManager/src/com/android/credentialmanager/ui/theme/Color.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelKosmos.kt index c9238459633c..0b9f897a6204 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/ui/theme/Color.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelKosmos.kt @@ -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. @@ -14,17 +14,16 @@ * limitations under the License. */ -package com.android.credentialmanager.ui.theme +package com.android.systemui.user.ui.viewmodel -import android.annotation.AttrRes -import androidx.compose.runtime.Composable -import androidx.compose.runtime.ReadOnlyComposable -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalContext +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.Kosmos.Fixture +import com.android.systemui.user.domain.interactor.guestUserInteractor +import com.android.systemui.user.domain.interactor.userSwitcherInteractor -/** Read the [Color] from the given [attribute]. */ -@Composable -@ReadOnlyComposable -fun colorAttr(@AttrRes attribute: Int): Color { - return AndroidColorScheme.getColor(LocalContext.current, attribute) +val Kosmos.userSwitcherViewModel by Fixture { + UserSwitcherViewModel( + userSwitcherInteractor = userSwitcherInteractor, + guestUserInteractor = guestUserInteractor, + ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/util/concurrency/FakeExecutorModule.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/util/concurrency/FakeExecutorModule.kt index 1f48d940f91c..11c09ee3d343 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/util/concurrency/FakeExecutorModule.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/util/concurrency/FakeExecutorModule.kt @@ -17,6 +17,7 @@ package com.android.systemui.util.concurrency import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.dagger.qualifiers.UiBackground import com.android.systemui.util.time.FakeSystemClock import dagger.Binds import dagger.Module @@ -27,8 +28,9 @@ import java.util.concurrent.Executor interface FakeExecutorModule { @Binds @Main @SysUISingleton fun bindMainExecutor(executor: FakeExecutor): Executor + @Binds @UiBackground @SysUISingleton fun bindUiBgExecutor(executor: FakeExecutor): Executor + companion object { - @Provides - fun provideFake(clock: FakeSystemClock) = FakeExecutor(clock) + @Provides fun provideFake(clock: FakeSystemClock) = FakeExecutor(clock) } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/util/time/FakeSystemClockKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/util/time/FakeSystemClockKosmos.kt index 914e65427f41..f3a8b14abab8 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/util/time/FakeSystemClockKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/util/time/FakeSystemClockKosmos.kt @@ -17,5 +17,19 @@ package com.android.systemui.util.time import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.testScope +import com.android.systemui.util.mockito.mock +import com.android.systemui.util.mockito.whenever +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.currentTime -var Kosmos.fakeSystemClock by Kosmos.Fixture { FakeSystemClock() } +@OptIn(ExperimentalCoroutinesApi::class) +val Kosmos.systemClock by + Kosmos.Fixture<SystemClock> { + mock { + whenever(elapsedRealtime()).thenAnswer { testScope.currentTime } + whenever(uptimeMillis()).thenAnswer { testScope.currentTime } + } + } + +val Kosmos.fakeSystemClock by Kosmos.Fixture { FakeSystemClock() } diff --git a/packages/SystemUI/tests/utils/src/com/android/telecom/TelecomManagerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/telecom/TelecomManagerKosmos.kt new file mode 100644 index 000000000000..4e0c0883eb02 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/telecom/TelecomManagerKosmos.kt @@ -0,0 +1,24 @@ +/* + * 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.telecom + +import android.telecom.TelecomManager +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.Kosmos.Fixture +import com.android.systemui.util.mockito.mock + +var Kosmos.telecomManager by Fixture<TelecomManager?> { mock() } diff --git a/services/autofill/java/com/android/server/autofill/Session.java b/services/autofill/java/com/android/server/autofill/Session.java index 6a81425c1443..f3b74ea00a58 100644 --- a/services/autofill/java/com/android/server/autofill/Session.java +++ b/services/autofill/java/com/android/server/autofill/Session.java @@ -1735,6 +1735,7 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState processResponseLockedForPcc(response, response.getClientState(), requestFlags); mFillResponseEventLogger.maybeSetLatencyResponseProcessingMillis(); + mFillResponseEventLogger.logAndEndEvent(); } @@ -1847,6 +1848,33 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState return; } synchronized (mLock) { + // TODO(b/319913595): refactor logging for fill response for primary and secondary + // providers + // Start a new FillResponse logger for the success case. + mFillResponseEventLogger.startLogForNewResponse(); + mFillResponseEventLogger.maybeSetRequestId(fillResponse.getRequestId()); + mFillResponseEventLogger.maybeSetAppPackageUid(uid); + mFillResponseEventLogger.maybeSetResponseStatus(RESPONSE_STATUS_SUCCESS); + mFillResponseEventLogger.startResponseProcessingTime(); + // Time passed since session was created + final long fillRequestReceivedRelativeTimestamp = + SystemClock.elapsedRealtime() - mLatencyBaseTime; + mPresentationStatsEventLogger.maybeSetFillResponseReceivedTimestampMs( + (int) (fillRequestReceivedRelativeTimestamp)); + mFillResponseEventLogger.maybeSetLatencyFillResponseReceivedMillis( + (int) (fillRequestReceivedRelativeTimestamp)); + if (mDestroyed) { + Slog.w(TAG, "Call to Session#onSecondaryFillResponse() rejected - session: " + + id + " destroyed"); + mFillResponseEventLogger.maybeSetResponseStatus(RESPONSE_STATUS_SESSION_DESTROYED); + mFillResponseEventLogger.logAndEndEvent(); + return; + } + + List<Dataset> datasetList = fillResponse.getDatasets(); + int datasetCount = (datasetList == null) ? 0 : datasetList.size(); + mFillResponseEventLogger.maybeSetTotalDatasetsProvided(datasetCount); + mFillResponseEventLogger.maybeSetAvailableCount(datasetCount); if (mSecondaryResponses == null) { mSecondaryResponses = new SparseArray<>(2); } @@ -1859,6 +1887,8 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState if (currentView != null) { currentView.maybeCallOnFillReady(flags); } + mFillResponseEventLogger.maybeSetLatencyResponseProcessingMillis(); + mFillResponseEventLogger.logAndEndEvent(); } } @@ -4271,13 +4301,19 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState if (value != null) { viewState.setCurrentValue(value); } - + boolean isCredmanRequested = (flags & FLAG_VIEW_REQUESTS_CREDMAN_SERVICE) != 0; if (shouldRequestSecondaryProvider(flags)) { if (requestNewFillResponseOnViewEnteredIfNecessaryLocked( id, viewState, flags)) { Slog.v(TAG, "Started a new fill request for secondary provider."); return; } + + FillResponse response = viewState.getSecondaryResponse(); + if (response != null) { + logPresentationStatsOnViewEntered(response, isCredmanRequested); + } + // If the ViewState is ready to be displayed, onReady() will be called. viewState.update(value, virtualBounds, flags); @@ -4363,15 +4399,9 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState return; } - if (viewState.getResponse() != null) { - boolean isCredmanRequested = (flags & FLAG_VIEW_REQUESTS_CREDMAN_SERVICE) != 0; - FillResponse response = viewState.getResponse(); - mPresentationStatsEventLogger.maybeSetRequestId(response.getRequestId()); - mPresentationStatsEventLogger.maybeSetIsCredentialRequest(isCredmanRequested); - mPresentationStatsEventLogger.maybeSetFieldClassificationRequestId( - mFieldClassificationIdSnapshot); - mPresentationStatsEventLogger.maybeSetAvailableCount( - response.getDatasets(), mCurrentViewId); + FillResponse response = viewState.getResponse(); + if (response != null) { + logPresentationStatsOnViewEntered(response, isCredmanRequested); } if (isSameViewEntered) { @@ -4412,6 +4442,17 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState } @GuardedBy("mLock") + private void logPresentationStatsOnViewEntered(FillResponse response, + boolean isCredmanRequested) { + mPresentationStatsEventLogger.maybeSetRequestId(response.getRequestId()); + mPresentationStatsEventLogger.maybeSetIsCredentialRequest(isCredmanRequested); + mPresentationStatsEventLogger.maybeSetFieldClassificationRequestId( + mFieldClassificationIdSnapshot); + mPresentationStatsEventLogger.maybeSetAvailableCount( + response.getDatasets(), mCurrentViewId); + } + + @GuardedBy("mLock") private void hideAugmentedAutofillLocked(@NonNull ViewState viewState) { if ((viewState.getState() & ViewState.STATE_TRIGGERED_AUGMENTED_AUTOFILL) != 0) { diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java index 056ec895821d..50e18628852d 100644 --- a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java +++ b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java @@ -21,6 +21,7 @@ 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.USE_COMPANION_TRANSPORTS; import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_VISIBLE; import static android.companion.AssociationRequest.DEVICE_PROFILE_AUTOMOTIVE_PROJECTION; @@ -82,6 +83,7 @@ import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManagerInternal; import android.content.pm.UserInfo; +import android.hardware.power.Mode; import android.net.MacAddress; import android.net.NetworkPolicyManager; import android.os.Binder; @@ -90,6 +92,7 @@ import android.os.Handler; import android.os.Message; import android.os.Parcel; import android.os.ParcelFileDescriptor; +import android.os.PowerManagerInternal; import android.os.PowerWhitelistManager; import android.os.RemoteCallbackList; import android.os.RemoteException; @@ -175,6 +178,7 @@ public class CompanionDeviceManagerService extends SystemService { private final PowerWhitelistManager mPowerWhitelistManager; private final UserManager mUserManager; final PackageManagerInternal mPackageManagerInternal; + private final PowerManagerInternal mPowerManagerInternal; /** * A structure that consists of two nested maps, and effectively maps (userId + packageName) to @@ -235,6 +239,7 @@ public class CompanionDeviceManagerService extends SystemService { mOnPackageVisibilityChangeListener = new OnPackageVisibilityChangeListener(mActivityManager); + mPowerManagerInternal = LocalServices.getService(PowerManagerInternal.class); } @Override @@ -949,6 +954,10 @@ public class CompanionDeviceManagerService extends SystemService { mAssociationStore.updateAssociation(association); mDevicePresenceMonitor.onSelfManagedDeviceConnected(associationId); + + if (association.getDeviceProfile() == REQUEST_COMPANION_PROFILE_AUTOMOTIVE_PROJECTION) { + mPowerManagerInternal.setPowerMode(Mode.AUTOMOTIVE_PROJECTION, true); + } } @Override @@ -963,6 +972,10 @@ public class CompanionDeviceManagerService extends SystemService { } mDevicePresenceMonitor.onSelfManagedDeviceDisconnected(associationId); + + if (association.getDeviceProfile() == REQUEST_COMPANION_PROFILE_AUTOMOTIVE_PROJECTION) { + mPowerManagerInternal.setPowerMode(Mode.AUTOMOTIVE_PROJECTION, false); + } } @Override diff --git a/services/companion/java/com/android/server/companion/transport/SecureTransport.java b/services/companion/java/com/android/server/companion/transport/SecureTransport.java index a0301a920d96..6e906ebe887a 100644 --- a/services/companion/java/com/android/server/companion/transport/SecureTransport.java +++ b/services/companion/java/com/android/server/companion/transport/SecureTransport.java @@ -34,7 +34,7 @@ class SecureTransport extends Transport implements SecureChannel.Callback { private volatile boolean mShouldProcessRequests = false; - private final BlockingQueue<byte[]> mRequestQueue = new ArrayBlockingQueue<>(100); + private final BlockingQueue<byte[]> mRequestQueue = new ArrayBlockingQueue<>(500); SecureTransport(int associationId, ParcelFileDescriptor fd, Context context) { super(associationId, fd, context); diff --git a/services/companion/java/com/android/server/companion/virtual/InputController.java b/services/companion/java/com/android/server/companion/virtual/InputController.java index d1274d49a14d..3b9d92dc3d02 100644 --- a/services/companion/java/com/android/server/companion/virtual/InputController.java +++ b/services/companion/java/com/android/server/companion/virtual/InputController.java @@ -30,6 +30,8 @@ import android.hardware.input.VirtualKeyEvent; import android.hardware.input.VirtualMouseButtonEvent; import android.hardware.input.VirtualMouseRelativeEvent; import android.hardware.input.VirtualMouseScrollEvent; +import android.hardware.input.VirtualStylusButtonEvent; +import android.hardware.input.VirtualStylusMotionEvent; import android.hardware.input.VirtualTouchEvent; import android.os.Handler; import android.os.IBinder; @@ -71,12 +73,14 @@ class InputController { static final String PHYS_TYPE_MOUSE = "Mouse"; static final String PHYS_TYPE_TOUCHSCREEN = "Touchscreen"; static final String PHYS_TYPE_NAVIGATION_TOUCHPAD = "NavigationTouchpad"; + static final String PHYS_TYPE_STYLUS = "Stylus"; @StringDef(prefix = { "PHYS_TYPE_" }, value = { PHYS_TYPE_DPAD, PHYS_TYPE_KEYBOARD, PHYS_TYPE_MOUSE, PHYS_TYPE_TOUCHSCREEN, PHYS_TYPE_NAVIGATION_TOUCHPAD, + PHYS_TYPE_STYLUS, }) @Retention(RetentionPolicy.SOURCE) @interface PhysType { @@ -188,6 +192,16 @@ class InputController { } } + void createStylus(@NonNull String deviceName, int vendorId, int productId, + @NonNull IBinder deviceToken, int displayId, int height, int width) + throws DeviceCreationException { + final String phys = createPhys(PHYS_TYPE_STYLUS); + createDeviceInternal(InputDeviceDescriptor.TYPE_STYLUS, deviceName, vendorId, + productId, deviceToken, displayId, phys, + () -> mNativeWrapper.openUinputStylus(deviceName, vendorId, productId, phys, + height, width)); + } + void unregisterInputDevice(@NonNull IBinder token) { synchronized (mLock) { final InputDeviceDescriptor inputDeviceDescriptor = mInputDeviceDescriptors.remove( @@ -410,6 +424,32 @@ class InputController { } } + boolean sendStylusMotionEvent(@NonNull IBinder token, @NonNull VirtualStylusMotionEvent event) { + synchronized (mLock) { + final InputDeviceDescriptor inputDeviceDescriptor = mInputDeviceDescriptors.get( + token); + if (inputDeviceDescriptor == null) { + return false; + } + return mNativeWrapper.writeStylusMotionEvent(inputDeviceDescriptor.getNativePointer(), + event.getToolType(), event.getAction(), event.getX(), event.getY(), + event.getPressure(), event.getTiltX(), event.getTiltY(), + event.getEventTimeNanos()); + } + } + + boolean sendStylusButtonEvent(@NonNull IBinder token, @NonNull VirtualStylusButtonEvent event) { + synchronized (mLock) { + final InputDeviceDescriptor inputDeviceDescriptor = mInputDeviceDescriptors.get( + token); + if (inputDeviceDescriptor == null) { + return false; + } + return mNativeWrapper.writeStylusButtonEvent(inputDeviceDescriptor.getNativePointer(), + event.getButtonCode(), event.getAction(), event.getEventTimeNanos()); + } + } + public void dump(@NonNull PrintWriter fout) { fout.println(" InputController: "); synchronized (mLock) { @@ -437,7 +477,7 @@ class InputController { } } - @VisibleForTesting + @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) Map<IBinder, InputDeviceDescriptor> getInputDeviceDescriptors() { final Map<IBinder, InputDeviceDescriptor> inputDeviceDescriptors = new ArrayMap<>(); synchronized (mLock) { @@ -454,6 +494,8 @@ class InputController { String phys); private static native long nativeOpenUinputTouchscreen(String deviceName, int vendorId, int productId, String phys, int height, int width); + private static native long nativeOpenUinputStylus(String deviceName, int vendorId, + int productId, String phys, int height, int width); private static native void nativeCloseUinput(long ptr); private static native boolean nativeWriteDpadKeyEvent(long ptr, int androidKeyCode, int action, long eventTimeNanos); @@ -468,6 +510,10 @@ class InputController { float relativeY, long eventTimeNanos); private static native boolean nativeWriteScrollEvent(long ptr, float xAxisMovement, float yAxisMovement, long eventTimeNanos); + private static native boolean nativeWriteStylusMotionEvent(long ptr, int toolType, int action, + int locationX, int locationY, int pressure, int tiltX, int tiltY, long eventTimeNanos); + private static native boolean nativeWriteStylusButtonEvent(long ptr, int buttonCode, int action, + long eventTimeNanos); /** Wrapper around the static native methods for tests. */ @VisibleForTesting @@ -491,6 +537,11 @@ class InputController { width); } + public long openUinputStylus(String deviceName, int vendorId, int productId, String phys, + int height, int width) { + return nativeOpenUinputStylus(deviceName, vendorId, productId, phys, height, width); + } + public void closeUinput(long ptr) { nativeCloseUinput(ptr); } @@ -527,21 +578,35 @@ class InputController { long eventTimeNanos) { return nativeWriteScrollEvent(ptr, xAxisMovement, yAxisMovement, eventTimeNanos); } + + public boolean writeStylusMotionEvent(long ptr, int toolType, int action, int locationX, + int locationY, int pressure, int tiltX, int tiltY, long eventTimeNanos) { + return nativeWriteStylusMotionEvent(ptr, toolType, action, locationX, locationY, + pressure, tiltX, tiltY, eventTimeNanos); + } + + public boolean writeStylusButtonEvent(long ptr, int buttonCode, int action, + long eventTimeNanos) { + return nativeWriteStylusButtonEvent(ptr, buttonCode, action, eventTimeNanos); + } } - @VisibleForTesting static final class InputDeviceDescriptor { + @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) + static final class InputDeviceDescriptor { static final int TYPE_KEYBOARD = 1; static final int TYPE_MOUSE = 2; static final int TYPE_TOUCHSCREEN = 3; static final int TYPE_DPAD = 4; static final int TYPE_NAVIGATION_TOUCHPAD = 5; + static final int TYPE_STYLUS = 6; @IntDef(prefix = { "TYPE_" }, value = { TYPE_KEYBOARD, TYPE_MOUSE, TYPE_TOUCHSCREEN, TYPE_DPAD, TYPE_NAVIGATION_TOUCHPAD, + TYPE_STYLUS, }) @Retention(RetentionPolicy.SOURCE) @interface Type { diff --git a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java index 44c3a8d7537f..f13f49a5d378 100644 --- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java +++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java @@ -23,6 +23,7 @@ import static android.companion.virtual.VirtualDeviceParams.ACTIVITY_POLICY_DEFA import static android.companion.virtual.VirtualDeviceParams.DEVICE_POLICY_DEFAULT; import static android.companion.virtual.VirtualDeviceParams.NAVIGATION_POLICY_DEFAULT_ALLOWED; import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_ACTIVITY; +import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_CAMERA; import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_CLIPBOARD; import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_RECENTS; import static android.content.pm.PackageManager.ACTION_REQUEST_PERMISSIONS; @@ -75,6 +76,9 @@ import android.hardware.input.VirtualMouseConfig; import android.hardware.input.VirtualMouseRelativeEvent; import android.hardware.input.VirtualMouseScrollEvent; import android.hardware.input.VirtualNavigationTouchpadConfig; +import android.hardware.input.VirtualStylusButtonEvent; +import android.hardware.input.VirtualStylusConfig; +import android.hardware.input.VirtualStylusMotionEvent; import android.hardware.input.VirtualTouchEvent; import android.hardware.input.VirtualTouchscreenConfig; import android.os.Binder; @@ -258,7 +262,9 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub runningAppsChangedCallback, params, DisplayManagerGlobal.getInstance(), - Flags.virtualCamera() ? new VirtualCameraController() : null); + Flags.virtualCamera() + ? new VirtualCameraController(params.getDevicePolicy(POLICY_TYPE_CAMERA)) + : null); } @VisibleForTesting @@ -776,6 +782,26 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub @Override // Binder call @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) + public void createVirtualStylus(@NonNull VirtualStylusConfig config, + @NonNull IBinder deviceToken) { + super.createVirtualStylus_enforcePermission(); + Objects.requireNonNull(config); + Objects.requireNonNull(deviceToken); + checkVirtualInputDeviceDisplayIdAssociation(config.getAssociatedDisplayId()); + final long ident = Binder.clearCallingIdentity(); + try { + mInputController.createStylus(config.getInputDeviceName(), config.getVendorId(), + config.getProductId(), deviceToken, config.getAssociatedDisplayId(), + config.getHeight(), config.getWidth()); + } catch (InputController.DeviceCreationException e) { + throw new IllegalArgumentException(e); + } finally { + Binder.restoreCallingIdentity(ident); + } + } + + @Override // Binder call + @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void unregisterInputDevice(IBinder token) { super.unregisterInputDevice_enforcePermission(); final long ident = Binder.clearCallingIdentity(); @@ -881,6 +907,36 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub @Override // Binder call @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) + public boolean sendStylusMotionEvent(@NonNull IBinder token, + @NonNull VirtualStylusMotionEvent event) { + super.sendStylusMotionEvent_enforcePermission(); + Objects.requireNonNull(token); + Objects.requireNonNull(event); + final long ident = Binder.clearCallingIdentity(); + try { + return mInputController.sendStylusMotionEvent(token, event); + } finally { + Binder.restoreCallingIdentity(ident); + } + } + + @Override // Binder call + @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) + public boolean sendStylusButtonEvent(@NonNull IBinder token, + @NonNull VirtualStylusButtonEvent event) { + super.sendStylusButtonEvent_enforcePermission(); + Objects.requireNonNull(token); + Objects.requireNonNull(event); + final long ident = Binder.clearCallingIdentity(); + try { + return mInputController.sendStylusButtonEvent(token, event); + } finally { + Binder.restoreCallingIdentity(ident); + } + } + + @Override // Binder call + @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void setShowPointerIcon(boolean showPointerIcon) { super.setShowPointerIcon_enforcePermission(); final long ident = Binder.clearCallingIdentity(); @@ -1337,6 +1393,11 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub } } + boolean isInputDeviceOwnedByVirtualDevice(int inputDeviceId) { + return mInputController.getInputDeviceDescriptors().values().stream().anyMatch( + inputDeviceDescriptor -> inputDeviceDescriptor.getInputDeviceId() == inputDeviceId); + } + void onEnteringPipBlocked(int uid) { // Do nothing. ActivityRecord#checkEnterPictureInPictureState logs that the display does not // support PiP. 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 0d5cdcbe484c..ef61498e16af 100644 --- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java +++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java @@ -838,10 +838,11 @@ public class VirtualDeviceManagerService extends SystemService { } @Override - public boolean isDisplayOwnedByAnyVirtualDevice(int displayId) { + public boolean isInputDeviceOwnedByVirtualDevice(int inputDeviceId) { ArrayList<VirtualDeviceImpl> virtualDevicesSnapshot = getVirtualDevicesSnapshot(); for (int i = 0; i < virtualDevicesSnapshot.size(); i++) { - if (virtualDevicesSnapshot.get(i).isDisplayOwnedByVirtualDevice(displayId)) { + if (virtualDevicesSnapshot.get(i) + .isInputDeviceOwnedByVirtualDevice(inputDeviceId)) { return true; } } diff --git a/services/companion/java/com/android/server/companion/virtual/camera/VirtualCameraController.java b/services/companion/java/com/android/server/companion/virtual/camera/VirtualCameraController.java index 2f9b6a56e316..2d82b5e7dd66 100644 --- a/services/companion/java/com/android/server/companion/virtual/camera/VirtualCameraController.java +++ b/services/companion/java/com/android/server/companion/virtual/camera/VirtualCameraController.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 The Android Open Source Project + * Copyright 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. @@ -16,10 +16,13 @@ package com.android.server.companion.virtual.camera; +import static android.companion.virtual.VirtualDeviceParams.DEVICE_POLICY_DEFAULT; + import static com.android.server.companion.virtual.camera.VirtualCameraConversionUtil.getServiceCameraConfiguration; import android.annotation.NonNull; import android.annotation.Nullable; +import android.companion.virtual.VirtualDeviceParams.DevicePolicy; import android.companion.virtual.camera.VirtualCameraConfig; import android.companion.virtualcamera.IVirtualCameraService; import android.companion.virtualcamera.VirtualCameraConfiguration; @@ -51,15 +54,21 @@ public final class VirtualCameraController implements IBinder.DeathRecipient { @GuardedBy("mServiceLock") @Nullable private IVirtualCameraService mVirtualCameraService; + @DevicePolicy + private final int mCameraPolicy; @GuardedBy("mCameras") private final Map<IBinder, CameraDescriptor> mCameras = new ArrayMap<>(); - public VirtualCameraController() {} + public VirtualCameraController(@DevicePolicy int cameraPolicy) { + this(/* virtualCameraService= */ null, cameraPolicy); + } @VisibleForTesting - VirtualCameraController(IVirtualCameraService virtualCameraService) { + VirtualCameraController(IVirtualCameraService virtualCameraService, + @DevicePolicy int cameraPolicy) { mVirtualCameraService = virtualCameraService; + mCameraPolicy = cameraPolicy; } /** @@ -68,6 +77,8 @@ public final class VirtualCameraController implements IBinder.DeathRecipient { * @param cameraConfig The {@link VirtualCameraConfig} sent by the client. */ public void registerCamera(@NonNull VirtualCameraConfig cameraConfig) { + checkConfigByPolicy(cameraConfig); + connectVirtualCameraServiceIfNeeded(); try { @@ -173,6 +184,29 @@ public final class VirtualCameraController implements IBinder.DeathRecipient { } } + private void checkConfigByPolicy(VirtualCameraConfig config) { + if (mCameraPolicy == DEVICE_POLICY_DEFAULT) { + throw new IllegalArgumentException( + "Cannot create virtual camera with DEVICE_POLICY_DEFAULT for " + + "POLICY_TYPE_CAMERA"); + } else if (isLensFacingAlreadyPresent(config.getLensFacing())) { + throw new IllegalArgumentException( + "Only a single virtual camera can be created with lens facing " + + config.getLensFacing()); + } + } + + private boolean isLensFacingAlreadyPresent(int lensFacing) { + synchronized (mCameras) { + for (CameraDescriptor cameraDescriptor : mCameras.values()) { + if (cameraDescriptor.mConfig.getLensFacing() == lensFacing) { + return true; + } + } + } + return false; + } + private void connectVirtualCameraServiceIfNeeded() { synchronized (mServiceLock) { // Try to connect to service if not connected already. diff --git a/services/companion/java/com/android/server/companion/virtual/camera/VirtualCameraConversionUtil.java b/services/companion/java/com/android/server/companion/virtual/camera/VirtualCameraConversionUtil.java index f24c4cc59336..c4a84b04c4b4 100644 --- a/services/companion/java/com/android/server/companion/virtual/camera/VirtualCameraConversionUtil.java +++ b/services/companion/java/com/android/server/companion/virtual/camera/VirtualCameraConversionUtil.java @@ -25,6 +25,7 @@ import android.companion.virtualcamera.IVirtualCameraService; import android.companion.virtualcamera.SupportedStreamConfiguration; import android.companion.virtualcamera.VirtualCameraConfiguration; import android.graphics.ImageFormat; +import android.graphics.PixelFormat; import android.os.RemoteException; import android.view.Surface; @@ -45,12 +46,12 @@ public final class VirtualCameraConversionUtil { getServiceCameraConfiguration(@NonNull VirtualCameraConfig cameraConfig) throws RemoteException { VirtualCameraConfiguration serviceConfiguration = new VirtualCameraConfiguration(); - serviceConfiguration.supportedStreamConfigs = cameraConfig.getStreamConfigs().stream() .map(VirtualCameraConversionUtil::convertSupportedStreamConfiguration) .toArray(SupportedStreamConfiguration[]::new); - + serviceConfiguration.sensorOrientation = cameraConfig.getSensorOrientation(); + serviceConfiguration.lensFacing = cameraConfig.getLensFacing(); serviceConfiguration.virtualCameraCallback = convertCallback(cameraConfig.getCallback()); return serviceConfiguration; } @@ -60,12 +61,10 @@ public final class VirtualCameraConversionUtil { @NonNull IVirtualCameraCallback camera) { return new android.companion.virtualcamera.IVirtualCameraCallback.Stub() { @Override - public void onStreamConfigured( - int streamId, Surface surface, int width, int height, int pixelFormat) - throws RemoteException { - VirtualCameraStreamConfig streamConfig = - createStreamConfig(width, height, pixelFormat); - camera.onStreamConfigured(streamId, surface, streamConfig); + public void onStreamConfigured(int streamId, Surface surface, int width, int height, + int format) throws RemoteException { + camera.onStreamConfigured(streamId, surface, width, height, + convertToJavaFormat(format)); } @Override @@ -81,23 +80,30 @@ public final class VirtualCameraConversionUtil { } @NonNull - private static VirtualCameraStreamConfig createStreamConfig( - int width, int height, int pixelFormat) { - return new VirtualCameraStreamConfig(width, height, pixelFormat); - } - - @NonNull private static SupportedStreamConfiguration convertSupportedStreamConfiguration( VirtualCameraStreamConfig stream) { SupportedStreamConfiguration supportedConfig = new SupportedStreamConfiguration(); supportedConfig.height = stream.getHeight(); supportedConfig.width = stream.getWidth(); - supportedConfig.pixelFormat = convertFormat(stream.getFormat()); + supportedConfig.pixelFormat = convertToHalFormat(stream.getFormat()); + supportedConfig.maxFps = stream.getMaximumFramesPerSecond(); return supportedConfig; } - private static int convertFormat(int format) { - return format == ImageFormat.YUV_420_888 ? Format.YUV_420_888 : Format.UNKNOWN; + private static int convertToHalFormat(int javaFormat) { + return switch (javaFormat) { + case ImageFormat.YUV_420_888 -> Format.YUV_420_888; + case PixelFormat.RGBA_8888 -> Format.RGBA_8888; + default -> Format.UNKNOWN; + }; + } + + private static int convertToJavaFormat(int halFormat) { + return switch (halFormat) { + case Format.YUV_420_888 -> ImageFormat.YUV_420_888; + case Format.RGBA_8888 -> PixelFormat.RGBA_8888; + default -> ImageFormat.UNKNOWN; + }; } private VirtualCameraConversionUtil() { diff --git a/services/core/java/com/android/server/BatteryService.java b/services/core/java/com/android/server/BatteryService.java index 5a44ac803cb4..9d9e7c9345be 100644 --- a/services/core/java/com/android/server/BatteryService.java +++ b/services/core/java/com/android/server/BatteryService.java @@ -1387,6 +1387,8 @@ public final class BatteryService extends SystemService { case BatteryManager.BATTERY_PROPERTY_MANUFACTURING_DATE: case BatteryManager.BATTERY_PROPERTY_FIRST_USAGE_DATE: case BatteryManager.BATTERY_PROPERTY_CHARGING_POLICY: + case BatteryManager.BATTERY_PROPERTY_SERIAL_NUMBER: + case BatteryManager.BATTERY_PROPERTY_PART_STATUS: mContext.enforceCallingPermission( android.Manifest.permission.BATTERY_STATS, null); break; diff --git a/services/core/java/com/android/server/BootReceiver.java b/services/core/java/com/android/server/BootReceiver.java index 329aac6f3a6a..9f279b1ba3fe 100644 --- a/services/core/java/com/android/server/BootReceiver.java +++ b/services/core/java/com/android/server/BootReceiver.java @@ -48,8 +48,6 @@ import com.android.internal.util.XmlUtils; import com.android.modules.utils.TypedXmlPullParser; import com.android.modules.utils.TypedXmlSerializer; import com.android.server.am.DropboxRateLimiter; -import com.android.server.os.TombstoneProtos; -import com.android.server.os.TombstoneProtos.Tombstone; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; @@ -62,14 +60,11 @@ import java.io.FileOutputStream; import java.io.IOException; import java.nio.file.Files; import java.nio.file.attribute.PosixFilePermissions; -import java.util.AbstractMap; import java.util.HashMap; import java.util.Iterator; -import java.util.Map; import java.util.concurrent.locks.ReentrantLock; import java.util.regex.Matcher; import java.util.regex.Pattern; -import java.util.stream.Collectors; /** * Performs a number of miscellaneous, non-system-critical actions @@ -337,12 +332,12 @@ public class BootReceiver extends BroadcastReceiver { * * @param ctx Context * @param tombstone path to the tombstone - * @param tombstoneProto the parsed proto tombstone + * @param proto whether the tombstone is stored as proto * @param processName the name of the process corresponding to the tombstone * @param tmpFileLock the lock for reading/writing tmp files */ public static void addTombstoneToDropBox( - Context ctx, File tombstone, Tombstone tombstoneProto, String processName, + Context ctx, File tombstone, boolean proto, String processName, ReentrantLock tmpFileLock) { final DropBoxManager db = ctx.getSystemService(DropBoxManager.class); if (db == null) { @@ -352,33 +347,31 @@ public class BootReceiver extends BroadcastReceiver { // Check if we should rate limit and abort early if needed. DropboxRateLimiter.RateLimitResult rateLimitResult = - sDropboxRateLimiter.shouldRateLimit(TAG_TOMBSTONE_PROTO_WITH_HEADERS, processName); + sDropboxRateLimiter.shouldRateLimit( + proto ? TAG_TOMBSTONE_PROTO_WITH_HEADERS : TAG_TOMBSTONE, processName); if (rateLimitResult.shouldRateLimit()) return; HashMap<String, Long> timestamps = readTimestamps(); try { - // Remove the memory data from the proto. - Tombstone tombstoneProtoWithoutMemory = removeMemoryFromTombstone(tombstoneProto); - - final byte[] tombstoneBytes = tombstoneProtoWithoutMemory.toByteArray(); - - // Use JNI to call the c++ proto to text converter and add the headers to the tombstone. - String tombstoneWithoutMemory = new StringBuilder(getBootHeadersToLogAndUpdate()) - .append(rateLimitResult.createHeader()) - .append(getTombstoneText(tombstoneBytes)) - .toString(); - - // Add the tombstone without memory data to dropbox. - db.addText(TAG_TOMBSTONE, tombstoneWithoutMemory); - - // Add the tombstone proto to dropbox. - if (recordFileTimestamp(tombstone, timestamps)) { - tmpFileLock.lock(); - try { - addAugmentedProtoToDropbox(tombstone, tombstoneBytes, db, rateLimitResult); - } finally { - tmpFileLock.unlock(); + if (proto) { + if (recordFileTimestamp(tombstone, timestamps)) { + // We need to attach the count indicating the number of dropped dropbox entries + // due to rate limiting. Do this by enclosing the proto tombsstone in a + // container proto that has the dropped entry count and the proto tombstone as + // bytes (to avoid the complexity of reading and writing nested protos). + tmpFileLock.lock(); + try { + addAugmentedProtoToDropbox(tombstone, db, rateLimitResult); + } finally { + tmpFileLock.unlock(); + } } + } else { + // Add the header indicating how many events have been dropped due to rate limiting. + final String headers = getBootHeadersToLogAndUpdate() + + rateLimitResult.createHeader(); + addFileToDropBox(db, timestamps, headers, tombstone.getPath(), LOG_SIZE, + TAG_TOMBSTONE); } } catch (IOException e) { Slog.e(TAG, "Can't log tombstone", e); @@ -387,8 +380,11 @@ public class BootReceiver extends BroadcastReceiver { } private static void addAugmentedProtoToDropbox( - File tombstone, byte[] tombstoneBytes, DropBoxManager db, + File tombstone, DropBoxManager db, DropboxRateLimiter.RateLimitResult rateLimitResult) throws IOException { + // Read the proto tombstone file as bytes. + final byte[] tombstoneBytes = Files.readAllBytes(tombstone.toPath()); + final File tombstoneProtoWithHeaders = File.createTempFile( tombstone.getName(), ".tmp", TOMBSTONE_TMP_DIR); Files.setPosixFilePermissions( @@ -421,8 +417,6 @@ public class BootReceiver extends BroadcastReceiver { } } - private static native String getTombstoneText(byte[] tombstoneBytes); - private static void addLastkToDropBox( DropBoxManager db, HashMap<String, Long> timestamps, String headers, String footers, String filename, int maxSize, @@ -440,31 +434,6 @@ public class BootReceiver extends BroadcastReceiver { addFileWithFootersToDropBox(db, timestamps, headers, footers, filename, maxSize, tag); } - /** Removes memory information from the Tombstone proto. */ - @VisibleForTesting - public static Tombstone removeMemoryFromTombstone(Tombstone tombstoneProto) { - Tombstone.Builder tombstoneBuilder = tombstoneProto.toBuilder() - .clearMemoryMappings() - .clearThreads() - .putAllThreads(tombstoneProto.getThreadsMap().entrySet() - .stream() - .map(BootReceiver::clearMemoryDump) - .collect(Collectors.toMap(e->e.getKey(), e->e.getValue()))); - - if (tombstoneProto.hasSignalInfo()) { - tombstoneBuilder.setSignalInfo( - tombstoneProto.getSignalInfo().toBuilder().clearFaultAdjacentMetadata()); - } - - return tombstoneBuilder.build(); - } - - private static AbstractMap.SimpleEntry<Integer, TombstoneProtos.Thread> clearMemoryDump( - Map.Entry<Integer, TombstoneProtos.Thread> e) { - return new AbstractMap.SimpleEntry<Integer, TombstoneProtos.Thread>( - e.getKey(), e.getValue().toBuilder().clearMemoryDump().build()); - } - private static void addFileToDropBox( DropBoxManager db, HashMap<String, Long> timestamps, String headers, String filename, int maxSize, String tag) throws IOException { diff --git a/services/core/java/com/android/server/SensitiveContentProtectionManagerService.java b/services/core/java/com/android/server/SensitiveContentProtectionManagerService.java index 70bd4b328b43..c3916422159e 100644 --- a/services/core/java/com/android/server/SensitiveContentProtectionManagerService.java +++ b/services/core/java/com/android/server/SensitiveContentProtectionManagerService.java @@ -34,11 +34,11 @@ import android.service.notification.StatusBarNotification; import android.util.ArraySet; import android.util.Log; +import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.server.wm.SensitiveContentPackages.PackageInfo; import com.android.server.wm.WindowManagerInternal; -import java.util.Collections; import java.util.Set; /** @@ -54,6 +54,10 @@ public final class SensitiveContentProtectionManagerService extends SystemServic private @Nullable MediaProjectionManager mProjectionManager; private @Nullable WindowManagerInternal mWindowManager; + final Object mSensitiveContentProtectionLock = new Object(); + @GuardedBy("mSensitiveContentProtectionLock") + private boolean mProjectionActive = false; + private final MediaProjectionManager.Callback mProjectionCallback = new MediaProjectionManager.Callback() { @Override @@ -132,14 +136,23 @@ public final class SensitiveContentProtectionManagerService extends SystemServic } private void onProjectionStart() { - StatusBarNotification[] notifications; - try { - notifications = mNotificationListener.getActiveNotifications(); - } catch (SecurityException e) { - Log.e(TAG, "SensitiveContentProtectionManagerService doesn't have access.", e); - notifications = new StatusBarNotification[0]; + synchronized (mSensitiveContentProtectionLock) { + mProjectionActive = true; + updateAppsThatShouldBlockScreenCapture(); } + } + + private void onProjectionEnd() { + synchronized (mSensitiveContentProtectionLock) { + mProjectionActive = false; + + // notify windowmanager to clear any sensitive notifications observed during projection + // session + mWindowManager.clearBlockedApps(); + } + } + private void updateAppsThatShouldBlockScreenCapture() { RankingMap rankingMap; try { rankingMap = mNotificationListener.getCurrentRanking(); @@ -148,41 +161,98 @@ public final class SensitiveContentProtectionManagerService extends SystemServic rankingMap = null; } - // notify windowmanager of any currently posted sensitive content notifications - Set<PackageInfo> packageInfos = getSensitivePackagesFromNotifications( - notifications, - rankingMap); - - mWindowManager.setShouldBlockScreenCaptureForApp(packageInfos); + updateAppsThatShouldBlockScreenCapture(rankingMap); } - private void onProjectionEnd() { - // notify windowmanager to clear any sensitive notifications observed during projection - // session - mWindowManager.setShouldBlockScreenCaptureForApp(Collections.emptySet()); + private void updateAppsThatShouldBlockScreenCapture(RankingMap rankingMap) { + StatusBarNotification[] notifications; + try { + notifications = mNotificationListener.getActiveNotifications(); + } catch (SecurityException e) { + Log.e(TAG, "SensitiveContentProtectionManagerService doesn't have access.", e); + notifications = new StatusBarNotification[0]; + } + + // notify windowmanager of any currently posted sensitive content notifications + ArraySet<PackageInfo> packageInfos = getSensitivePackagesFromNotifications( + notifications, rankingMap); + + mWindowManager.addBlockScreenCaptureForApps(packageInfos); } - private Set<PackageInfo> getSensitivePackagesFromNotifications( - StatusBarNotification[] notifications, RankingMap rankingMap) { + private ArraySet<PackageInfo> getSensitivePackagesFromNotifications( + @NonNull StatusBarNotification[] notifications, RankingMap rankingMap) { + ArraySet<PackageInfo> sensitivePackages = new ArraySet<>(); if (rankingMap == null) { Log.w(TAG, "Ranking map not initialized."); - return Collections.emptySet(); + return sensitivePackages; } - Set<PackageInfo> sensitivePackages = new ArraySet<>(); for (StatusBarNotification sbn : notifications) { - NotificationListenerService.Ranking ranking = - rankingMap.getRawRankingObject(sbn.getKey()); - if (ranking != null && ranking.hasSensitiveContent()) { - PackageInfo info = new PackageInfo(sbn.getPackageName(), sbn.getUid()); + PackageInfo info = getSensitivePackageFromNotification(sbn, rankingMap); + if (info != null) { sensitivePackages.add(info); } } return sensitivePackages; } - // TODO(b/317251408): add trigger that updates on onNotificationPosted, - // onNotificationRankingUpdate and onListenerConnected + private PackageInfo getSensitivePackageFromNotification(StatusBarNotification sbn, + RankingMap rankingMap) { + if (sbn == null) { + Log.w(TAG, "Unable to protect null notification"); + return null; + } + if (rankingMap == null) { + Log.w(TAG, "Ranking map not initialized."); + return null; + } + + NotificationListenerService.Ranking ranking = rankingMap.getRawRankingObject(sbn.getKey()); + if (ranking != null && ranking.hasSensitiveContent()) { + return new PackageInfo(sbn.getPackageName(), sbn.getUid()); + } + return null; + } + @VisibleForTesting - static class NotificationListener extends NotificationListenerService {} + class NotificationListener extends NotificationListenerService { + @Override + public void onListenerConnected() { + super.onListenerConnected(); + // Projection started before notification listener was connected + synchronized (mSensitiveContentProtectionLock) { + if (mProjectionActive) { + updateAppsThatShouldBlockScreenCapture(); + } + } + } + + @Override + public void onNotificationPosted(StatusBarNotification sbn, RankingMap rankingMap) { + super.onNotificationPosted(sbn, rankingMap); + synchronized (mSensitiveContentProtectionLock) { + if (!mProjectionActive) { + return; + } + + // notify windowmanager of any currently posted sensitive content notifications + PackageInfo packageInfo = getSensitivePackageFromNotification(sbn, rankingMap); + + if (packageInfo != null) { + mWindowManager.addBlockScreenCaptureForApps(new ArraySet(Set.of(packageInfo))); + } + } + } + + @Override + public void onNotificationRankingUpdate(RankingMap rankingMap) { + super.onNotificationRankingUpdate(rankingMap); + synchronized (mSensitiveContentProtectionLock) { + if (mProjectionActive) { + updateAppsThatShouldBlockScreenCapture(rankingMap); + } + } + } + } } diff --git a/services/core/java/com/android/server/StorageManagerService.java b/services/core/java/com/android/server/StorageManagerService.java index 7a4ac6ac4500..ea1b0f5f66f7 100644 --- a/services/core/java/com/android/server/StorageManagerService.java +++ b/services/core/java/com/android/server/StorageManagerService.java @@ -1076,6 +1076,7 @@ class StorageManagerService extends IStorageManager.Stub final UserManager userManager = mContext.getSystemService(UserManager.class); final List<UserInfo> users = userManager.getUsers(); + extendWatchdogTimeout("#onReset might be slow"); mStorageSessionController.onReset(mVold, () -> { mHandler.removeCallbacksAndMessages(null); }); @@ -5040,9 +5041,9 @@ class StorageManagerService extends IStorageManager.Stub @Override public IFsveritySetupAuthToken createFsveritySetupAuthToken(ParcelFileDescriptor authFd, - int appUid, @UserIdInt int userId) throws IOException { + int uid) throws IOException { try { - return mInstaller.createFsveritySetupAuthToken(authFd, appUid, userId); + return mInstaller.createFsveritySetupAuthToken(authFd, uid); } catch (Installer.InstallerException e) { throw new IOException(e); } diff --git a/services/core/java/com/android/server/TelephonyRegistry.java b/services/core/java/com/android/server/TelephonyRegistry.java index eb6fdd72f2c3..bd67cf42014a 100644 --- a/services/core/java/com/android/server/TelephonyRegistry.java +++ b/services/core/java/com/android/server/TelephonyRegistry.java @@ -418,6 +418,8 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { LinkCapacityEstimate.INVALID, LinkCapacityEstimate.INVALID))); private List<List<LinkCapacityEstimate>> mLinkCapacityEstimateLists; + private int[] mSimultaneousCellularCallingSubIds = {}; + private int[] mECBMReason; private boolean[] mECBMStarted; private int[] mSCBMReason; @@ -564,7 +566,9 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { || events.contains(TelephonyCallback.EVENT_VOICE_ACTIVATION_STATE_CHANGED) || events.contains(TelephonyCallback.EVENT_RADIO_POWER_STATE_CHANGED) || events.contains(TelephonyCallback.EVENT_ALLOWED_NETWORK_TYPE_LIST_CHANGED) - || events.contains(TelephonyCallback.EVENT_EMERGENCY_CALLBACK_MODE_CHANGED); + || events.contains(TelephonyCallback.EVENT_EMERGENCY_CALLBACK_MODE_CHANGED) + || events.contains(TelephonyCallback + .EVENT_SIMULTANEOUS_CELLULAR_CALLING_SUBSCRIPTIONS_CHANGED); } private static final int MSG_USER_SWITCHED = 1; @@ -1122,6 +1126,21 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { return; } + int phoneId = -1; + int subscriptionId = SubscriptionManager.DEFAULT_SUBSCRIPTION_ID; + if(Flags.preventSystemServerAndPhoneDeadlock()) { + // Legacy applications pass SubscriptionManager.DEFAULT_SUB_ID, + // force all illegal subId to SubscriptionManager.DEFAULT_SUB_ID + if (!SubscriptionManager.isValidSubscriptionId(subId)) { + if (DBG) { + log("invalid subscription id, use default id"); + } + } else { //APP specify subID + subscriptionId = subId; + } + phoneId = getPhoneIdFromSubId(subscriptionId); + } + synchronized (mRecords) { // register IBinder b = callback.asBinder(); @@ -1141,17 +1160,23 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { r.renounceFineLocationAccess = renounceFineLocationAccess; r.callerUid = Binder.getCallingUid(); r.callerPid = Binder.getCallingPid(); - // Legacy applications pass SubscriptionManager.DEFAULT_SUB_ID, - // force all illegal subId to SubscriptionManager.DEFAULT_SUB_ID - if (!SubscriptionManager.isValidSubscriptionId(subId)) { - if (DBG) { - log("invalid subscription id, use default id"); + + if(!Flags.preventSystemServerAndPhoneDeadlock()) { + // Legacy applications pass SubscriptionManager.DEFAULT_SUB_ID, + // force all illegal subId to SubscriptionManager.DEFAULT_SUB_ID + if (!SubscriptionManager.isValidSubscriptionId(subId)) { + if (DBG) { + log("invalid subscription id, use default id"); + } + r.subId = SubscriptionManager.DEFAULT_SUBSCRIPTION_ID; + } else {//APP specify subID + r.subId = subId; } - r.subId = SubscriptionManager.DEFAULT_SUBSCRIPTION_ID; - } else {//APP specify subID - r.subId = subId; + r.phoneId = getPhoneIdFromSubId(r.subId); + } else { + r.subId = subscriptionId; + r.phoneId = phoneId; } - r.phoneId = getPhoneIdFromSubId(r.subId); r.eventList = events; if (DBG) { @@ -1427,6 +1452,15 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { remove(r.binder); } } + if (events.contains(TelephonyCallback + .EVENT_SIMULTANEOUS_CELLULAR_CALLING_SUBSCRIPTIONS_CHANGED)) { + try { + r.callback.onSimultaneousCallingStateChanged( + mSimultaneousCellularCallingSubIds); + } catch (RemoteException ex) { + remove(r.binder); + } + } if (events.contains( TelephonyCallback.EVENT_LINK_CAPACITY_ESTIMATE_CHANGED)) { try { @@ -1880,8 +1914,14 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { } private void notifyCarrierNetworkChangeWithPermission(int subId, boolean active) { + int phoneId = -1; + if(Flags.preventSystemServerAndPhoneDeadlock()) { + phoneId = getPhoneIdFromSubId(subId); + } synchronized (mRecords) { - int phoneId = getPhoneIdFromSubId(subId); + if(!Flags.preventSystemServerAndPhoneDeadlock()) { + phoneId = getPhoneIdFromSubId(subId); + } mCarrierNetworkChangeState[phoneId] = active; if (VDBG) { @@ -3092,6 +3132,43 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { } } + /** + * Notify the listeners that simultaneous cellular calling subscriptions have changed + * @param subIds The set of subIds that support simultaneous cellular calling + */ + public void notifySimultaneousCellularCallingSubscriptionsChanged(int[] subIds) { + if (!checkNotifyPermission("notifySimultaneousCellularCallingSubscriptionsChanged()")) { + return; + } + + if (VDBG) { + StringBuilder b = new StringBuilder(); + b.append("notifySimultaneousCellularCallingSubscriptionsChanged: "); + b.append("subIds = {"); + for (int i : subIds) { + b.append(" "); + b.append(i); + } + b.append("}"); + log(b.toString()); + } + + synchronized (mRecords) { + mSimultaneousCellularCallingSubIds = subIds; + for (Record r : mRecords) { + if (r.matchTelephonyCallbackEvent(TelephonyCallback + .EVENT_SIMULTANEOUS_CELLULAR_CALLING_SUBSCRIPTIONS_CHANGED)) { + try { + r.callback.onSimultaneousCallingStateChanged(subIds); + } catch (RemoteException ex) { + mRemoveList.add(r.binder); + } + } + } + handleRemoveListLocked(); + } + } + @Override public void addCarrierPrivilegesCallback( int phoneId, diff --git a/services/core/java/com/android/server/UiModeManagerService.java b/services/core/java/com/android/server/UiModeManagerService.java index 66a10e4a3c11..cd8be338a031 100644 --- a/services/core/java/com/android/server/UiModeManagerService.java +++ b/services/core/java/com/android/server/UiModeManagerService.java @@ -16,8 +16,10 @@ package com.android.server; +import static android.app.Flags.modesApi; import static android.app.UiModeManager.ContrastUtils.CONTRAST_DEFAULT_VALUE; import static android.app.UiModeManager.DEFAULT_PRIORITY; +import static android.app.UiModeManager.MODE_ATTENTION_THEME_OVERLAY_OFF; import static android.app.UiModeManager.MODE_NIGHT_AUTO; import static android.app.UiModeManager.MODE_NIGHT_CUSTOM; import static android.app.UiModeManager.MODE_NIGHT_CUSTOM_TYPE_BEDTIME; @@ -50,6 +52,7 @@ import android.app.NotificationManager; import android.app.PendingIntent; import android.app.StatusBarManager; import android.app.UiModeManager; +import android.app.UiModeManager.AttentionModeThemeOverlayType; import android.app.UiModeManager.NightModeCustomReturnType; import android.app.UiModeManager.NightModeCustomType; import android.content.BroadcastReceiver; @@ -134,6 +137,7 @@ final class UiModeManagerService extends SystemService { private int mLastBroadcastState = Intent.EXTRA_DOCK_STATE_UNDOCKED; private int mNightMode = UiModeManager.MODE_NIGHT_NO; private int mNightModeCustomType = UiModeManager.MODE_NIGHT_CUSTOM_TYPE_UNKNOWN; + private int mAttentionModeThemeOverlay = UiModeManager.MODE_ATTENTION_THEME_OVERLAY_OFF; private final LocalTime DEFAULT_CUSTOM_NIGHT_START_TIME = LocalTime.of(22, 0); private final LocalTime DEFAULT_CUSTOM_NIGHT_END_TIME = LocalTime.of(6, 0); private LocalTime mCustomAutoNightModeStartMilliseconds = DEFAULT_CUSTOM_NIGHT_START_TIME; @@ -839,6 +843,8 @@ final class UiModeManagerService extends SystemService { ? customModeType : MODE_NIGHT_CUSTOM_TYPE_UNKNOWN; mNightMode = mode; + //deactivates AttentionMode if user toggles DarkTheme + mAttentionModeThemeOverlay = MODE_ATTENTION_THEME_OVERLAY_OFF; resetNightModeOverrideLocked(); persistNightMode(user); // on screen off will update configuration instead @@ -879,6 +885,29 @@ final class UiModeManagerService extends SystemService { } } + @android.annotation.EnforcePermission(android.Manifest.permission.MODIFY_DAY_NIGHT_MODE) + @Override + public void setAttentionModeThemeOverlay( + @AttentionModeThemeOverlayType int attentionModeThemeOverlayType) { + setAttentionModeThemeOverlay_enforcePermission(); + + synchronized (mLock) { + if (mAttentionModeThemeOverlay != attentionModeThemeOverlayType) { + mAttentionModeThemeOverlay = attentionModeThemeOverlayType; + Binder.withCleanCallingIdentity(()-> updateLocked(0, 0)); + } + } + } + + @android.annotation.EnforcePermission(android.Manifest.permission.MODIFY_DAY_NIGHT_MODE) + @Override + public @AttentionModeThemeOverlayType int getAttentionModeThemeOverlay() { + getAttentionModeThemeOverlay_enforcePermission(); + synchronized (mLock) { + return mAttentionModeThemeOverlay; + } + } + @Override public void setApplicationNightMode(@UiModeManager.NightMode int mode) { switch (mode) { @@ -1406,7 +1435,7 @@ final class UiModeManagerService extends SystemService { pw.print(Shell.nightModeToStr(mNightMode, mNightModeCustomType)); pw.print(") "); pw.print(" mOverrideOn/Off="); pw.print(mOverrideNightModeOn); pw.print("/"); pw.print(mOverrideNightModeOff); - + pw.print(" mAttentionModeThemeOverlay="); pw.print(mAttentionModeThemeOverlay); pw.print(" mNightModeLocked="); pw.println(mNightModeLocked); pw.print(" mCarModeEnabled="); pw.print(mCarModeEnabled); @@ -1685,7 +1714,7 @@ final class UiModeManagerService extends SystemService { } @UiModeManager.NightMode - private int getComputedUiModeConfiguration(@UiModeManager.NightMode int uiMode) { + private int getComputedUiModeConfiguration(int uiMode) { uiMode |= mComputedNightMode ? Configuration.UI_MODE_NIGHT_YES : Configuration.UI_MODE_NIGHT_NO; uiMode &= mComputedNightMode ? ~Configuration.UI_MODE_NIGHT_NO @@ -1980,18 +2009,26 @@ final class UiModeManagerService extends SystemService { } private void updateComputedNightModeLocked(boolean activate) { - mComputedNightMode = activate; - if (mNightMode == MODE_NIGHT_YES || mNightMode == UiModeManager.MODE_NIGHT_NO) { - return; - } - if (mOverrideNightModeOn && !mComputedNightMode) { - mComputedNightMode = true; - return; - } - if (mOverrideNightModeOff && mComputedNightMode) { - mComputedNightMode = false; - return; + boolean newComputedValue = activate; + if (mNightMode != MODE_NIGHT_YES && mNightMode != UiModeManager.MODE_NIGHT_NO) { + if (mOverrideNightModeOn && !newComputedValue) { + newComputedValue = true; + } else if (mOverrideNightModeOff && newComputedValue) { + newComputedValue = false; + } + } + + if (modesApi()) { + // Computes final night mode values based on Attention Mode. + mComputedNightMode = switch (mAttentionModeThemeOverlay) { + case (UiModeManager.MODE_ATTENTION_THEME_OVERLAY_NIGHT) -> true; + case (UiModeManager.MODE_ATTENTION_THEME_OVERLAY_DAY) -> false; + default -> newComputedValue; // case OFF + }; + } else { + mComputedNightMode = newComputedValue; } + if (mNightMode != MODE_NIGHT_AUTO || (mTwilightManager != null && mTwilightManager.getLastTwilightState() != null)) { resetNightModeOverrideLocked(); diff --git a/services/core/java/com/android/server/Watchdog.java b/services/core/java/com/android/server/Watchdog.java index fd17261bda41..c18bacb51671 100644 --- a/services/core/java/com/android/server/Watchdog.java +++ b/services/core/java/com/android/server/Watchdog.java @@ -172,6 +172,7 @@ public class Watchdog implements Dumpable { public static final String[] AIDL_INTERFACE_PREFIXES_OF_INTEREST = new String[] { "android.hardware.audio.core.IModule/", "android.hardware.audio.core.IConfig/", + "android.hardware.audio.effect.IFactory/", "android.hardware.biometrics.face.IFace/", "android.hardware.biometrics.fingerprint.IFingerprint/", "android.hardware.bluetooth.IBluetoothHci/", diff --git a/services/core/java/com/android/server/adaptiveauth/OWNERS b/services/core/java/com/android/server/adaptiveauth/OWNERS new file mode 100644 index 000000000000..b18810564d88 --- /dev/null +++ b/services/core/java/com/android/server/adaptiveauth/OWNERS @@ -0,0 +1 @@ +hainingc@google.com
\ No newline at end of file diff --git a/services/core/java/com/android/server/am/ActiveServices.java b/services/core/java/com/android/server/am/ActiveServices.java index 0cff8b7e88ed..9b1fade198fc 100644 --- a/services/core/java/com/android/server/am/ActiveServices.java +++ b/services/core/java/com/android/server/am/ActiveServices.java @@ -94,6 +94,8 @@ import static android.os.Process.ROOT_UID; import static android.os.Process.SHELL_UID; import static android.os.Process.SYSTEM_UID; import static android.os.Process.ZYGOTE_POLICY_FLAG_EMPTY; +import static android.content.flags.Flags.enableBindPackageIsolatedProcess; + import static com.android.internal.messages.nano.SystemMessageProto.SystemMessage.NOTE_FOREGROUND_SERVICE_BG_LAUNCH; import static com.android.internal.util.FrameworkStatsLog.FOREGROUND_SERVICE_STATE_CHANGED__FGS_START_API__FGSSTARTAPI_DELEGATE; @@ -791,13 +793,15 @@ public final class ActiveServices { static String getProcessNameForService(ServiceInfo sInfo, ComponentName name, String callingPackage, String instanceName, boolean isSdkSandbox, - boolean inSharedIsolatedProcess) { + boolean inSharedIsolatedProcess, boolean inPrivateSharedIsolatedProcess) { if (isSdkSandbox) { // For SDK sandbox, the process name is passed in as the instanceName return instanceName; } - if ((sInfo.flags & ServiceInfo.FLAG_ISOLATED_PROCESS) == 0) { - // For regular processes, just the name in sInfo + if ((sInfo.flags & ServiceInfo.FLAG_ISOLATED_PROCESS) == 0 + || (inPrivateSharedIsolatedProcess && !isDefaultProcessService(sInfo))) { + // For regular processes, or private package-shared isolated processes, just the name + // in sInfo return sInfo.processName; } // Isolated processes remain. @@ -809,6 +813,10 @@ public final class ActiveServices { } } + private static boolean isDefaultProcessService(ServiceInfo serviceInfo) { + return serviceInfo.applicationInfo.processName.equals(serviceInfo.processName); + } + private static void traceInstant(@NonNull String message, @NonNull ServiceRecord service) { if (!Trace.isTagEnabled(Trace.TRACE_TAG_ACTIVITY_MANAGER)) { return; @@ -864,7 +872,7 @@ public final class ActiveServices { ServiceLookupResult res = retrieveServiceLocked(service, instanceName, isSdkSandboxService, sdkSandboxClientAppUid, sdkSandboxClientAppPackage, resolvedType, callingPackage, - callingPid, callingUid, userId, true, callerFg, false, false, null, false); + callingPid, callingUid, userId, true, callerFg, false, false, null, false, false); if (res == null) { return null; } @@ -1550,7 +1558,7 @@ public final class ActiveServices { ServiceLookupResult r = retrieveServiceLocked(service, instanceName, isSdkSandboxService, sdkSandboxClientAppUid, sdkSandboxClientAppPackage, resolvedType, null, Binder.getCallingPid(), Binder.getCallingUid(), userId, false, false, false, false, - null, false); + null, false, false); if (r != null) { if (r.record != null) { final long origId = Binder.clearCallingIdentity(); @@ -1642,7 +1650,7 @@ public final class ActiveServices { IBinder peekServiceLocked(Intent service, String resolvedType, String callingPackage) { ServiceLookupResult r = retrieveServiceLocked(service, null, resolvedType, callingPackage, Binder.getCallingPid(), Binder.getCallingUid(), - UserHandle.getCallingUserId(), false, false, false, false, false); + UserHandle.getCallingUserId(), false, false, false, false, false, false); IBinder ret = null; if (r != null) { @@ -3714,6 +3722,9 @@ public final class ActiveServices { || (flags & Context.BIND_EXTERNAL_SERVICE_LONG) != 0; final boolean allowInstant = (flags & Context.BIND_ALLOW_INSTANT) != 0; final boolean inSharedIsolatedProcess = (flags & Context.BIND_SHARED_ISOLATED_PROCESS) != 0; + final boolean inPrivateSharedIsolatedProcess = + ((flags & Context.BIND_PACKAGE_ISOLATED_PROCESS) != 0) + && enableBindPackageIsolatedProcess(); final boolean matchQuarantined = (flags & Context.BIND_MATCH_QUARANTINED_COMPONENTS) != 0; @@ -3725,7 +3736,7 @@ public final class ActiveServices { isSdkSandboxService, sdkSandboxClientAppUid, sdkSandboxClientAppPackage, resolvedType, callingPackage, callingPid, callingUid, userId, true, callerFg, isBindExternal, allowInstant, null /* fgsDelegateOptions */, - inSharedIsolatedProcess, matchQuarantined); + inSharedIsolatedProcess, inPrivateSharedIsolatedProcess, matchQuarantined); if (res == null) { return 0; } @@ -4204,14 +4215,14 @@ public final class ActiveServices { } private ServiceLookupResult retrieveServiceLocked(Intent service, - String instanceName, String resolvedType, String callingPackage, - int callingPid, int callingUid, int userId, - boolean createIfNeeded, boolean callingFromFg, boolean isBindExternal, - boolean allowInstant, boolean inSharedIsolatedProcess) { + String instanceName, String resolvedType, String callingPackage, int callingPid, + int callingUid, int userId, boolean createIfNeeded, boolean callingFromFg, + boolean isBindExternal, boolean allowInstant, boolean inSharedIsolatedProcess, + boolean inPrivateSharedIsolatedProcess) { return retrieveServiceLocked(service, instanceName, false, INVALID_UID, null, resolvedType, callingPackage, callingPid, callingUid, userId, createIfNeeded, callingFromFg, isBindExternal, allowInstant, null /* fgsDelegateOptions */, - inSharedIsolatedProcess); + inSharedIsolatedProcess, inPrivateSharedIsolatedProcess); } // TODO(b/265746493): Special case for HotwordDetectionService, @@ -4233,21 +4244,22 @@ public final class ActiveServices { String callingPackage, int callingPid, int callingUid, int userId, boolean createIfNeeded, boolean callingFromFg, boolean isBindExternal, boolean allowInstant, ForegroundServiceDelegationOptions fgsDelegateOptions, - boolean inSharedIsolatedProcess) { + boolean inSharedIsolatedProcess, boolean inPrivateSharedIsolatedProcess) { return retrieveServiceLocked(service, instanceName, isSdkSandboxService, sdkSandboxClientAppUid, sdkSandboxClientAppPackage, resolvedType, callingPackage, callingPid, callingUid, userId, createIfNeeded, callingFromFg, isBindExternal, allowInstant, fgsDelegateOptions, inSharedIsolatedProcess, - false /* matchQuarantined */); + inPrivateSharedIsolatedProcess, false /* matchQuarantined */); } - private ServiceLookupResult retrieveServiceLocked(Intent service, - String instanceName, boolean isSdkSandboxService, int sdkSandboxClientAppUid, - String sdkSandboxClientAppPackage, String resolvedType, + private ServiceLookupResult retrieveServiceLocked( + Intent service, String instanceName, boolean isSdkSandboxService, + int sdkSandboxClientAppUid, String sdkSandboxClientAppPackage, String resolvedType, String callingPackage, int callingPid, int callingUid, int userId, boolean createIfNeeded, boolean callingFromFg, boolean isBindExternal, boolean allowInstant, ForegroundServiceDelegationOptions fgsDelegateOptions, - boolean inSharedIsolatedProcess, boolean matchQuarantined) { + boolean inSharedIsolatedProcess, boolean inPrivateSharedIsolatedProcess, + boolean matchQuarantined) { if (isSdkSandboxService && instanceName == null) { throw new IllegalArgumentException("No instanceName provided for sdk sandbox process"); } @@ -4344,7 +4356,8 @@ public final class ActiveServices { final ServiceRestarter res = new ServiceRestarter(); final String processName = getProcessNameForService(sInfo, cn, callingPackage, null /* instanceName */, false /* isSdkSandbox */, - false /* inSharedIsolatedProcess */); + false /* inSharedIsolatedProcess */, + false /*inPrivateSharedIsolatedProcess*/); r = new ServiceRecord(mAm, cn /* name */, cn /* instanceName */, sInfo.applicationInfo.packageName, sInfo.applicationInfo.uid, filter, sInfo, callingFromFg, res, processName, @@ -4415,6 +4428,10 @@ public final class ActiveServices { throw new SecurityException("BIND_EXTERNAL_SERVICE failed, " + className + " is not exported"); } + if (inPrivateSharedIsolatedProcess) { + throw new SecurityException("BIND_PACKAGE_ISOLATED_PROCESS cannot be " + + "applied to an external service."); + } if ((sInfo.flags & ServiceInfo.FLAG_ISOLATED_PROCESS) == 0) { throw new SecurityException("BIND_EXTERNAL_SERVICE failed, " + className + " is not an isolatedProcess"); @@ -4448,28 +4465,32 @@ public final class ActiveServices { throw new SecurityException("BIND_EXTERNAL_SERVICE failed, " + name + " is not an externalService"); } - if (inSharedIsolatedProcess) { + if (inSharedIsolatedProcess && inPrivateSharedIsolatedProcess) { + throw new SecurityException("Either BIND_SHARED_ISOLATED_PROCESS or " + + "BIND_PACKAGE_ISOLATED_PROCESS should be set. Not both."); + } + if (inSharedIsolatedProcess || inPrivateSharedIsolatedProcess) { if ((sInfo.flags & ServiceInfo.FLAG_ISOLATED_PROCESS) == 0) { throw new SecurityException("BIND_SHARED_ISOLATED_PROCESS failed, " + className + " is not an isolatedProcess"); } + } + if (inPrivateSharedIsolatedProcess && isDefaultProcessService(sInfo)) { + throw new SecurityException("BIND_PACKAGE_ISOLATED_PROCESS cannot be used for " + + "services running in the main app process."); + } + if (inSharedIsolatedProcess) { + if (instanceName == null) { + throw new IllegalArgumentException("instanceName must be provided for " + + "binding a service into a shared isolated process."); + } if ((sInfo.flags & ServiceInfo.FLAG_ALLOW_SHARED_ISOLATED_PROCESS) == 0) { throw new SecurityException("BIND_SHARED_ISOLATED_PROCESS failed, " + className + " has not set the allowSharedIsolatedProcess " + " attribute."); } - if (instanceName == null) { - throw new IllegalArgumentException("instanceName must be provided for " - + "binding a service into a shared isolated process."); - } } 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)) { @@ -4503,11 +4524,13 @@ public final class ActiveServices { = new Intent.FilterComparison(service.cloneFilter()); final ServiceRestarter res = new ServiceRestarter(); String processName = getProcessNameForService(sInfo, name, callingPackage, - instanceName, isSdkSandboxService, inSharedIsolatedProcess); + instanceName, isSdkSandboxService, inSharedIsolatedProcess, + inPrivateSharedIsolatedProcess); r = new ServiceRecord(mAm, className, name, definingPackageName, definingUid, filter, sInfo, callingFromFg, res, processName, sdkSandboxClientAppUid, - sdkSandboxClientAppPackage, inSharedIsolatedProcess); + sdkSandboxClientAppPackage, + (inSharedIsolatedProcess || inPrivateSharedIsolatedProcess)); res.setService(r); smap.mServicesByInstanceName.put(name, r); smap.mServicesByIntent.put(filter, r); @@ -5377,13 +5400,13 @@ public final class ActiveServices { return msg; } mAm.mProcessList.getAppStartInfoTracker().handleProcessServiceStart(startTimeNs, app, r, - hostingRecord, true); + true); if (isolated) { r.isolationHostProc = app; } } else { mAm.mProcessList.getAppStartInfoTracker().handleProcessServiceStart(startTimeNs, app, r, - hostingRecord, false); + false); } if (r.fgRequired) { @@ -8504,7 +8527,8 @@ public final class ActiveServices { null /* sdkSandboxClientAppPackage */, null /* resolvedType */, callingPackage, callingPid, callingUid, userId, true /* createIfNeeded */, false /* callingFromFg */, false /* isBindExternal */, false /* allowInstant */ , - options, false /* inSharedIsolatedProcess */); + options, false /* inSharedIsolatedProcess */, + false /*inPrivateSharedIsolatedProcess*/); if (res == null || res.record == null) { Slog.d(TAG, "startForegroundServiceDelegateLocked retrieveServiceLocked returns null"); diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java index e583a6cd6b1f..f6d954a69044 100644 --- a/services/core/java/com/android/server/am/ActivityManagerService.java +++ b/services/core/java/com/android/server/am/ActivityManagerService.java @@ -4830,7 +4830,11 @@ public class ActivityManagerService extends IActivityManager.Stub if (!mConstants.mEnableWaitForFinishAttachApplication) { finishAttachApplicationInner(startSeq, callingUid, pid); } - maybeSendBootCompletedLocked(app); + + // Temporarily disable sending BOOT_COMPLETED to see if this was impacting perf tests + if (false) { + maybeSendBootCompletedLocked(app); + } } catch (Exception e) { // We need kill the process group here. (b/148588589) Slog.wtf(TAG, "Exception thrown during bind of " + app, e); @@ -6525,7 +6529,24 @@ public class ActivityManagerService extends IActivityManager.Stub @Override public int checkUriPermission(Uri uri, int pid, int uid, final int modeFlags, int userId, IBinder callerToken) { - enforceNotIsolatedCaller("checkUriPermission"); + return checkUriPermission(uri, pid, uid, modeFlags, userId, + /* isFullAccessForContentUri */ false, "checkUriPermission"); + } + + /** + * @param uri This uri must NOT contain an embedded userId. + * @param userId The userId in which the uri is to be resolved. + */ + @Override + public int checkContentUriPermissionFull(Uri uri, int pid, int uid, + final int modeFlags, int userId) { + return checkUriPermission(uri, pid, uid, modeFlags, userId, + /* isFullAccessForContentUri */ true, "checkContentUriPermissionFull"); + } + + private int checkUriPermission(Uri uri, int pid, int uid, + final int modeFlags, int userId, boolean isFullAccessForContentUri, String methodName) { + enforceNotIsolatedCaller(methodName); // Our own process gets to do everything. if (pid == MY_PID) { @@ -6536,8 +6557,10 @@ public class ActivityManagerService extends IActivityManager.Stub return PackageManager.PERMISSION_DENIED; } } - return mUgmInternal.checkUriPermission(new GrantUri(userId, uri, modeFlags), uid, modeFlags) - ? PackageManager.PERMISSION_GRANTED : PackageManager.PERMISSION_DENIED; + boolean granted = mUgmInternal.checkUriPermission(new GrantUri(userId, uri, modeFlags), uid, + modeFlags, isFullAccessForContentUri); + + return granted ? PackageManager.PERMISSION_GRANTED : PackageManager.PERMISSION_DENIED; } @Override @@ -9858,7 +9881,7 @@ public class ActivityManagerService extends IActivityManager.Stub @Override - public void setApplicationStartInfoCompleteListener( + public void addApplicationStartInfoCompleteListener( IApplicationStartInfoCompleteListener listener, int userId) { enforceNotIsolatedCaller("setApplicationStartInfoCompleteListener"); @@ -9873,7 +9896,8 @@ public class ActivityManagerService extends IActivityManager.Stub @Override - public void clearApplicationStartInfoCompleteListener(int userId) { + public void removeApplicationStartInfoCompleteListener( + IApplicationStartInfoCompleteListener listener, int userId) { enforceNotIsolatedCaller("clearApplicationStartInfoCompleteListener"); // For the simplification, we don't support USER_ALL nor USER_CURRENT here. @@ -9882,7 +9906,8 @@ public class ActivityManagerService extends IActivityManager.Stub } final int callingUid = Binder.getCallingUid(); - mProcessList.getAppStartInfoTracker().clearStartInfoCompleteListener(callingUid, true); + mProcessList.getAppStartInfoTracker().removeStartInfoCompleteListener(listener, callingUid, + true); } @Override @@ -13747,11 +13772,6 @@ public class ActivityManagerService extends IActivityManager.Stub 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 diff --git a/services/core/java/com/android/server/am/AppStartInfoTracker.java b/services/core/java/com/android/server/am/AppStartInfoTracker.java index 82e554e67b7e..c85723525aa1 100644 --- a/services/core/java/com/android/server/am/AppStartInfoTracker.java +++ b/services/core/java/com/android/server/am/AppStartInfoTracker.java @@ -81,18 +81,18 @@ public final class AppStartInfoTracker { private static final int FOREACH_ACTION_REMOVE_ITEM = 1; private static final int FOREACH_ACTION_STOP_ITERATION = 2; - private static final int APP_START_INFO_HISTORY_LIST_SIZE = 16; + @VisibleForTesting static final int APP_START_INFO_HISTORY_LIST_SIZE = 16; @VisibleForTesting static final String APP_START_STORE_DIR = "procstartstore"; @VisibleForTesting static final String APP_START_INFO_FILE = "procstartinfo"; - private final Object mLock = new Object(); + @VisibleForTesting final Object mLock = new Object(); - private boolean mEnabled = false; + @VisibleForTesting boolean mEnabled = false; /** Initialized in {@link #init} and read-only after that. */ - private ActivityManagerService mService; + @VisibleForTesting ActivityManagerService mService; /** Initialized in {@link #init} and read-only after that. */ private Handler mHandler; @@ -112,14 +112,14 @@ public final class AppStartInfoTracker { * * <p>Initialized in {@link #init} and read-only after that. No lock is needed. */ - private int mAppStartInfoHistoryListSize; + @VisibleForTesting int mAppStartInfoHistoryListSize; @GuardedBy("mLock") private final ProcessMap<AppStartInfoContainer> mData; /** UID as key. */ @GuardedBy("mLock") - private final SparseArray<ApplicationStartInfoCompleteCallback> mCallbacks; + private final SparseArray<ArrayList<ApplicationStartInfoCompleteCallback>> mCallbacks; /** * Whether or not we've loaded the historical app process start info from persistent storage. @@ -146,7 +146,8 @@ public final class AppStartInfoTracker { * Key is timestamp of launch from {@link #ActivityMetricsLaunchObserver}. */ @GuardedBy("mLock") - private ArrayMap<Long, ApplicationStartInfo> mInProgRecords = new ArrayMap<>(); + @VisibleForTesting + final ArrayMap<Long, ApplicationStartInfo> mInProgRecords = new ArrayMap<>(); AppStartInfoTracker() { mCallbacks = new SparseArray<>(); @@ -229,7 +230,7 @@ public final class AppStartInfoTracker { ApplicationStartInfo info = mInProgRecords.get(id); info.setStartType((int) temperature); addBaseFieldsFromProcessRecord(info, app); - addStartInfoLocked(info); + mInProgRecords.put(id, addStartInfoLocked(info)); } else { mInProgRecords.remove(id); } @@ -262,6 +263,7 @@ public final class AppStartInfoTracker { ApplicationStartInfo info = mInProgRecords.get(id); info.setStartupState(ApplicationStartInfo.STARTUP_STATE_FIRST_FRAME_DRAWN); info.setLaunchMode(launchMode); + checkCompletenessAndCallback(info); } } @@ -281,7 +283,7 @@ public final class AppStartInfoTracker { } public void handleProcessServiceStart(long startTimeNs, ProcessRecord app, - ServiceRecord serviceRecord, HostingRecord hostingRecord, boolean cold) { + ServiceRecord serviceRecord, boolean cold) { synchronized (mLock) { if (!mEnabled) { return; @@ -297,7 +299,9 @@ public final class AppStartInfoTracker { && serviceRecord.permission.contains("android.permission.BIND_JOB_SERVICE") ? ApplicationStartInfo.START_REASON_JOB : ApplicationStartInfo.START_REASON_SERVICE); - start.setIntent(serviceRecord.intent.getIntent()); + if (serviceRecord.intent != null) { + start.setIntent(serviceRecord.intent.getIntent()); + } addStartInfoLocked(start); } } @@ -378,6 +382,7 @@ public final class AppStartInfoTracker { start.setPackageUid(app.info.uid); start.setDefiningUid(definingUid > 0 ? definingUid : app.info.uid); start.setProcessName(app.processName); + start.setPackageName(app.info.packageName); } void reportApplicationOnCreateTimeNanos(ProcessRecord app, long timeNs) { @@ -419,12 +424,12 @@ public final class AppStartInfoTracker { } private void addTimestampToStart(ProcessRecord app, long timeNs, int key) { - addTimestampToStart(app.processName, app.uid, timeNs, key); + addTimestampToStart(app.info.packageName, app.uid, timeNs, key); } - private void addTimestampToStart(String processName, int uid, long timeNs, int key) { + private void addTimestampToStart(String packageName, int uid, long timeNs, int key) { synchronized (mLock) { - AppStartInfoContainer container = mData.get(processName, uid); + AppStartInfoContainer container = mData.get(packageName, uid); if (container == null) { // Record was not created, discard new data. return; @@ -443,11 +448,11 @@ public final class AppStartInfoTracker { final ApplicationStartInfo info = new ApplicationStartInfo(raw); - AppStartInfoContainer container = mData.get(raw.getProcessName(), raw.getRealUid()); + AppStartInfoContainer container = mData.get(raw.getPackageName(), raw.getRealUid()); if (container == null) { container = new AppStartInfoContainer(mAppStartInfoHistoryListSize); container.mUid = info.getRealUid(); - mData.put(raw.getProcessName(), raw.getRealUid(), container); + mData.put(raw.getPackageName(), raw.getRealUid(), container); } container.addStartInfoLocked(info); @@ -465,11 +470,18 @@ public final class AppStartInfoTracker { synchronized (mLock) { if (startInfo.getStartupState() == ApplicationStartInfo.STARTUP_STATE_FIRST_FRAME_DRAWN) { - ApplicationStartInfoCompleteCallback callback = + final List<ApplicationStartInfoCompleteCallback> callbacks = mCallbacks.get(startInfo.getRealUid()); - if (callback != null) { - callback.onApplicationStartInfoComplete(startInfo); + if (callbacks == null) { + return; } + final int size = callbacks.size(); + for (int i = 0; i < size; i++) { + if (callbacks.get(i) != null) { + callbacks.get(i).onApplicationStartInfoComplete(startInfo); + } + } + mCallbacks.remove(startInfo.getRealUid()); } } } @@ -479,6 +491,9 @@ public final class AppStartInfoTracker { if (!mEnabled) { return; } + if (maxNum == 0) { + maxNum = APP_START_INFO_HISTORY_LIST_SIZE; + } final long identity = Binder.clearCallingIdentity(); try { synchronized (mLock) { @@ -542,7 +557,6 @@ public final class AppStartInfoTracker { } catch (RemoteException e) { /*ignored*/ } - clearStartInfoCompleteListener(mUid, true); } void unlinkToDeath() { @@ -551,7 +565,7 @@ public final class AppStartInfoTracker { @Override public void binderDied() { - clearStartInfoCompleteListener(mUid, false); + removeStartInfoCompleteListener(mCallback, mUid, false); } } @@ -561,22 +575,43 @@ public final class AppStartInfoTracker { if (!mEnabled) { return; } - mCallbacks.put(uid, new ApplicationStartInfoCompleteCallback(listener, uid)); + ArrayList<ApplicationStartInfoCompleteCallback> callbacks = mCallbacks.get(uid); + if (callbacks == null) { + mCallbacks.set(uid, + callbacks = new ArrayList<ApplicationStartInfoCompleteCallback>()); + } + callbacks.add(new ApplicationStartInfoCompleteCallback(listener, uid)); } } - void clearStartInfoCompleteListener(final int uid, boolean unlinkDeathRecipient) { + void removeStartInfoCompleteListener( + final IApplicationStartInfoCompleteListener listener, final int uid, + boolean unlinkDeathRecipient) { synchronized (mLock) { if (!mEnabled) { return; } - if (unlinkDeathRecipient) { - ApplicationStartInfoCompleteCallback callback = mCallbacks.get(uid); - if (callback != null) { - callback.unlinkToDeath(); + final ArrayList<ApplicationStartInfoCompleteCallback> callbacks = mCallbacks.get(uid); + if (callbacks == null) { + return; + } + final int size = callbacks.size(); + int index; + for (index = 0; index < size; index++) { + final ApplicationStartInfoCompleteCallback callback = callbacks.get(index); + if (callback.mCallback == listener) { + if (unlinkDeathRecipient) { + callback.unlinkToDeath(); + } + break; } } - mCallbacks.remove(uid); + if (index < size) { + callbacks.remove(index); + } + if (callbacks.isEmpty()) { + mCallbacks.remove(uid); + } } } @@ -864,6 +899,7 @@ public final class AppStartInfoTracker { mProcStartInfoFile.delete(); } mData.getMap().clear(); + mInProgRecords.clear(); } } @@ -933,6 +969,10 @@ public final class AppStartInfoTracker { /** Convenience method to obtain timestamp of beginning of start.*/ private static long getStartTimestamp(ApplicationStartInfo startInfo) { + if (startInfo.getStartupTimestamps() == null + || !startInfo.getStartupTimestamps().containsKey(START_TIMESTAMP_LAUNCH)) { + return -1; + } return startInfo.getStartupTimestamps().get(START_TIMESTAMP_LAUNCH); } @@ -970,7 +1010,6 @@ public final class AppStartInfoTracker { if (oldestIndex >= 0) { mInfos.remove(oldestIndex); } - mInfos.remove(0); } mInfos.add(info); Collections.sort(mInfos, (a, b) -> diff --git a/services/core/java/com/android/server/am/ContentProviderHelper.java b/services/core/java/com/android/server/am/ContentProviderHelper.java index 30f21a65b5b1..095d907d7df6 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 @@ public class ContentProviderHelper { ProviderInfo cpi = providers.get(i); boolean singleton = mService.isSingleton(cpi.processName, cpi.applicationInfo, cpi.name, cpi.flags); - 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 + 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 // 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,7 +1398,8 @@ public class ContentProviderHelper { final boolean processMatch = Objects.equals(pi.processName, app.processName) || pi.multiprocess; - final boolean userMatch = !isSingletonOrSystemUserOnly(pi) + final boolean userMatch = !mService.isSingleton( + pi.processName, pi.applicationInfo, pi.name, pi.flags) || app.userId == UserHandle.USER_SYSTEM; final boolean isInstantApp = pi.applicationInfo.isInstantApp(); final boolean splitInstalled = pi.splitName == null @@ -1984,13 +1985,4 @@ public class ContentProviderHelper { 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/ProcessList.java b/services/core/java/com/android/server/am/ProcessList.java index b03183cb37d5..fa5dbd2543d3 100644 --- a/services/core/java/com/android/server/am/ProcessList.java +++ b/services/core/java/com/android/server/am/ProcessList.java @@ -2935,7 +2935,11 @@ public final class ProcessList { return true; } - private static void freezeBinderAndPackageCgroup(ArrayList<Pair<ProcessRecord, Boolean>> procs, + private static boolean unfreezePackageCgroup(int packageUID) { + return freezePackageCgroup(packageUID, false); + } + + private static void freezeBinderAndPackageCgroup(List<Pair<ProcessRecord, Boolean>> procs, int packageUID) { // Freeze all binder processes under the target UID (whose cgroup is about to be frozen). // Since we're going to kill these, we don't need to unfreze them later. @@ -2943,12 +2947,9 @@ public final class ProcessList { // processes (forks) should not be Binder users. int N = procs.size(); for (int i = 0; i < N; i++) { - final int uid = procs.get(i).first.uid; final int pid = procs.get(i).first.getPid(); int nRetries = 0; - // We only freeze the cgroup of the target package, so we do not need to freeze the - // Binder interfaces of dependant processes in other UIDs. - if (pid > 0 && uid == packageUID) { + if (pid > 0) { try { int rc; do { @@ -2962,12 +2963,19 @@ public final class ProcessList { } // We freeze the entire UID (parent) cgroup so that newly-specialized processes also freeze - // despite being added to a new child cgroup. The cgroups of package dependant processes are - // not frozen, since it's possible this would freeze processes with no dependency on the - // package being killed here. + // despite being added to a child cgroup created after this call that would otherwise be + // unfrozen. freezePackageCgroup(packageUID, true); } + private static List<Pair<ProcessRecord, Boolean>> getUIDSublist( + List<Pair<ProcessRecord, Boolean>> procs, int startIdx) { + final int uid = procs.get(startIdx).first.uid; + int endIdx = startIdx + 1; + while (endIdx < procs.size() && procs.get(endIdx).first.uid == uid) ++endIdx; + return procs.subList(startIdx, endIdx); + } + @GuardedBy({"mService", "mProcLock"}) boolean killPackageProcessesLSP(String packageName, int appId, int userId, int minOomAdj, boolean callerWillRestart, boolean allowRestart, @@ -3063,25 +3071,36 @@ public final class ProcessList { } } - final int packageUID = UserHandle.getUid(userId, appId); - final boolean doFreeze = appId >= Process.FIRST_APPLICATION_UID - && appId <= Process.LAST_APPLICATION_UID; - if (doFreeze) { - freezeBinderAndPackageCgroup(procs, packageUID); + final boolean killingUserApp = appId >= Process.FIRST_APPLICATION_UID + && appId <= Process.LAST_APPLICATION_UID; + + if (killingUserApp) { + procs.sort((o1, o2) -> Integer.compare(o1.first.uid, o2.first.uid)); } - int N = procs.size(); - for (int i=0; i<N; i++) { - final Pair<ProcessRecord, Boolean> proc = procs.get(i); - removeProcessLocked(proc.first, callerWillRestart, allowRestart || proc.second, - reasonCode, subReason, reason, !doFreeze /* async */); + int idx = 0; + while (idx < procs.size()) { + final List<Pair<ProcessRecord, Boolean>> uidProcs = getUIDSublist(procs, idx); + final int packageUID = uidProcs.get(0).first.uid; + + // Do not freeze for system apps or for dependencies of the targeted package, but + // make sure to freeze the targeted package for all users if called with USER_ALL. + final boolean doFreeze = killingUserApp && UserHandle.getAppId(packageUID) == appId; + + if (doFreeze) freezeBinderAndPackageCgroup(uidProcs, packageUID); + + for (Pair<ProcessRecord, Boolean> proc : uidProcs) { + removeProcessLocked(proc.first, callerWillRestart, allowRestart || proc.second, + reasonCode, subReason, reason, !doFreeze /* async */); + } + killAppZygotesLocked(packageName, appId, userId, false /* force */); + + if (doFreeze) unfreezePackageCgroup(packageUID); + + idx += uidProcs.size(); } - killAppZygotesLocked(packageName, appId, userId, false /* force */); mService.updateOomAdjLocked(OOM_ADJ_REASON_PROCESS_END); - if (doFreeze) { - freezePackageCgroup(packageUID, false); - } - return N > 0; + return procs.size() > 0; } @GuardedBy("mService") diff --git a/services/core/java/com/android/server/app/GameManagerSettings.java b/services/core/java/com/android/server/app/GameManagerSettings.java index 5189017f5bf0..b084cf3c3b12 100644 --- a/services/core/java/com/android/server/app/GameManagerSettings.java +++ b/services/core/java/com/android/server/app/GameManagerSettings.java @@ -251,6 +251,7 @@ public class GameManagerSettings { + type); } } + str.close(); } catch (XmlPullParserException | java.io.IOException e) { Slog.wtf(TAG, "Error reading game manager settings", e); return false; diff --git a/services/core/java/com/android/server/appop/AppOpsService.java b/services/core/java/com/android/server/appop/AppOpsService.java index df8d9e1a406c..2ed217a89397 100644 --- a/services/core/java/com/android/server/appop/AppOpsService.java +++ b/services/core/java/com/android/server/appop/AppOpsService.java @@ -65,6 +65,9 @@ import static android.app.AppOpsManager.opRestrictsRead; import static android.app.AppOpsManager.opToName; import static android.app.AppOpsManager.opToPublicName; import static android.companion.virtual.VirtualDeviceManager.PERSISTENT_DEVICE_ID_DEFAULT; +import static android.content.Intent.ACTION_PACKAGE_ADDED; +import static android.content.Intent.ACTION_PACKAGE_REMOVED; +import static android.content.Intent.EXTRA_REPLACING; import static android.content.pm.PermissionInfo.PROTECTION_DANGEROUS; import static android.content.pm.PermissionInfo.PROTECTION_FLAG_APPOP; @@ -973,7 +976,29 @@ public class AppOpsService extends IAppOpsService.Stub { String pkgName = intent.getData().getEncodedSchemeSpecificPart(); int uid = intent.getIntExtra(Intent.EXTRA_UID, Process.INVALID_UID); - if (action.equals(Intent.ACTION_PACKAGE_REPLACED)) { + if (action.equals(ACTION_PACKAGE_ADDED) + && !intent.getBooleanExtra(EXTRA_REPLACING, false)) { + PackageInfo pi = getPackageManagerInternal().getPackageInfo(pkgName, + PackageManager.GET_PERMISSIONS, Process.myUid(), + UserHandle.getUserId(uid)); + boolean isSamplingTarget = isSamplingTarget(pi); + synchronized (AppOpsService.this) { + if (isSamplingTarget) { + mRarelyUsedPackages.add(pkgName); + } + UidState uidState = getUidStateLocked(uid, true); + if (!uidState.pkgOps.containsKey(pkgName)) { + uidState.pkgOps.put(pkgName, + new Ops(pkgName, uidState)); + } + + createSandboxUidStateIfNotExistsForAppLocked(uid); + } + } else if (action.equals(ACTION_PACKAGE_REMOVED) && !intent.hasExtra(EXTRA_REPLACING)) { + synchronized (AppOpsService.this) { + packageRemovedLocked(uid, pkgName); + } + } else if (action.equals(Intent.ACTION_PACKAGE_REPLACED)) { AndroidPackage pkg = getPackageManagerInternal().getPackage(pkgName); if (pkg == null) { return; @@ -1052,7 +1077,9 @@ public class AppOpsService extends IAppOpsService.Stub { mHistoricalRegistry.systemReady(mContext.getContentResolver()); IntentFilter packageUpdateFilter = new IntentFilter(); + packageUpdateFilter.addAction(ACTION_PACKAGE_ADDED); packageUpdateFilter.addAction(Intent.ACTION_PACKAGE_REPLACED); + packageUpdateFilter.addAction(ACTION_PACKAGE_REMOVED); packageUpdateFilter.addDataScheme("package"); mContext.registerReceiverAsUser(mOnPackageUpdatedReceiver, UserHandle.ALL, @@ -1079,7 +1106,7 @@ public class AppOpsService extends IAppOpsService.Stub { String action; if (!ArrayUtils.contains(pkgsInUid, pkg)) { - action = Intent.ACTION_PACKAGE_REMOVED; + action = ACTION_PACKAGE_REMOVED; } else { action = Intent.ACTION_PACKAGE_REPLACED; } @@ -1160,44 +1187,6 @@ public class AppOpsService extends IAppOpsService.Stub { // onUserRemoved handled by #removeUser }); - - getPackageManagerInternal().getPackageList( - new PackageManagerInternal.PackageListObserver() { - @Override - public void onPackageAdded(String packageName, int appId) { - PackageInfo pi = getPackageManagerInternal().getPackageInfo(packageName, - PackageManager.GET_PERMISSIONS, Process.myUid(), - mContext.getUserId()); - boolean isSamplingTarget = isSamplingTarget(pi); - int[] userIds = getUserManagerInternal().getUserIds(); - synchronized (AppOpsService.this) { - if (isSamplingTarget) { - mRarelyUsedPackages.add(packageName); - } - for (int i = 0; i < userIds.length; i++) { - int uid = UserHandle.getUid(userIds[i], appId); - UidState uidState = getUidStateLocked(uid, true); - if (!uidState.pkgOps.containsKey(packageName)) { - uidState.pkgOps.put(packageName, - new Ops(packageName, uidState)); - } - - createSandboxUidStateIfNotExistsForAppLocked(uid); - } - } - } - - @Override - public void onPackageRemoved(String packageName, int appId) { - int[] userIds = getUserManagerInternal().getUserIds(); - synchronized (AppOpsService.this) { - for (int i = 0; i < userIds.length; i++) { - int uid = UserHandle.getUid(userIds[i], appId); - packageRemovedLocked(uid, packageName); - } - } - } - }); } /** @@ -2893,6 +2882,10 @@ public class AppOpsService extends IAppOpsService.Stub { return new SyncNotedAppOp(AppOpsManager.MODE_ERRORED, code, attributionTag, packageName); } + if (proxyAttributionTag != null + && !isAttributionTagDefined(packageName, proxyPackageName, proxyAttributionTag)) { + proxyAttributionTag = null; + } synchronized (this) { final Ops ops = getOpsLocked(uid, packageName, attributionTag, @@ -3487,6 +3480,10 @@ public class AppOpsService extends IAppOpsService.Stub { return new SyncNotedAppOp(AppOpsManager.MODE_ERRORED, code, attributionTag, packageName); } + if (proxyAttributionTag != null + && !isAttributionTagDefined(packageName, proxyPackageName, proxyAttributionTag)) { + proxyAttributionTag = null; + } boolean isRestricted = false; int startType = START_TYPE_FAILED; @@ -4340,6 +4337,36 @@ public class AppOpsService extends IAppOpsService.Stub { return false; } + /** + * Checks to see if the attribution tag is defined in either package or proxyPackage. + * This method is intended for ProxyAttributionTag validation and returns false + * if it does not exist in either one of them. + * + * @param packageName Name of the package + * @param proxyPackageName Name of the proxy package + * @param attributionTag attribution tag to be checked + * + * @return boolean specifying if attribution tag is valid or not + */ + private boolean isAttributionTagDefined(@Nullable String packageName, + @Nullable String proxyPackageName, + @Nullable String attributionTag) { + if (packageName == null) { + return false; + } else if (attributionTag == null) { + return true; + } + PackageManagerInternal pmInt = LocalServices.getService(PackageManagerInternal.class); + if (proxyPackageName != null) { + AndroidPackage proxyPkg = pmInt.getPackage(proxyPackageName); + if (proxyPkg != null && isAttributionInPackage(proxyPkg, attributionTag)) { + return true; + } + } + AndroidPackage pkg = pmInt.getPackage(packageName); + return isAttributionInPackage(pkg, attributionTag); + } + private void logVerifyAndGetBypassFailure(int uid, @NonNull SecurityException e, @NonNull String methodName) { if (Process.isIsolated(uid)) { diff --git a/services/core/java/com/android/server/audio/AudioDeviceBroker.java b/services/core/java/com/android/server/audio/AudioDeviceBroker.java index f80228afa52d..99b45ec79571 100644 --- a/services/core/java/com/android/server/audio/AudioDeviceBroker.java +++ b/services/core/java/com/android/server/audio/AudioDeviceBroker.java @@ -1812,22 +1812,21 @@ public class AudioDeviceBroker { "msg: MSG_L_SET_BT_ACTIVE_DEVICE " + "received with null profile proxy: " + btInfo)).printLog(TAG)); - sendMsg(MSG_CHECK_MUTE_MUSIC, SENDMSG_REPLACE, 0 /*delay*/); - return; - } - @AudioSystem.AudioFormatNativeEnumForBtCodec final int codec = - mBtHelper.getCodecWithFallback(btInfo.mDevice, - btInfo.mProfile, btInfo.mIsLeOutput, - "MSG_L_SET_BT_ACTIVE_DEVICE"); - mDeviceInventory.onSetBtActiveDevice(btInfo, codec, - (btInfo.mProfile - != BluetoothProfile.LE_AUDIO || btInfo.mIsLeOutput) - ? mAudioService.getBluetoothContextualVolumeStream() - : AudioSystem.STREAM_DEFAULT); - if (btInfo.mProfile == BluetoothProfile.LE_AUDIO - || btInfo.mProfile == BluetoothProfile.HEARING_AID) { - onUpdateCommunicationRouteClient(isBluetoothScoRequested(), - "setBluetoothActiveDevice"); + } else { + @AudioSystem.AudioFormatNativeEnumForBtCodec final int codec = + mBtHelper.getCodecWithFallback(btInfo.mDevice, + btInfo.mProfile, btInfo.mIsLeOutput, + "MSG_L_SET_BT_ACTIVE_DEVICE"); + mDeviceInventory.onSetBtActiveDevice(btInfo, codec, + (btInfo.mProfile + != BluetoothProfile.LE_AUDIO || btInfo.mIsLeOutput) + ? mAudioService.getBluetoothContextualVolumeStream() + : AudioSystem.STREAM_DEFAULT); + if (btInfo.mProfile == BluetoothProfile.LE_AUDIO + || btInfo.mProfile == BluetoothProfile.HEARING_AID) { + onUpdateCommunicationRouteClient(isBluetoothScoRequested(), + "setBluetoothActiveDevice"); + } } } } diff --git a/services/core/java/com/android/server/audio/AudioDeviceInventory.java b/services/core/java/com/android/server/audio/AudioDeviceInventory.java index bf20ae3b516d..57b19cda7c12 100644 --- a/services/core/java/com/android/server/audio/AudioDeviceInventory.java +++ b/services/core/java/com/android/server/audio/AudioDeviceInventory.java @@ -764,7 +764,7 @@ public class AudioDeviceInventory { /** only public for mocking/spying, do not call outside of AudioService */ // @GuardedBy("mDeviceBroker.mSetModeLock") @VisibleForTesting - @GuardedBy("mDeviceBroker.mDeviceStateLock") + //@GuardedBy("AudioDeviceBroker.this.mDeviceStateLock") public void onSetBtActiveDevice(@NonNull AudioDeviceBroker.BtDeviceInfo btInfo, @AudioSystem.AudioFormatNativeEnumForBtCodec int codec, int streamType) { diff --git a/services/core/java/com/android/server/biometrics/AuthService.java b/services/core/java/com/android/server/biometrics/AuthService.java index d5d8fd22314b..8fd2ee2bdc33 100644 --- a/services/core/java/com/android/server/biometrics/AuthService.java +++ b/services/core/java/com/android/server/biometrics/AuthService.java @@ -20,6 +20,7 @@ package com.android.server.biometrics; // TODO(b/141025588): Create separate internal and external permissions for AuthService. // TODO(b/141025588): Get rid of the USE_FINGERPRINT permission. +import static android.Manifest.permission.MANAGE_BIOMETRIC_DIALOG; import static android.Manifest.permission.TEST_BIOMETRIC; import static android.Manifest.permission.USE_BIOMETRIC; import static android.Manifest.permission.USE_BIOMETRIC_INTERNAL; @@ -304,6 +305,9 @@ public class AuthService extends SystemService { if (promptInfo.containsPrivateApiConfigurations()) { checkInternalPermission(); } + if (promptInfo.containsManageBioApiConfigurations()) { + checkManageBiometricPermission(); + } final long identity = Binder.clearCallingIdentity(); try { @@ -984,6 +988,11 @@ public class AuthService extends SystemService { "Must have USE_BIOMETRIC_INTERNAL permission"); } + private void checkManageBiometricPermission() { + getContext().enforceCallingOrSelfPermission(MANAGE_BIOMETRIC_DIALOG, + "Must have MANAGE_BIOMETRIC_DIALOG permission"); + } + private void checkPermission() { if (getContext().checkCallingOrSelfPermission(USE_FINGERPRINT) != PackageManager.PERMISSION_GRANTED) { diff --git a/services/core/java/com/android/server/biometrics/log/BiometricContext.java b/services/core/java/com/android/server/biometrics/log/BiometricContext.java index 3dcea19b6077..f8a98675c0c7 100644 --- a/services/core/java/com/android/server/biometrics/log/BiometricContext.java +++ b/services/core/java/com/android/server/biometrics/log/BiometricContext.java @@ -77,6 +77,9 @@ public interface BiometricContext { @AuthenticateOptions.DisplayState int getDisplayState(); + /** Gets whether touches on sensor are ignored by HAL */ + boolean isHardwareIgnoringTouches(); + /** * Subscribe to context changes. * diff --git a/services/core/java/com/android/server/biometrics/log/BiometricContextProvider.java b/services/core/java/com/android/server/biometrics/log/BiometricContextProvider.java index 95a047faef07..535b7b743625 100644 --- a/services/core/java/com/android/server/biometrics/log/BiometricContextProvider.java +++ b/services/core/java/com/android/server/biometrics/log/BiometricContextProvider.java @@ -86,8 +86,8 @@ public final class BiometricContextProvider implements BiometricContext { @Nullable private final Handler mHandler; private int mDockState = Intent.EXTRA_DOCK_STATE_UNDOCKED; private int mFoldState = IBiometricContextListener.FoldState.UNKNOWN; - private int mDisplayState = AuthenticateOptions.DISPLAY_STATE_UNKNOWN; + private boolean mIsHardwareIgnoringTouches = false; @VisibleForTesting final BroadcastReceiver mDockStateReceiver = new BroadcastReceiver() { @Override @@ -129,6 +129,14 @@ public final class BiometricContextProvider implements BiometricContext { notifyChanged(); } } + + @Override + public void onHardwareIgnoreTouchesChanged(boolean shouldIgnore) { + if (mIsHardwareIgnoringTouches != shouldIgnore) { + mIsHardwareIgnoringTouches = shouldIgnore; + notifyChanged(); + } + } }); service.registerSessionListener(SESSION_TYPES, new ISessionListener.Stub() { @Override @@ -215,6 +223,11 @@ public final class BiometricContextProvider implements BiometricContext { } @Override + public boolean isHardwareIgnoringTouches() { + return mIsHardwareIgnoringTouches; + } + + @Override public void subscribe(@NonNull OperationContextExt context, @NonNull Consumer<OperationContext> consumer) { mSubscribers.put(context, consumer); @@ -254,6 +267,7 @@ public final class BiometricContextProvider implements BiometricContext { + "bp session: " + getBiometricPromptSessionInfo() + ", " + "displayState: " + getDisplayState() + ", " + "isAwake: " + isAwake() + ", " + + "isHardwareIgnoring: " + isHardwareIgnoringTouches() + ", " + "isDisplayOn: " + isDisplayOn() + ", " + "dock: " + getDockedState() + ", " + "rotation: " + getCurrentRotation() + ", " diff --git a/services/core/java/com/android/server/biometrics/log/OperationContextExt.java b/services/core/java/com/android/server/biometrics/log/OperationContextExt.java index b4e0dff615f5..0045d44af9a1 100644 --- a/services/core/java/com/android/server/biometrics/log/OperationContextExt.java +++ b/services/core/java/com/android/server/biometrics/log/OperationContextExt.java @@ -20,12 +20,14 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.content.Intent; import android.hardware.biometrics.AuthenticateOptions; +import android.hardware.biometrics.BiometricAuthenticator; import android.hardware.biometrics.IBiometricContextListener; import android.hardware.biometrics.common.AuthenticateReason; import android.hardware.biometrics.common.DisplayState; import android.hardware.biometrics.common.FoldState; import android.hardware.biometrics.common.OperationContext; import android.hardware.biometrics.common.OperationReason; +import android.hardware.biometrics.common.OperationState; import android.hardware.biometrics.common.WakeReason; import android.hardware.face.FaceAuthenticateOptions; import android.hardware.fingerprint.FingerprintAuthenticateOptions; @@ -51,13 +53,26 @@ public class OperationContextExt { /** Create a context. */ public OperationContextExt(boolean isBP) { - this(new OperationContext(), isBP); + this(new OperationContext(), isBP, BiometricAuthenticator.TYPE_NONE); + } + + public OperationContextExt(boolean isBP, @BiometricAuthenticator.Modality int modality) { + this(new OperationContext(), isBP, modality); } /** Create a wrapped context. */ - public OperationContextExt(@NonNull OperationContext context, boolean isBP) { + public OperationContextExt(@NonNull OperationContext context, boolean isBP, + @BiometricAuthenticator.Modality int modality) { mAidlContext = context; mIsBP = isBP; + + if (modality == BiometricAuthenticator.TYPE_FINGERPRINT) { + mAidlContext.operationState = OperationState.fingerprintOperationState( + new OperationState.FingerprintOperationState()); + } else if (modality == BiometricAuthenticator.TYPE_FACE) { + mAidlContext.operationState = OperationState.faceOperationState( + new OperationState.FaceOperationState()); + } } /** @@ -247,12 +262,23 @@ public class OperationContextExt { return mOrientation; } + /** The current operation state */ + public OperationState getOperationState() { + return mAidlContext.operationState; + } + /** Update this object with the latest values from the given context. */ OperationContextExt update(@NonNull BiometricContext biometricContext, boolean isCrypto) { mAidlContext.isAod = biometricContext.isAod(); mAidlContext.displayState = toAidlDisplayState(biometricContext.getDisplayState()); mAidlContext.foldState = toAidlFoldState(biometricContext.getFoldState()); mAidlContext.isCrypto = isCrypto; + + if (mAidlContext.operationState != null && mAidlContext.operationState.getTag() + == OperationState.fingerprintOperationState) { + mAidlContext.operationState.getFingerprintOperationState().isHardwareIgnoringTouches = + biometricContext.isHardwareIgnoringTouches(); + } setFirstSessionId(biometricContext); mIsDisplayOn = biometricContext.isDisplayOn(); diff --git a/services/core/java/com/android/server/biometrics/sensors/ClientMonitorCallbackConverter.java b/services/core/java/com/android/server/biometrics/sensors/ClientMonitorCallbackConverter.java index 1a682a9ffefa..1fc7c70de8fb 100644 --- a/services/core/java/com/android/server/biometrics/sensors/ClientMonitorCallbackConverter.java +++ b/services/core/java/com/android/server/biometrics/sensors/ClientMonitorCallbackConverter.java @@ -41,22 +41,42 @@ import android.os.RemoteException; * a common interface. */ public class ClientMonitorCallbackConverter { - private IBiometricSensorReceiver mSensorReceiver; // BiometricService - private IFaceServiceReceiver mFaceServiceReceiver; // FaceManager - private IFingerprintServiceReceiver mFingerprintServiceReceiver; // FingerprintManager + private final IBiometricSensorReceiver mSensorReceiver; // BiometricService + private final IFaceServiceReceiver mFaceServiceReceiver; // FaceManager + private final IFingerprintServiceReceiver mFingerprintServiceReceiver; // FingerprintManager public ClientMonitorCallbackConverter(IBiometricSensorReceiver sensorReceiver) { mSensorReceiver = sensorReceiver; + mFaceServiceReceiver = null; + mFingerprintServiceReceiver = null; } public ClientMonitorCallbackConverter(IFaceServiceReceiver faceServiceReceiver) { + mSensorReceiver = null; mFaceServiceReceiver = faceServiceReceiver; + mFingerprintServiceReceiver = null; } public ClientMonitorCallbackConverter(IFingerprintServiceReceiver fingerprintServiceReceiver) { + mSensorReceiver = null; + mFaceServiceReceiver = null; mFingerprintServiceReceiver = fingerprintServiceReceiver; } + /** + * Returns an int representing the {@link BiometricAuthenticator.Modality} of the active + * ServiceReceiver + */ + @BiometricAuthenticator.Modality + public int getModality() { + if (mFaceServiceReceiver != null) { + return BiometricAuthenticator.TYPE_FACE; + } else if (mFingerprintServiceReceiver != null) { + return BiometricAuthenticator.TYPE_FINGERPRINT; + } + return BiometricAuthenticator.TYPE_NONE; + } + // The following apply to all clients public void onAcquired(int sensorId, int acquiredInfo, int vendorCode) throws RemoteException { diff --git a/services/core/java/com/android/server/biometrics/sensors/HalClientMonitor.java b/services/core/java/com/android/server/biometrics/sensors/HalClientMonitor.java index 03658ce21fc2..0f01510bd908 100644 --- a/services/core/java/com/android/server/biometrics/sensors/HalClientMonitor.java +++ b/services/core/java/com/android/server/biometrics/sensors/HalClientMonitor.java @@ -19,6 +19,7 @@ package com.android.server.biometrics.sensors; import android.annotation.NonNull; import android.annotation.Nullable; import android.content.Context; +import android.hardware.biometrics.BiometricAuthenticator; import android.os.IBinder; import com.android.server.biometrics.log.BiometricContext; @@ -58,7 +59,8 @@ public abstract class HalClientMonitor<T> extends BaseClientMonitor { super(context, token, listener, userId, owner, cookie, sensorId, biometricLogger, biometricContext); mLazyDaemon = lazyDaemon; - mOperationContext = new OperationContextExt(isBiometricPrompt()); + int modality = listener != null ? listener.getModality() : BiometricAuthenticator.TYPE_NONE; + mOperationContext = new OperationContextExt(isBiometricPrompt(), modality); } @Nullable 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 29c5a3de3a80..145885de5c32 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 @@ -28,6 +28,7 @@ import android.hardware.biometrics.BiometricFingerprintConstants; import android.hardware.biometrics.BiometricFingerprintConstants.FingerprintAcquired; import android.hardware.biometrics.BiometricManager.Authenticators; import android.hardware.biometrics.common.ICancellationSignal; +import android.hardware.biometrics.common.OperationState; import android.hardware.biometrics.fingerprint.PointerContext; import android.hardware.fingerprint.FingerprintAuthenticateOptions; import android.hardware.fingerprint.FingerprintManager; @@ -326,6 +327,12 @@ public class FingerprintAuthenticationClient if (session.hasContextMethods()) { try { session.getSession().onContextChanged(ctx); + // TODO(b/317414324): Deprecate setIgnoreDisplayTouches + if (ctx.operationState != null && ctx.operationState.getTag() + == OperationState.fingerprintOperationState) { + session.getSession().setIgnoreDisplayTouches( + ctx.operationState.getFingerprintOperationState().isHardwareIgnoringTouches); + } } catch (RemoteException e) { Slog.e(TAG, "Unable to notify context changed", e); } diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintDetectClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintDetectClient.java index e58e5ae117b5..3aab7b300a87 100644 --- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintDetectClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintDetectClient.java @@ -23,6 +23,7 @@ import android.annotation.Nullable; import android.content.Context; import android.hardware.biometrics.BiometricRequestConstants; import android.hardware.biometrics.common.ICancellationSignal; +import android.hardware.biometrics.common.OperationState; import android.hardware.fingerprint.FingerprintAuthenticateOptions; import android.hardware.fingerprint.IUdfpsOverlayController; import android.os.IBinder; @@ -122,6 +123,12 @@ public class FingerprintDetectClient extends AcquisitionClient<AidlSession> getBiometricContext().subscribe(opContext, ctx -> { try { session.getSession().onContextChanged(ctx); + // TODO(b/317414324): Deprecate setIgnoreDisplayTouches + if (ctx.operationState != null && ctx.operationState.getTag() + == OperationState.fingerprintOperationState) { + session.getSession().setIgnoreDisplayTouches( + ctx.operationState.getFingerprintOperationState().isHardwareIgnoringTouches); + } } catch (RemoteException e) { Slog.e(TAG, "Unable to notify context changed", e); } diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintEnrollClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintEnrollClient.java index c0761ed8f32b..bf5011de1e59 100644 --- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintEnrollClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintEnrollClient.java @@ -26,6 +26,7 @@ import android.hardware.biometrics.BiometricFingerprintConstants; import android.hardware.biometrics.BiometricFingerprintConstants.FingerprintAcquired; import android.hardware.biometrics.BiometricStateListener; import android.hardware.biometrics.common.ICancellationSignal; +import android.hardware.biometrics.common.OperationState; import android.hardware.biometrics.fingerprint.PointerContext; import android.hardware.fingerprint.Fingerprint; import android.hardware.fingerprint.FingerprintManager; @@ -220,6 +221,12 @@ public class FingerprintEnrollClient extends EnrollClient<AidlSession> implement getBiometricContext().subscribe(opContext, ctx -> { try { session.getSession().onContextChanged(ctx); + // TODO(b/317414324): Deprecate setIgnoreDisplayTouches + if (ctx.operationState != null && ctx.operationState.getTag() + == OperationState.fingerprintOperationState) { + session.getSession().setIgnoreDisplayTouches( + ctx.operationState.getFingerprintOperationState().isHardwareIgnoringTouches); + } } catch (RemoteException e) { Slog.e(TAG, "Unable to notify context changed", e); } diff --git a/services/core/java/com/android/server/broadcastradio/TEST_MAPPING b/services/core/java/com/android/server/broadcastradio/TEST_MAPPING new file mode 100644 index 000000000000..ee4eeb634c84 --- /dev/null +++ b/services/core/java/com/android/server/broadcastradio/TEST_MAPPING @@ -0,0 +1,7 @@ +{ + "imports": [ + { + "path": "frameworks/base/core/tests/BroadcastRadioTests" + } + ] +} diff --git a/services/core/java/com/android/server/companion/virtual/VirtualDeviceManagerInternal.java b/services/core/java/com/android/server/companion/virtual/VirtualDeviceManagerInternal.java index 823788f0b249..b17978370bd7 100644 --- a/services/core/java/com/android/server/companion/virtual/VirtualDeviceManagerInternal.java +++ b/services/core/java/com/android/server/companion/virtual/VirtualDeviceManagerInternal.java @@ -137,9 +137,9 @@ public abstract class VirtualDeviceManagerInternal { public abstract boolean isAppRunningOnAnyVirtualDevice(int uid); /** - * Returns true if the {@code displayId} is owned by any virtual device + * @return whether the input device with the given id was created by a virtual device. */ - public abstract boolean isDisplayOwnedByAnyVirtualDevice(int displayId); + public abstract boolean isInputDeviceOwnedByVirtualDevice(int inputDeviceId); /** * Gets the ids of VirtualDisplays owned by a VirtualDevice. diff --git a/services/core/java/com/android/server/display/AutomaticBrightnessController.java b/services/core/java/com/android/server/display/AutomaticBrightnessController.java index 8910b6e58432..082776ad6085 100644 --- a/services/core/java/com/android/server/display/AutomaticBrightnessController.java +++ b/services/core/java/com/android/server/display/AutomaticBrightnessController.java @@ -624,10 +624,10 @@ public class AutomaticBrightnessController { pw.println(" Current mode=" + autoBrightnessModeToString(mCurrentBrightnessMapper.getMode())); - pw.println(); for (int i = 0; i < mBrightnessMappingStrategyMap.size(); i++) { + pw.println(); pw.println(" Mapper for mode " - + autoBrightnessModeToString(mBrightnessMappingStrategyMap.keyAt(i)) + "="); + + autoBrightnessModeToString(mBrightnessMappingStrategyMap.keyAt(i)) + ":"); mBrightnessMappingStrategyMap.valueAt(i).dump(pw, mBrightnessRangeController.getNormalBrightnessMax()); } @@ -1159,7 +1159,7 @@ public class AutomaticBrightnessController { if (mCurrentBrightnessMapper.getMode() == mode) { return; } - Slog.i(TAG, "Switching to mode " + mode); + Slog.i(TAG, "Switching to mode " + autoBrightnessModeToString(mode)); if (mode == AUTO_BRIGHTNESS_MODE_IDLE || mCurrentBrightnessMapper.getMode() == AUTO_BRIGHTNESS_MODE_IDLE) { switchModeAndShortTermModels(mode); diff --git a/services/core/java/com/android/server/display/config/DisplayBrightnessMappingConfig.java b/services/core/java/com/android/server/display/config/DisplayBrightnessMappingConfig.java index 544f490913e2..e0bdda511df3 100644 --- a/services/core/java/com/android/server/display/config/DisplayBrightnessMappingConfig.java +++ b/services/core/java/com/android/server/display/config/DisplayBrightnessMappingConfig.java @@ -165,8 +165,15 @@ public class DisplayBrightnessMappingConfig { */ public float[] getLuxArray(@AutomaticBrightnessController.AutomaticBrightnessMode int mode, int preset) { - return mBrightnessLevelsLuxMap.get( + float[] luxArray = mBrightnessLevelsLuxMap.get( autoBrightnessModeToString(mode) + "_" + autoBrightnessPresetToString(preset)); + if (luxArray != null) { + return luxArray; + } + + // No array for this preset, fall back to the normal preset + return mBrightnessLevelsLuxMap.get(autoBrightnessModeToString(mode) + "_" + + AutoBrightnessSettingName.normal.getRawName()); } /** @@ -184,8 +191,15 @@ public class DisplayBrightnessMappingConfig { */ public float[] getBrightnessArray( @AutomaticBrightnessController.AutomaticBrightnessMode int mode, int preset) { - return mBrightnessLevelsMap.get( + float[] brightnessArray = mBrightnessLevelsMap.get( autoBrightnessModeToString(mode) + "_" + autoBrightnessPresetToString(preset)); + if (brightnessArray != null) { + return brightnessArray; + } + + // No array for this preset, fall back to the normal preset + return mBrightnessLevelsMap.get(autoBrightnessModeToString(mode) + "_" + + AutoBrightnessSettingName.normal.getRawName()); } @Override diff --git a/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java b/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java index be48eb437dfe..1ae255933f66 100644 --- a/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java +++ b/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java @@ -100,6 +100,10 @@ public class DisplayManagerFlags { Flags.FLAG_ENABLE_VSYNC_LOW_POWER_VOTE, Flags::enableVsyncLowPowerVote); + private final FlagState mVsyncLowLightVote = new FlagState( + Flags.FLAG_ENABLE_VSYNC_LOW_LIGHT_VOTE, + Flags::enableVsyncLowLightVote); + private final FlagState mBrightnessWearBedtimeModeClamperFlagState = new FlagState( Flags.FLAG_BRIGHTNESS_WEAR_BEDTIME_MODE_CLAMPER, Flags::brightnessWearBedtimeModeClamper); @@ -220,6 +224,10 @@ public class DisplayManagerFlags { return mVsyncLowPowerVote.isEnabled(); } + public boolean isVsyncLowLightVoteEnabled() { + return mVsyncLowLightVote.isEnabled(); + } + public boolean isBrightnessWearBedtimeModeClamperEnabled() { return mBrightnessWearBedtimeModeClamperFlagState.isEnabled(); } diff --git a/services/core/java/com/android/server/display/feature/display_flags.aconfig b/services/core/java/com/android/server/display/feature/display_flags.aconfig index a2319a8a7c07..c2f52b5ad8a0 100644 --- a/services/core/java/com/android/server/display/feature/display_flags.aconfig +++ b/services/core/java/com/android/server/display/feature/display_flags.aconfig @@ -146,6 +146,14 @@ flag { } flag { + name: "enable_vsync_low_light_vote" + namespace: "display_manager" + description: "Feature flag for vsync low light vote" + bug: "314921657" + is_fixed_read_only: true +} + +flag { name: "brightness_wear_bedtime_mode_clamper" namespace: "display_manager" description: "Feature flag for the Wear Bedtime mode brightness clamper" diff --git a/services/core/java/com/android/server/display/mode/DisplayModeDirector.java b/services/core/java/com/android/server/display/mode/DisplayModeDirector.java index ad3deffb9590..8707000ce962 100644 --- a/services/core/java/com/android/server/display/mode/DisplayModeDirector.java +++ b/services/core/java/com/android/server/display/mode/DisplayModeDirector.java @@ -215,7 +215,8 @@ public class DisplayModeDirector { mDeviceConfigDisplaySettings = new DeviceConfigDisplaySettings(); mSettingsObserver = new SettingsObserver(context, handler, mDvrrSupported, displayManagerFlags); - mBrightnessObserver = new BrightnessObserver(context, handler, injector); + mBrightnessObserver = new BrightnessObserver(context, handler, injector, mDvrrSupported, + displayManagerFlags); mDefaultDisplayDeviceConfig = null; mUdfpsObserver = new UdfpsObserver(); mVotesStorage = new VotesStorage(this::notifyDesiredDisplayModeSpecsChangedLocked, @@ -1510,6 +1511,8 @@ public class DisplayModeDirector { private final Injector mInjector; private final Handler mHandler; + private final boolean mVsyncLowLightBlockingVoteEnabled; + private final IThermalEventListener.Stub mThermalListener = new IThermalEventListener.Stub() { @Override @@ -1544,7 +1547,8 @@ public class DisplayModeDirector { @GuardedBy("mLock") private @Temperature.ThrottlingStatus int mThermalStatus = Temperature.THROTTLING_NONE; - BrightnessObserver(Context context, Handler handler, Injector injector) { + BrightnessObserver(Context context, Handler handler, Injector injector, + boolean dvrrSupported , DisplayManagerFlags flags) { mContext = context; mHandler = handler; mInjector = injector; @@ -1552,6 +1556,7 @@ public class DisplayModeDirector { /* attemptReadFromFeatureParams= */ false); mRefreshRateInHighZone = context.getResources().getInteger( R.integer.config_fixedRefreshRateInHighZone); + mVsyncLowLightBlockingVoteEnabled = dvrrSupported && flags.isVsyncLowLightVoteEnabled(); } /** @@ -2131,7 +2136,17 @@ public class DisplayModeDirector { Vote.forPhysicalRefreshRates(range.min, range.max); } } - refreshRateSwitchingVote = Vote.forDisableRefreshRateSwitching(); + + if (mVsyncLowLightBlockingVoteEnabled) { + refreshRateSwitchingVote = Vote.forSupportedModesAndDisableRefreshRateSwitching( + List.of( + new SupportedModesVote.SupportedMode( + /* peakRefreshRate= */ 60f, /* vsyncRate= */ 60f), + new SupportedModesVote.SupportedMode( + /* peakRefreshRate= */120f, /* vsyncRate= */ 120f))); + } else { + refreshRateSwitchingVote = Vote.forDisableRefreshRateSwitching(); + } } boolean insideHighZone = hasValidHighZone() diff --git a/services/core/java/com/android/server/display/mode/Vote.java b/services/core/java/com/android/server/display/mode/Vote.java index 8f3957011b59..e8d5a194f8f4 100644 --- a/services/core/java/com/android/server/display/mode/Vote.java +++ b/services/core/java/com/android/server/display/mode/Vote.java @@ -170,6 +170,13 @@ interface Vote { return new SupportedModesVote(supportedModes); } + + static Vote forSupportedModesAndDisableRefreshRateSwitching( + List<SupportedModesVote.SupportedMode> supportedModes) { + return new CombinedVote( + List.of(forDisableRefreshRateSwitching(), forSupportedModes(supportedModes))); + } + static String priorityToString(int priority) { switch (priority) { case PRIORITY_APP_REQUEST_BASE_MODE_REFRESH_RATE: diff --git a/services/core/java/com/android/server/display/mode/VotesStatsReporter.java b/services/core/java/com/android/server/display/mode/VotesStatsReporter.java index a30c4d2b5b0b..e80b9451dd14 100644 --- a/services/core/java/com/android/server/display/mode/VotesStatsReporter.java +++ b/services/core/java/com/android/server/display/mode/VotesStatsReporter.java @@ -16,12 +16,19 @@ package com.android.server.display.mode; +import static com.android.internal.util.FrameworkStatsLog.DISPLAY_MODE_DIRECTOR_VOTE_CHANGED; +import static com.android.internal.util.FrameworkStatsLog.DISPLAY_MODE_DIRECTOR_VOTE_CHANGED__VOTE_STATUS__STATUS_ACTIVE; +import static com.android.internal.util.FrameworkStatsLog.DISPLAY_MODE_DIRECTOR_VOTE_CHANGED__VOTE_STATUS__STATUS_ADDED; +import static com.android.internal.util.FrameworkStatsLog.DISPLAY_MODE_DIRECTOR_VOTE_CHANGED__VOTE_STATUS__STATUS_REMOVED; + import android.annotation.NonNull; import android.annotation.Nullable; import android.os.Trace; import android.util.SparseArray; import android.view.Display; +import com.android.internal.util.FrameworkStatsLog; + /** * The VotesStatsReporter is responsible for collecting and sending Vote related statistics */ @@ -31,42 +38,77 @@ class VotesStatsReporter { private final boolean mIgnoredRenderRate; private final boolean mFrameworkStatsLogReportingEnabled; + private int mLastMinPriorityReported = Vote.MAX_PRIORITY + 1; + public VotesStatsReporter(boolean ignoreRenderRate, boolean refreshRateVotingTelemetryEnabled) { mIgnoredRenderRate = ignoreRenderRate; mFrameworkStatsLogReportingEnabled = refreshRateVotingTelemetryEnabled; } - void reportVoteAdded(int displayId, int priority, @NonNull Vote vote) { + void reportVoteChanged(int displayId, int priority, @Nullable Vote vote) { + if (vote == null) { + reportVoteRemoved(displayId, priority); + } else { + reportVoteAdded(displayId, priority, vote); + } + } + + private void reportVoteAdded(int displayId, int priority, @NonNull Vote vote) { int maxRefreshRate = getMaxRefreshRate(vote, mIgnoredRenderRate); Trace.traceCounter(Trace.TRACE_TAG_POWER, TAG + "." + displayId + ":" + Vote.priorityToString(priority), maxRefreshRate); - // if ( mFrameworkStatsLogReportingEnabled) { - // FrameworkStatsLog.write(VOTE_CHANGED, displayID, priority, ADDED, maxRefreshRate, -1); - // } + if (mFrameworkStatsLogReportingEnabled) { + FrameworkStatsLog.write( + DISPLAY_MODE_DIRECTOR_VOTE_CHANGED, displayId, priority, + DISPLAY_MODE_DIRECTOR_VOTE_CHANGED__VOTE_STATUS__STATUS_ADDED, + maxRefreshRate, -1); + } } - void reportVoteRemoved(int displayId, int priority) { + private void reportVoteRemoved(int displayId, int priority) { Trace.traceCounter(Trace.TRACE_TAG_POWER, TAG + "." + displayId + ":" + Vote.priorityToString(priority), -1); - // if ( mFrameworkStatsLogReportingEnabled) { - // FrameworkStatsLog.write(VOTE_CHANGED, displayID, priority, REMOVED, -1, -1); - // } + if (mFrameworkStatsLogReportingEnabled) { + FrameworkStatsLog.write( + DISPLAY_MODE_DIRECTOR_VOTE_CHANGED, displayId, priority, + DISPLAY_MODE_DIRECTOR_VOTE_CHANGED__VOTE_STATUS__STATUS_REMOVED, -1, -1); + } } void reportVotesActivated(int displayId, int minPriority, @Nullable Display.Mode baseMode, SparseArray<Vote> votes) { -// if (!mFrameworkStatsLogReportingEnabled) { -// return; -// } -// int selectedRefreshRate = baseMode != null ? (int) baseMode.getRefreshRate() : -1; -// for (int priority = minPriority; priority <= Vote.MAX_PRIORITY; priority ++) { -// Vote vote = votes.get(priority); -// if (vote != null) { -// int maxRefreshRate = getMaxRefreshRate(vote, mIgnoredRenderRate); -// FrameworkStatsLog.write(VOTE_CHANGED, displayId, priority, -// ACTIVE, maxRefreshRate, selectedRefreshRate); -// } -// } + if (!mFrameworkStatsLogReportingEnabled) { + return; + } + int selectedRefreshRate = baseMode != null ? (int) baseMode.getRefreshRate() : -1; + for (int priority = Vote.MIN_PRIORITY; priority <= Vote.MAX_PRIORITY; priority++) { + if (priority < mLastMinPriorityReported && priority < minPriority) { + continue; + } + Vote vote = votes.get(priority); + if (vote == null) { + continue; + } + + // Was previously reported ACTIVE, changed to ADDED + if (priority >= mLastMinPriorityReported && priority < minPriority) { + int maxRefreshRate = getMaxRefreshRate(vote, mIgnoredRenderRate); + FrameworkStatsLog.write( + DISPLAY_MODE_DIRECTOR_VOTE_CHANGED, displayId, priority, + DISPLAY_MODE_DIRECTOR_VOTE_CHANGED__VOTE_STATUS__STATUS_ADDED, + maxRefreshRate, selectedRefreshRate); + } + // Was previously reported ADDED, changed to ACTIVE + if (priority >= minPriority && priority < mLastMinPriorityReported) { + int maxRefreshRate = getMaxRefreshRate(vote, mIgnoredRenderRate); + FrameworkStatsLog.write( + DISPLAY_MODE_DIRECTOR_VOTE_CHANGED, displayId, priority, + DISPLAY_MODE_DIRECTOR_VOTE_CHANGED__VOTE_STATUS__STATUS_ACTIVE, + maxRefreshRate, selectedRefreshRate); + } + + mLastMinPriorityReported = minPriority; + } } private static int getMaxRefreshRate(@NonNull Vote vote, boolean ignoreRenderRate) { diff --git a/services/core/java/com/android/server/display/mode/VotesStorage.java b/services/core/java/com/android/server/display/mode/VotesStorage.java index 7a1f7e9d857c..56c7c18c0a11 100644 --- a/services/core/java/com/android/server/display/mode/VotesStorage.java +++ b/services/core/java/com/android/server/display/mode/VotesStorage.java @@ -117,22 +117,13 @@ class VotesStorage { Slog.i(TAG, "Updated votes for display=" + displayId + " votes=" + votes); } if (changed) { - reportVoteStats(displayId, priority, vote); + if (mVotesStatsReporter != null) { + mVotesStatsReporter.reportVoteChanged(displayId, priority, vote); + } mListener.onChanged(); } } - private void reportVoteStats(int displayId, int priority, @Nullable Vote vote) { - if (mVotesStatsReporter == null) { - return; - } - if (vote == null) { - mVotesStatsReporter.reportVoteRemoved(displayId, priority); - } else { - mVotesStatsReporter.reportVoteAdded(displayId, priority, vote); - } - } - /** dump class values, for debugging */ void dump(@NonNull PrintWriter pw) { SparseArray<SparseArray<Vote>> votesByDisplayLocal = new SparseArray<>(); diff --git a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java index 1cd267dee2fe..d34661d4d6ac 100644 --- a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java +++ b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java @@ -1290,15 +1290,19 @@ public final class HdmiCecLocalDeviceTv extends HdmiCecLocalDevice { mService.getHdmiCecNetwork().removeCecSwitches(portId); } - // Turning System Audio Mode off when the AVR is unlugged or standby. - // When the device is not unplugged but reawaken from standby, we check if the System - // Audio Control Feature is enabled or not then decide if turning SAM on/off accordingly. - if (getAvrDeviceInfo() != null && portId == getAvrDeviceInfo().getPortId()) { - HdmiLogger.debug("Port ID:%d, 5v=%b", portId, connected); - if (!connected) { - setSystemAudioMode(false); - } else { - onNewAvrAdded(getAvrDeviceInfo()); + if (!mService.isEarcEnabled() || !mService.isEarcSupported()) { + HdmiDeviceInfo avr = getAvrDeviceInfo(); + if (avr != null + && portId == avr.getPortId() + && isConnectedToArcPort(avr.getPhysicalAddress())) { + HdmiLogger.debug("Port ID:%d, 5v=%b", portId, connected); + if (connected) { + if (mArcEstablished) { + enableAudioReturnChannel(true); + } + } else { + enableAudioReturnChannel(false); + } } } diff --git a/services/core/java/com/android/server/hdmi/HdmiControlService.java b/services/core/java/com/android/server/hdmi/HdmiControlService.java index eaf754dc7520..e0e825d9147a 100644 --- a/services/core/java/com/android/server/hdmi/HdmiControlService.java +++ b/services/core/java/com/android/server/hdmi/HdmiControlService.java @@ -3617,7 +3617,7 @@ public class HdmiControlService extends SystemService { } } - @VisibleForTesting + @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) protected boolean isEarcSupported() { synchronized (mLock) { return mEarcSupported; diff --git a/services/core/java/com/android/server/hdmi/NewDeviceAction.java b/services/core/java/com/android/server/hdmi/NewDeviceAction.java index f3532e5ce7e9..b6c0e5d970a1 100644 --- a/services/core/java/com/android/server/hdmi/NewDeviceAction.java +++ b/services/core/java/com/android/server/hdmi/NewDeviceAction.java @@ -53,6 +53,7 @@ final class NewDeviceAction extends HdmiCecFeatureAction { private int mVendorId; private String mDisplayName; private int mTimeoutRetry; + private HdmiDeviceInfo mOldDeviceInfo; /** * Constructor. @@ -73,6 +74,38 @@ final class NewDeviceAction extends HdmiCecFeatureAction { @Override public boolean start() { + mOldDeviceInfo = + localDevice().mService.getHdmiCecNetwork().getCecDeviceInfo(mDeviceLogicalAddress); + // If there's deviceInfo with same (logical address, physical address) set + // Then addCecDevice should be delayed until system information process is finished + if (mOldDeviceInfo != null + && mOldDeviceInfo.getPhysicalAddress() == mDevicePhysicalAddress) { + Slog.d(TAG, "Start NewDeviceAction with old deviceInfo:[" + + mOldDeviceInfo.toString() + "]"); + } else { + // Add the device ahead with default information to handle <Active Source> + // promptly, rather than waiting till the new device action is finished. + Slog.d(TAG, "Start NewDeviceAction with default deviceInfo"); + HdmiDeviceInfo deviceInfo = HdmiDeviceInfo.cecDeviceBuilder() + .setLogicalAddress(mDeviceLogicalAddress) + .setPhysicalAddress(mDevicePhysicalAddress) + .setPortId(tv().getPortId(mDevicePhysicalAddress)) + .setDeviceType(mDeviceType) + .setVendorId(Constants.VENDOR_ID_UNKNOWN) + .build(); + // If a deviceInfo with same logical address but different physical address exists + // We should remove the old deviceInfo first + // This will happen if the interval between unplugging and plugging device is too short + // and HotplugDetection Action fails to remove the old deviceInfo, or when the newly + // plugged device violates HDMI Spec and uses an occupied logical address + if (mOldDeviceInfo != null) { + Slog.d(TAG, "Remove device by NewDeviceAction, logical address conflicts: " + + mDevicePhysicalAddress); + localDevice().mService.getHdmiCecNetwork().removeCecDevice( + localDevice(), mDeviceLogicalAddress); + } + localDevice().mService.getHdmiCecNetwork().addCecDevice(deviceInfo); + } requestOsdName(true); return true; } @@ -182,14 +215,30 @@ final class NewDeviceAction extends HdmiCecFeatureAction { .setVendorId(mVendorId) .setDisplayName(mDisplayName) .build(); - localDevice().mService.getHdmiCecNetwork().updateCecDevice(deviceInfo); - // Consume CEC messages we already got for this newly found device. - tv().processDelayedMessages(mDeviceLogicalAddress); + // Check if oldDevice is same as newDevice + // If so, don't add newDevice info, preventing ARC or HDMI source re-connection + if (mOldDeviceInfo != null + && mOldDeviceInfo.getLogicalAddress() == mDeviceLogicalAddress + && mOldDeviceInfo.getPhysicalAddress() == mDevicePhysicalAddress + && mOldDeviceInfo.getDeviceType() == mDeviceType + && mOldDeviceInfo.getVendorId() == mVendorId + && mOldDeviceInfo.getDisplayName().equals(mDisplayName)) { + // Consume CEC messages we already got for this newly found device. + tv().processDelayedMessages(mDeviceLogicalAddress); + Slog.d(TAG, "Ignore NewDevice, deviceInfo is same as current device"); + Slog.d(TAG, "Old:[" + mOldDeviceInfo.toString() + + "]; New:[" + deviceInfo.toString() + "]"); + } else { + Slog.d(TAG, "Add NewDevice:[" + deviceInfo.toString() + "]"); + localDevice().mService.getHdmiCecNetwork().addCecDevice(deviceInfo); - if (HdmiUtils.isEligibleAddressForDevice(HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM, - mDeviceLogicalAddress)) { - tv().onNewAvrAdded(deviceInfo); + // Consume CEC messages we already got for this newly found device. + tv().processDelayedMessages(mDeviceLogicalAddress); + if (HdmiUtils.isEligibleAddressForDevice(HdmiDeviceInfo.DEVICE_AUDIO_SYSTEM, + mDeviceLogicalAddress)) { + tv().onNewAvrAdded(deviceInfo); + } } } diff --git a/services/core/java/com/android/server/health/HealthServiceWrapperAidl.java b/services/core/java/com/android/server/health/HealthServiceWrapperAidl.java index 1153cc37e3da..8a3a56cdc9ca 100644 --- a/services/core/java/com/android/server/health/HealthServiceWrapperAidl.java +++ b/services/core/java/com/android/server/health/HealthServiceWrapperAidl.java @@ -16,6 +16,8 @@ package com.android.server.health; +import static android.os.Flags.batteryPartStatusApi; + import android.annotation.NonNull; import android.annotation.Nullable; import android.hardware.health.BatteryHealthData; @@ -150,6 +152,18 @@ class HealthServiceWrapperAidl extends HealthServiceWrapper { healthData = service.getBatteryHealthData(); prop.setLong(healthData.batteryStateOfHealth); break; + case BatteryManager.BATTERY_PROPERTY_SERIAL_NUMBER: + if (batteryPartStatusApi()) { + healthData = service.getBatteryHealthData(); + prop.setString(healthData.batterySerialNumber); + } + break; + case BatteryManager.BATTERY_PROPERTY_PART_STATUS: + if (batteryPartStatusApi()) { + healthData = service.getBatteryHealthData(); + prop.setLong(healthData.batteryPartStatus); + } + break; } } catch (UnsupportedOperationException e) { // Leave prop untouched. diff --git a/services/core/java/com/android/server/input/KeyboardLayoutManager.java b/services/core/java/com/android/server/input/KeyboardLayoutManager.java index 8580b9664075..46668de042d4 100644 --- a/services/core/java/com/android/server/input/KeyboardLayoutManager.java +++ b/services/core/java/com/android/server/input/KeyboardLayoutManager.java @@ -76,6 +76,8 @@ import com.android.internal.inputmethod.InputMethodSubtypeHandle; import com.android.internal.messages.nano.SystemMessageProto; import com.android.internal.notification.SystemNotificationChannels; import com.android.internal.util.XmlUtils; +import com.android.server.LocalServices; +import com.android.server.companion.virtual.VirtualDeviceManagerInternal; import com.android.server.input.KeyboardMetricsCollector.KeyboardConfigurationEvent; import com.android.server.input.KeyboardMetricsCollector.LayoutSelectionCriteria; import com.android.server.inputmethod.InputMethodManagerInternal; @@ -197,7 +199,7 @@ class KeyboardLayoutManager implements InputManager.InputDeviceListener { final KeyboardIdentifier keyboardIdentifier = new KeyboardIdentifier(inputDevice); KeyboardConfiguration config = mConfiguredKeyboards.get(deviceId); if (config == null) { - config = new KeyboardConfiguration(); + config = new KeyboardConfiguration(deviceId); mConfiguredKeyboards.put(deviceId, config); } @@ -1093,19 +1095,26 @@ class KeyboardLayoutManager implements InputManager.InputDeviceListener { @MainThread private void maybeUpdateNotification() { - if (mConfiguredKeyboards.size() == 0) { - hideKeyboardLayoutNotification(); - return; - } + List<KeyboardConfiguration> configurations = new ArrayList<>(); for (int i = 0; i < mConfiguredKeyboards.size(); i++) { + int deviceId = mConfiguredKeyboards.keyAt(i); + KeyboardConfiguration config = mConfiguredKeyboards.valueAt(i); + if (isVirtualDevice(deviceId)) { + continue; + } // If we have a keyboard with no selected layouts, we should always show missing // layout notification even if there are other keyboards that are configured properly. - if (!mConfiguredKeyboards.valueAt(i).hasConfiguredLayouts()) { + if (!config.hasConfiguredLayouts()) { showMissingKeyboardLayoutNotification(); return; } + configurations.add(config); } - showConfiguredKeyboardLayoutNotification(); + if (configurations.size() == 0) { + hideKeyboardLayoutNotification(); + return; + } + showConfiguredKeyboardLayoutNotification(configurations); } @MainThread @@ -1185,10 +1194,11 @@ class KeyboardLayoutManager implements InputManager.InputDeviceListener { } @MainThread - private void showConfiguredKeyboardLayoutNotification() { + private void showConfiguredKeyboardLayoutNotification( + List<KeyboardConfiguration> configurations) { final Resources r = mContext.getResources(); - if (mConfiguredKeyboards.size() != 1) { + if (configurations.size() != 1) { showKeyboardLayoutNotification( r.getString(R.string.keyboard_layout_notification_multiple_selected_title), r.getString(R.string.keyboard_layout_notification_multiple_selected_message), @@ -1196,8 +1206,8 @@ class KeyboardLayoutManager implements InputManager.InputDeviceListener { return; } - final InputDevice inputDevice = getInputDevice(mConfiguredKeyboards.keyAt(0)); - final KeyboardConfiguration config = mConfiguredKeyboards.valueAt(0); + final KeyboardConfiguration config = configurations.get(0); + final InputDevice inputDevice = getInputDevice(config.getDeviceId()); if (inputDevice == null || !config.hasConfiguredLayouts()) { return; } @@ -1356,6 +1366,13 @@ class KeyboardLayoutManager implements InputManager.InputDeviceListener { return false; } + @VisibleForTesting + public boolean isVirtualDevice(int deviceId) { + VirtualDeviceManagerInternal vdm = LocalServices.getService( + VirtualDeviceManagerInternal.class); + return vdm != null && vdm.isInputDeviceOwnedByVirtualDevice(deviceId); + } + private static int[] getScriptCodes(@Nullable Locale locale) { if (locale == null) { return new int[0]; @@ -1430,11 +1447,22 @@ class KeyboardLayoutManager implements InputManager.InputDeviceListener { } private static class KeyboardConfiguration { + // If null or empty, it means no layout is configured for the device. And user needs to // manually set up the device. @Nullable private Set<String> mConfiguredLayouts; + private final int mDeviceId; + + private KeyboardConfiguration(int deviceId) { + mDeviceId = deviceId; + } + + private int getDeviceId() { + return mDeviceId; + } + private boolean hasConfiguredLayouts() { return mConfiguredLayouts != null && !mConfiguredLayouts.isEmpty(); } diff --git a/services/core/java/com/android/server/inputmethod/ClientController.java b/services/core/java/com/android/server/inputmethod/ClientController.java index 293464054fdc..21b952bb7760 100644 --- a/services/core/java/com/android/server/inputmethod/ClientController.java +++ b/services/core/java/com/android/server/inputmethod/ClientController.java @@ -25,9 +25,13 @@ import android.util.SparseArray; import android.view.inputmethod.InputBinding; import com.android.internal.annotations.GuardedBy; +import com.android.internal.annotations.VisibleForTesting; import com.android.internal.inputmethod.IInputMethodClient; import com.android.internal.inputmethod.IRemoteInputConnection; +import java.util.ArrayList; +import java.util.List; + /** * Store and manage {@link InputMethodManagerService} clients. This class was designed to be a * singleton in {@link InputMethodManagerService} since it stores information about all clients, @@ -37,9 +41,7 @@ import com.android.internal.inputmethod.IRemoteInputConnection; * As part of the re-architecture plan (described in go/imms-rearchitecture-plan), the following * fields and methods will be moved out from IMMS and placed here: * <ul> - * <li>mCurClient (ClientState)</li> * <li>mClients (ArrayMap of ClientState indexed by IBinder)</li> - * <li>mLastSwitchUserId</li> * </ul> * <p> * Nested Classes (to move from IMMS): @@ -54,7 +56,6 @@ import com.android.internal.inputmethod.IRemoteInputConnection; * <li>removeClient</li> * <li>verifyClientAndPackageMatch</li> * <li>setImeTraceEnabledForAllClients (make it reactive)</li> - * <li>unbindCurrentClient</li> * </ul> */ // TODO(b/314150112): Update the Javadoc above, by removing the re-architecture steps, once this @@ -65,18 +66,32 @@ final class ClientController { @GuardedBy("ImfLock.class") final ArrayMap<IBinder, ClientState> mClients = new ArrayMap<>(); + @GuardedBy("ImfLock.class") + private final List<ClientControllerCallback> mCallbacks = new ArrayList<>(); + private final PackageManagerInternal mPackageManagerInternal; + interface ClientControllerCallback { + + void onClientRemoved(ClientState client); + } + ClientController(PackageManagerInternal packageManagerInternal) { mPackageManagerInternal = packageManagerInternal; } @GuardedBy("ImfLock.class") - void addClient(IInputMethodClientInvoker clientInvoker, - IRemoteInputConnection inputConnection, - int selfReportedDisplayId, IBinder.DeathRecipient deathRecipient, int callerUid, + ClientState addClient(IInputMethodClientInvoker clientInvoker, + IRemoteInputConnection inputConnection, int selfReportedDisplayId, int callerUid, int callerPid) { - // TODO: Optimize this linear search. + final IBinder.DeathRecipient deathRecipient = () -> { + // Exceptionally holding ImfLock here since this is a internal lambda expression. + synchronized (ImfLock.class) { + removeClientAsBinder(clientInvoker.asBinder()); + } + }; + + // TODO(b/319457906): Optimize this linear search. final int numClients = mClients.size(); for (int i = 0; i < numClients; ++i) { final ClientState state = mClients.valueAt(i); @@ -101,14 +116,40 @@ final class ClientController { // have the client crash. Thus we do not verify the display ID at all here. Instead we // later check the display ID every time the client needs to interact with the specified // display. - mClients.put(clientInvoker.asBinder(), new ClientState(clientInvoker, inputConnection, - callerUid, callerPid, selfReportedDisplayId, deathRecipient)); + final ClientState cs = new ClientState(clientInvoker, inputConnection, + callerUid, callerPid, selfReportedDisplayId, deathRecipient); + mClients.put(clientInvoker.asBinder(), cs); + return cs; + } + + @VisibleForTesting + @GuardedBy("ImfLock.class") + boolean removeClient(IInputMethodClient client) { + return removeClientAsBinder(client.asBinder()); + } + + @GuardedBy("ImfLock.class") + private boolean removeClientAsBinder(IBinder binder) { + final ClientState cs = mClients.remove(binder); + if (cs == null) { + return false; + } + binder.unlinkToDeath(cs.mClientDeathRecipient, 0 /* flags */); + for (int i = 0; i < mCallbacks.size(); i++) { + mCallbacks.get(i).onClientRemoved(cs); + } + return true; + } + + @GuardedBy("ImfLock.class") + void addClientControllerCallback(ClientControllerCallback callback) { + mCallbacks.add(callback); } @GuardedBy("ImfLock.class") boolean verifyClientAndPackageMatch( @NonNull IInputMethodClient client, @NonNull String packageName) { - ClientState cs = mClients.get(client.asBinder()); + final ClientState cs = mClients.get(client.asBinder()); if (cs == null) { throw new IllegalArgumentException("unknown client " + client.asBinder()); } diff --git a/services/core/java/com/android/server/inputmethod/HardwareKeyboardShortcutController.java b/services/core/java/com/android/server/inputmethod/HardwareKeyboardShortcutController.java index 898d5a5e0644..ad27c5252cbe 100644 --- a/services/core/java/com/android/server/inputmethod/HardwareKeyboardShortcutController.java +++ b/services/core/java/com/android/server/inputmethod/HardwareKeyboardShortcutController.java @@ -53,8 +53,7 @@ final class HardwareKeyboardShortcutController { @GuardedBy("ImfLock.class") void reset(@NonNull ArrayMap<String, InputMethodInfo> methodMap) { mSubtypeHandles.clear(); - final InputMethodUtils.InputMethodSettings settings = - new InputMethodUtils.InputMethodSettings(methodMap, mUserId); + final InputMethodSettings settings = new InputMethodSettings(methodMap, mUserId); for (final InputMethodInfo imi : settings.getEnabledInputMethodListLocked()) { if (!imi.shouldShowInInputMethodPicker()) { continue; diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java index 4db9ead05636..8448fc233ae2 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java @@ -183,7 +183,6 @@ import com.android.server.companion.virtual.VirtualDeviceManagerInternal; import com.android.server.input.InputManagerInternal; import com.android.server.inputmethod.InputMethodManagerInternal.InputMethodListListener; import com.android.server.inputmethod.InputMethodSubtypeSwitchingController.ImeSubtypeListItem; -import com.android.server.inputmethod.InputMethodUtils.InputMethodSettings; import com.android.server.pm.UserManagerInternal; import com.android.server.statusbar.StatusBarManagerInternal; import com.android.server.utils.PriorityDump; @@ -479,7 +478,6 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub /** * The client that is currently bound to an input method. */ - // TODO(b/314150112): Move this to ClientController. @Nullable private ClientState mCurClient; @@ -1677,7 +1675,11 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub mVisibilityStateComputer = new ImeVisibilityStateComputer(this); mVisibilityApplier = new DefaultImeVisibilityApplier(this); + mClientController = new ClientController(mPackageManagerInternal); + synchronized (ImfLock.class) { + mClientController.addClientControllerCallback(c -> onClientRemoved(c)); + } mPreventImeStartupUnlessTextEditor = mRes.getBoolean( com.android.internal.R.bool.config_preventImeStartupUnlessTextEditor); @@ -2169,47 +2171,41 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub // actually running. final int callerUid = Binder.getCallingUid(); final int callerPid = Binder.getCallingPid(); - - // TODO(b/314150112): Move the death recipient logic to ClientController when moving - // removeClient method. - final IBinder.DeathRecipient deathRecipient = () -> removeClient(client); final IInputMethodClientInvoker clientInvoker = IInputMethodClientInvoker.create(client, mHandler); synchronized (ImfLock.class) { mClientController.addClient(clientInvoker, inputConnection, selfReportedDisplayId, - deathRecipient, callerUid, callerPid); + callerUid, callerPid); } } - // TODO(b/314150112): Move this to ClientController. - void removeClient(IInputMethodClient client) { + // TODO(b/314150112): Move this method to InputMethodBindingController + /** + * Hide the IME if the removed user is the current user. + */ + private void onClientRemoved(ClientController.ClientState client) { synchronized (ImfLock.class) { - ClientState cs = mClientController.mClients.remove(client.asBinder()); - if (cs != null) { - client.asBinder().unlinkToDeath(cs.mClientDeathRecipient, 0 /* flags */); - clearClientSessionLocked(cs); - clearClientSessionForAccessibilityLocked(cs); - - if (mCurClient == cs) { - hideCurrentInputLocked(mCurFocusedWindow, null /* statsToken */, 0 /* flags */, - null /* resultReceiver */, SoftInputShowHideReason.HIDE_REMOVE_CLIENT); - if (mBoundToMethod) { - mBoundToMethod = false; - IInputMethodInvoker curMethod = getCurMethodLocked(); - if (curMethod != null) { - // When we unbind input, we are unbinding the client, so we always - // unbind ime and a11y together. - curMethod.unbindInput(); - AccessibilityManagerInternal.get().unbindInput(); - } + clearClientSessionLocked(client); + clearClientSessionForAccessibilityLocked(client); + if (mCurClient == client) { + hideCurrentInputLocked(mCurFocusedWindow, null /* statsToken */, 0 /* flags */, + null /* resultReceiver */, SoftInputShowHideReason.HIDE_REMOVE_CLIENT); + if (mBoundToMethod) { + mBoundToMethod = false; + IInputMethodInvoker curMethod = getCurMethodLocked(); + if (curMethod != null) { + // When we unbind input, we are unbinding the client, so we always + // unbind ime and a11y together. + curMethod.unbindInput(); + AccessibilityManagerInternal.get().unbindInput(); } - mBoundToAccessibility = false; - mCurClient = null; - } - if (mCurFocusedWindowClient == cs) { - mCurFocusedWindowClient = null; - mCurFocusedWindowEditorInfo = null; } + mBoundToAccessibility = false; + mCurClient = null; + } + if (mCurFocusedWindowClient == client) { + mCurFocusedWindowClient = null; + mCurFocusedWindowEditorInfo = null; } } } @@ -2219,8 +2215,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub void unbindCurrentClientLocked(@UnbindReason int unbindClientReason) { if (mCurClient != null) { if (DEBUG) { - Slog.v(TAG, "unbindCurrentInputLocked: client=" - + mCurClient.mClient.asBinder()); + Slog.v(TAG, "unbindCurrentInputLocked: client=" + mCurClient.mClient.asBinder()); } if (mBoundToMethod) { mBoundToMethod = false; @@ -2313,7 +2308,8 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub final StartInputInfo info = new StartInputInfo(mSettings.getCurrentUserId(), getCurTokenLocked(), mCurTokenDisplayId, getCurIdLocked(), startInputReason, restarting, - UserHandle.getUserId(mCurClient.mUid), mCurClient.mSelfReportedDisplayId, + UserHandle.getUserId(mCurClient.mUid), + mCurClient.mSelfReportedDisplayId, mCurFocusedWindow, mCurEditorInfo, mCurFocusedWindowSoftInputMode, getSequenceNumberLocked()); mImeTargetWindowMap.put(startInputToken, mCurFocusedWindow); @@ -2324,14 +2320,14 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub // 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(mCurClient.mUid)) { + if (mSettings.getCurrentUserId() == UserHandle.getUserId( + mCurClient.mUid)) { mPackageManagerInternal.grantImplicitAccess(mSettings.getCurrentUserId(), null /* intent */, UserHandle.getAppId(getCurMethodUidLocked()), mCurClient.mUid, true /* direct */); } - @InputMethodNavButtonFlags - final int navButtonFlags = getInputMethodNavButtonFlagsLocked(); + @InputMethodNavButtonFlags final int navButtonFlags = getInputMethodNavButtonFlagsLocked(); final SessionState session = mCurClient.mCurSession; setEnabledSessionLocked(session); session.mMethod.startInput(startInputToken, mCurInputConnection, mCurEditorInfo, restarting, @@ -2751,8 +2747,8 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub && curMethod.asBinder() == method.asBinder()) { if (mCurClient != null) { clearClientSessionLocked(mCurClient); - mCurClient.mCurSession = new SessionState(mCurClient, - method, session, channel); + mCurClient.mCurSession = new SessionState( + mCurClient, method, session, channel); InputBindResult res = attachNewInputLocked( StartInputReason.SESSION_CREATED_BY_IME, true); attachNewAccessibilityLocked(StartInputReason.SESSION_CREATED_BY_IME, true); @@ -5777,8 +5773,10 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub // TODO(b/305829876): Implement user ID verification if (mCurClient != null) { clearClientSessionForAccessibilityLocked(mCurClient, accessibilityConnectionId); - mCurClient.mAccessibilitySessions.put(accessibilityConnectionId, - new AccessibilitySessionState(mCurClient, accessibilityConnectionId, + mCurClient.mAccessibilitySessions.put( + accessibilityConnectionId, + new AccessibilitySessionState(mCurClient, + accessibilityConnectionId, session)); attachNewAccessibilityLocked(StartInputReason.SESSION_CREATED_BY_ACCESSIBILITY, @@ -5812,7 +5810,8 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub } // A11yManagerService unbinds the disabled accessibility service. We don't need // to do it here. - mCurClient.mClient.onUnbindAccessibilityService(getSequenceNumberLocked(), + mCurClient.mClient.onUnbindAccessibilityService( + getSequenceNumberLocked(), accessibilityConnectionId); } // We only have sessions when we bound to an input method. Remove this session diff --git a/services/core/java/com/android/server/inputmethod/InputMethodSettings.java b/services/core/java/com/android/server/inputmethod/InputMethodSettings.java new file mode 100644 index 000000000000..4c7d7557de7d --- /dev/null +++ b/services/core/java/com/android/server/inputmethod/InputMethodSettings.java @@ -0,0 +1,686 @@ +/* + * 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.NonNull; +import android.annotation.Nullable; +import android.annotation.UserIdInt; +import android.content.pm.PackageManagerInternal; +import android.os.LocaleList; +import android.provider.Settings; +import android.text.TextUtils; +import android.util.ArrayMap; +import android.util.IntArray; +import android.util.Pair; +import android.util.Printer; +import android.util.Slog; +import android.view.inputmethod.InputMethodInfo; +import android.view.inputmethod.InputMethodSubtype; + +import com.android.internal.annotations.VisibleForTesting; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.function.Predicate; + +/** + * Utility class for putting and getting settings for InputMethod. + * + * <p>This is used in two ways:</p> + * <ul> + * <li>Singleton instance in {@link InputMethodManagerService}, which is updated on + * user-switch to follow the current user.</li> + * <li>On-demand instances when we need settings for non-current users.</li> + * </ul> + */ +final class InputMethodSettings { + public static final boolean DEBUG = false; + private static final String TAG = "InputMethodSettings"; + + private static final int NOT_A_SUBTYPE_ID = InputMethodUtils.NOT_A_SUBTYPE_ID; + private static final String NOT_A_SUBTYPE_ID_STR = String.valueOf(NOT_A_SUBTYPE_ID); + private static final char INPUT_METHOD_SEPARATOR = InputMethodUtils.INPUT_METHOD_SEPARATOR; + private static final char INPUT_METHOD_SUBTYPE_SEPARATOR = + InputMethodUtils.INPUT_METHOD_SUBTYPE_SEPARATOR; + + private final ArrayMap<String, InputMethodInfo> mMethodMap; + @UserIdInt + private final int mCurrentUserId; + + private static void buildEnabledInputMethodsSettingString( + StringBuilder builder, Pair<String, ArrayList<String>> ime) { + builder.append(ime.first); + // Inputmethod and subtypes are saved in the settings as follows: + // ime0;subtype0;subtype1:ime1;subtype0:ime2:ime3;subtype0;subtype1 + for (String subtypeId : ime.second) { + builder.append(INPUT_METHOD_SUBTYPE_SEPARATOR).append(subtypeId); + } + } + + InputMethodSettings(ArrayMap<String, InputMethodInfo> methodMap, @UserIdInt int userId) { + mMethodMap = methodMap; + mCurrentUserId = userId; + String ime = getSelectedInputMethod(); + String defaultDeviceIme = getSelectedDefaultDeviceInputMethod(); + if (defaultDeviceIme != null && !Objects.equals(ime, defaultDeviceIme)) { + putSelectedInputMethod(defaultDeviceIme); + putSelectedDefaultDeviceInputMethod(null); + } + } + + private void putString(@NonNull String key, @Nullable String str) { + SecureSettingsWrapper.putString(key, str, mCurrentUserId); + } + + @Nullable + private String getString(@NonNull String key, @Nullable String defaultValue) { + return SecureSettingsWrapper.getString(key, defaultValue, mCurrentUserId); + } + + private void putInt(String key, int value) { + SecureSettingsWrapper.putInt(key, value, mCurrentUserId); + } + + private int getInt(String key, int defaultValue) { + return SecureSettingsWrapper.getInt(key, defaultValue, mCurrentUserId); + } + + ArrayList<InputMethodInfo> getEnabledInputMethodListLocked() { + return getEnabledInputMethodListWithFilterLocked(null /* matchingCondition */); + } + + @NonNull + ArrayList<InputMethodInfo> getEnabledInputMethodListWithFilterLocked( + @Nullable Predicate<InputMethodInfo> matchingCondition) { + return createEnabledInputMethodListLocked( + getEnabledInputMethodsAndSubtypeListLocked(), matchingCondition); + } + + List<InputMethodSubtype> getEnabledInputMethodSubtypeListLocked( + InputMethodInfo imi, boolean allowsImplicitlyEnabledSubtypes) { + List<InputMethodSubtype> enabledSubtypes = + getEnabledInputMethodSubtypeListLocked(imi); + if (allowsImplicitlyEnabledSubtypes && enabledSubtypes.isEmpty()) { + enabledSubtypes = SubtypeUtils.getImplicitlyApplicableSubtypesLocked( + SystemLocaleWrapper.get(mCurrentUserId), imi); + } + return InputMethodSubtype.sort(imi, enabledSubtypes); + } + + List<InputMethodSubtype> getEnabledInputMethodSubtypeListLocked(InputMethodInfo imi) { + List<Pair<String, ArrayList<String>>> imsList = + getEnabledInputMethodsAndSubtypeListLocked(); + ArrayList<InputMethodSubtype> enabledSubtypes = new ArrayList<>(); + if (imi != null) { + for (Pair<String, ArrayList<String>> imsPair : imsList) { + InputMethodInfo info = mMethodMap.get(imsPair.first); + if (info != null && info.getId().equals(imi.getId())) { + final int subtypeCount = info.getSubtypeCount(); + for (int i = 0; i < subtypeCount; ++i) { + InputMethodSubtype ims = info.getSubtypeAt(i); + for (String s : imsPair.second) { + if (String.valueOf(ims.hashCode()).equals(s)) { + enabledSubtypes.add(ims); + } + } + } + break; + } + } + } + return enabledSubtypes; + } + + List<Pair<String, ArrayList<String>>> getEnabledInputMethodsAndSubtypeListLocked() { + final String enabledInputMethodsStr = getEnabledInputMethodsStr(); + final TextUtils.SimpleStringSplitter inputMethodSplitter = + new TextUtils.SimpleStringSplitter(INPUT_METHOD_SEPARATOR); + final TextUtils.SimpleStringSplitter subtypeSplitter = + new TextUtils.SimpleStringSplitter(INPUT_METHOD_SUBTYPE_SEPARATOR); + final ArrayList<Pair<String, ArrayList<String>>> imsList = new ArrayList<>(); + if (TextUtils.isEmpty(enabledInputMethodsStr)) { + return imsList; + } + inputMethodSplitter.setString(enabledInputMethodsStr); + while (inputMethodSplitter.hasNext()) { + String nextImsStr = inputMethodSplitter.next(); + subtypeSplitter.setString(nextImsStr); + if (subtypeSplitter.hasNext()) { + ArrayList<String> subtypeHashes = new ArrayList<>(); + // The first element is ime id. + String imeId = subtypeSplitter.next(); + while (subtypeSplitter.hasNext()) { + subtypeHashes.add(subtypeSplitter.next()); + } + imsList.add(new Pair<>(imeId, subtypeHashes)); + } + } + return imsList; + } + + /** + * Build and put a string of EnabledInputMethods with removing specified Id. + * + * @return the specified id was removed or not. + */ + boolean buildAndPutEnabledInputMethodsStrRemovingIdLocked( + StringBuilder builder, List<Pair<String, ArrayList<String>>> imsList, String id) { + boolean isRemoved = false; + boolean needsAppendSeparator = false; + for (Pair<String, ArrayList<String>> ims : imsList) { + String curId = ims.first; + if (curId.equals(id)) { + // We are disabling this input method, and it is + // currently enabled. Skip it to remove from the + // new list. + isRemoved = true; + } else { + if (needsAppendSeparator) { + builder.append(INPUT_METHOD_SEPARATOR); + } else { + needsAppendSeparator = true; + } + buildEnabledInputMethodsSettingString(builder, ims); + } + } + if (isRemoved) { + // Update the setting with the new list of input methods. + putEnabledInputMethodsStr(builder.toString()); + } + return isRemoved; + } + + private ArrayList<InputMethodInfo> createEnabledInputMethodListLocked( + List<Pair<String, ArrayList<String>>> imsList, + Predicate<InputMethodInfo> matchingCondition) { + final ArrayList<InputMethodInfo> res = new ArrayList<>(); + for (Pair<String, ArrayList<String>> ims : imsList) { + InputMethodInfo info = mMethodMap.get(ims.first); + if (info != null && !info.isVrOnly() + && (matchingCondition == null || matchingCondition.test(info))) { + res.add(info); + } + } + return res; + } + + void putEnabledInputMethodsStr(@Nullable String str) { + if (DEBUG) { + Slog.d(TAG, "putEnabledInputMethodStr: " + str); + } + if (TextUtils.isEmpty(str)) { + // OK to coalesce to null, since getEnabledInputMethodsStr() can take care of the + // empty data scenario. + putString(Settings.Secure.ENABLED_INPUT_METHODS, null); + } else { + putString(Settings.Secure.ENABLED_INPUT_METHODS, str); + } + } + + @NonNull + String getEnabledInputMethodsStr() { + return getString(Settings.Secure.ENABLED_INPUT_METHODS, ""); + } + + private void saveSubtypeHistory( + List<Pair<String, String>> savedImes, String newImeId, String newSubtypeId) { + StringBuilder builder = new StringBuilder(); + boolean isImeAdded = false; + if (!TextUtils.isEmpty(newImeId) && !TextUtils.isEmpty(newSubtypeId)) { + builder.append(newImeId).append(INPUT_METHOD_SUBTYPE_SEPARATOR).append( + newSubtypeId); + isImeAdded = true; + } + for (Pair<String, String> ime : savedImes) { + String imeId = ime.first; + String subtypeId = ime.second; + if (TextUtils.isEmpty(subtypeId)) { + subtypeId = NOT_A_SUBTYPE_ID_STR; + } + if (isImeAdded) { + builder.append(INPUT_METHOD_SEPARATOR); + } else { + isImeAdded = true; + } + builder.append(imeId).append(INPUT_METHOD_SUBTYPE_SEPARATOR).append( + subtypeId); + } + // Remove the last INPUT_METHOD_SEPARATOR + putSubtypeHistoryStr(builder.toString()); + } + + private void addSubtypeToHistory(String imeId, String subtypeId) { + List<Pair<String, String>> subtypeHistory = loadInputMethodAndSubtypeHistoryLocked(); + for (Pair<String, String> ime : subtypeHistory) { + if (ime.first.equals(imeId)) { + if (DEBUG) { + Slog.v(TAG, "Subtype found in the history: " + imeId + ", " + + ime.second); + } + // We should break here + subtypeHistory.remove(ime); + break; + } + } + if (DEBUG) { + Slog.v(TAG, "Add subtype to the history: " + imeId + ", " + subtypeId); + } + saveSubtypeHistory(subtypeHistory, imeId, subtypeId); + } + + private void putSubtypeHistoryStr(@NonNull String str) { + if (DEBUG) { + Slog.d(TAG, "putSubtypeHistoryStr: " + str); + } + if (TextUtils.isEmpty(str)) { + // OK to coalesce to null, since getSubtypeHistoryStr() can take care of the empty + // data scenario. + putString(Settings.Secure.INPUT_METHODS_SUBTYPE_HISTORY, null); + } else { + putString(Settings.Secure.INPUT_METHODS_SUBTYPE_HISTORY, str); + } + } + + Pair<String, String> getLastInputMethodAndSubtypeLocked() { + // Gets the first one from the history + return getLastSubtypeForInputMethodLockedInternal(null); + } + + @Nullable + InputMethodSubtype getLastInputMethodSubtypeLocked() { + final Pair<String, String> lastIme = getLastInputMethodAndSubtypeLocked(); + // TODO: Handle the case of the last IME with no subtypes + if (lastIme == null || TextUtils.isEmpty(lastIme.first) + || TextUtils.isEmpty(lastIme.second)) { + return null; + } + final InputMethodInfo lastImi = mMethodMap.get(lastIme.first); + if (lastImi == null) return null; + try { + final int lastSubtypeHash = Integer.parseInt(lastIme.second); + final int lastSubtypeId = SubtypeUtils.getSubtypeIdFromHashCode(lastImi, + lastSubtypeHash); + if (lastSubtypeId < 0 || lastSubtypeId >= lastImi.getSubtypeCount()) { + return null; + } + return lastImi.getSubtypeAt(lastSubtypeId); + } catch (NumberFormatException e) { + return null; + } + } + + String getLastSubtypeForInputMethodLocked(String imeId) { + Pair<String, String> ime = getLastSubtypeForInputMethodLockedInternal(imeId); + if (ime != null) { + return ime.second; + } else { + return null; + } + } + + private Pair<String, String> getLastSubtypeForInputMethodLockedInternal(String imeId) { + List<Pair<String, ArrayList<String>>> enabledImes = + getEnabledInputMethodsAndSubtypeListLocked(); + List<Pair<String, String>> subtypeHistory = loadInputMethodAndSubtypeHistoryLocked(); + for (Pair<String, String> imeAndSubtype : subtypeHistory) { + final String imeInTheHistory = imeAndSubtype.first; + // If imeId is empty, returns the first IME and subtype in the history + if (TextUtils.isEmpty(imeId) || imeInTheHistory.equals(imeId)) { + final String subtypeInTheHistory = imeAndSubtype.second; + final String subtypeHashCode = + getEnabledSubtypeHashCodeForInputMethodAndSubtypeLocked( + enabledImes, imeInTheHistory, subtypeInTheHistory); + if (!TextUtils.isEmpty(subtypeHashCode)) { + if (DEBUG) { + Slog.d(TAG, + "Enabled subtype found in the history: " + subtypeHashCode); + } + return new Pair<>(imeInTheHistory, subtypeHashCode); + } + } + } + if (DEBUG) { + Slog.d(TAG, "No enabled IME found in the history"); + } + return null; + } + + private String getEnabledSubtypeHashCodeForInputMethodAndSubtypeLocked(List<Pair<String, + ArrayList<String>>> enabledImes, String imeId, String subtypeHashCode) { + final LocaleList localeList = SystemLocaleWrapper.get(mCurrentUserId); + for (Pair<String, ArrayList<String>> enabledIme : enabledImes) { + if (enabledIme.first.equals(imeId)) { + final ArrayList<String> explicitlyEnabledSubtypes = enabledIme.second; + final InputMethodInfo imi = mMethodMap.get(imeId); + if (explicitlyEnabledSubtypes.size() == 0) { + // If there are no explicitly enabled subtypes, applicable subtypes are + // enabled implicitly. + // If IME is enabled and no subtypes are enabled, applicable subtypes + // are enabled implicitly, so needs to treat them to be enabled. + if (imi != null && imi.getSubtypeCount() > 0) { + List<InputMethodSubtype> implicitlyEnabledSubtypes = + SubtypeUtils.getImplicitlyApplicableSubtypesLocked(localeList, + imi); + final int numSubtypes = implicitlyEnabledSubtypes.size(); + for (int i = 0; i < numSubtypes; ++i) { + final InputMethodSubtype st = implicitlyEnabledSubtypes.get(i); + if (String.valueOf(st.hashCode()).equals(subtypeHashCode)) { + return subtypeHashCode; + } + } + } + } else { + for (String s : explicitlyEnabledSubtypes) { + if (s.equals(subtypeHashCode)) { + // If both imeId and subtypeId are enabled, return subtypeId. + try { + final int hashCode = Integer.parseInt(subtypeHashCode); + // Check whether the subtype id is valid or not + if (SubtypeUtils.isValidSubtypeId(imi, hashCode)) { + return s; + } else { + return NOT_A_SUBTYPE_ID_STR; + } + } catch (NumberFormatException e) { + return NOT_A_SUBTYPE_ID_STR; + } + } + } + } + // If imeId was enabled but subtypeId was disabled. + return NOT_A_SUBTYPE_ID_STR; + } + } + // If both imeId and subtypeId are disabled, return null + return null; + } + + private List<Pair<String, String>> loadInputMethodAndSubtypeHistoryLocked() { + ArrayList<Pair<String, String>> imsList = new ArrayList<>(); + final String subtypeHistoryStr = getSubtypeHistoryStr(); + if (TextUtils.isEmpty(subtypeHistoryStr)) { + return imsList; + } + final TextUtils.SimpleStringSplitter inputMethodSplitter = + new TextUtils.SimpleStringSplitter(INPUT_METHOD_SEPARATOR); + final TextUtils.SimpleStringSplitter subtypeSplitter = + new TextUtils.SimpleStringSplitter(INPUT_METHOD_SUBTYPE_SEPARATOR); + inputMethodSplitter.setString(subtypeHistoryStr); + while (inputMethodSplitter.hasNext()) { + String nextImsStr = inputMethodSplitter.next(); + subtypeSplitter.setString(nextImsStr); + if (subtypeSplitter.hasNext()) { + String subtypeId = NOT_A_SUBTYPE_ID_STR; + // The first element is ime id. + String imeId = subtypeSplitter.next(); + while (subtypeSplitter.hasNext()) { + subtypeId = subtypeSplitter.next(); + break; + } + imsList.add(new Pair<>(imeId, subtypeId)); + } + } + return imsList; + } + + @NonNull + private String getSubtypeHistoryStr() { + final String history = getString(Settings.Secure.INPUT_METHODS_SUBTYPE_HISTORY, ""); + if (DEBUG) { + Slog.d(TAG, "getSubtypeHistoryStr: " + history); + } + return history; + } + + void putSelectedInputMethod(String imeId) { + if (DEBUG) { + Slog.d(TAG, "putSelectedInputMethodStr: " + imeId + ", " + + mCurrentUserId); + } + putString(Settings.Secure.DEFAULT_INPUT_METHOD, imeId); + } + + void putSelectedSubtype(int subtypeId) { + if (DEBUG) { + Slog.d(TAG, "putSelectedInputMethodSubtypeStr: " + subtypeId + ", " + + mCurrentUserId); + } + putInt(Settings.Secure.SELECTED_INPUT_METHOD_SUBTYPE, subtypeId); + } + + @Nullable + String getSelectedInputMethod() { + final String imi = getString(Settings.Secure.DEFAULT_INPUT_METHOD, null); + if (DEBUG) { + Slog.d(TAG, "getSelectedInputMethodStr: " + imi); + } + return imi; + } + + @Nullable + String getSelectedDefaultDeviceInputMethod() { + final String imi = getString(Settings.Secure.DEFAULT_DEVICE_INPUT_METHOD, null); + if (DEBUG) { + Slog.d(TAG, "getSelectedDefaultDeviceInputMethodStr: " + imi + ", " + + mCurrentUserId); + } + return imi; + } + + void putSelectedDefaultDeviceInputMethod(String imeId) { + if (DEBUG) { + Slog.d(TAG, "putSelectedDefaultDeviceInputMethodStr: " + imeId + ", " + + mCurrentUserId); + } + putString(Settings.Secure.DEFAULT_DEVICE_INPUT_METHOD, imeId); + } + + void putDefaultVoiceInputMethod(String imeId) { + if (DEBUG) { + Slog.d(TAG, + "putDefaultVoiceInputMethodStr: " + imeId + ", " + mCurrentUserId); + } + putString(Settings.Secure.DEFAULT_VOICE_INPUT_METHOD, imeId); + } + + @Nullable + String getDefaultVoiceInputMethod() { + final String imi = getString(Settings.Secure.DEFAULT_VOICE_INPUT_METHOD, null); + if (DEBUG) { + Slog.d(TAG, "getDefaultVoiceInputMethodStr: " + imi); + } + return imi; + } + + boolean isSubtypeSelected() { + return getSelectedInputMethodSubtypeHashCode() != NOT_A_SUBTYPE_ID; + } + + private int getSelectedInputMethodSubtypeHashCode() { + return getInt(Settings.Secure.SELECTED_INPUT_METHOD_SUBTYPE, + NOT_A_SUBTYPE_ID); + } + + @UserIdInt + public int getCurrentUserId() { + return mCurrentUserId; + } + + int getSelectedInputMethodSubtypeId(String selectedImiId) { + final InputMethodInfo imi = mMethodMap.get(selectedImiId); + if (imi == null) { + return NOT_A_SUBTYPE_ID; + } + final int subtypeHashCode = getSelectedInputMethodSubtypeHashCode(); + return SubtypeUtils.getSubtypeIdFromHashCode(imi, subtypeHashCode); + } + + void saveCurrentInputMethodAndSubtypeToHistory(String curMethodId, + InputMethodSubtype currentSubtype) { + String subtypeId = NOT_A_SUBTYPE_ID_STR; + if (currentSubtype != null) { + subtypeId = String.valueOf(currentSubtype.hashCode()); + } + if (InputMethodUtils.canAddToLastInputMethod(currentSubtype)) { + addSubtypeToHistory(curMethodId, subtypeId); + } + } + + /** + * A variant of {@link InputMethodManagerService#getCurrentInputMethodSubtypeLocked()} for + * non-current users. + * + * <p>TODO: Address code duplication between this and + * {@link InputMethodManagerService#getCurrentInputMethodSubtypeLocked()}.</p> + * + * @return {@link InputMethodSubtype} if exists. {@code null} otherwise. + */ + @Nullable + InputMethodSubtype getCurrentInputMethodSubtypeForNonCurrentUsers() { + final String selectedMethodId = getSelectedInputMethod(); + if (selectedMethodId == null) { + return null; + } + final InputMethodInfo imi = mMethodMap.get(selectedMethodId); + if (imi == null || imi.getSubtypeCount() == 0) { + return null; + } + + final int subtypeHashCode = getSelectedInputMethodSubtypeHashCode(); + if (subtypeHashCode != NOT_A_SUBTYPE_ID) { + final int subtypeIndex = SubtypeUtils.getSubtypeIdFromHashCode(imi, + subtypeHashCode); + if (subtypeIndex >= 0) { + return imi.getSubtypeAt(subtypeIndex); + } + } + + // 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); + // If there is only one explicitly or implicitly enabled subtype, just returns it. + if (explicitlyOrImplicitlyEnabledSubtypes.isEmpty()) { + return null; + } + if (explicitlyOrImplicitlyEnabledSubtypes.size() == 1) { + return explicitlyOrImplicitlyEnabledSubtypes.get(0); + } + final String locale = SystemLocaleWrapper.get(mCurrentUserId).get(0).toString(); + final InputMethodSubtype subtype = SubtypeUtils.findLastResortApplicableSubtypeLocked( + explicitlyOrImplicitlyEnabledSubtypes, SubtypeUtils.SUBTYPE_MODE_KEYBOARD, + locale, true); + if (subtype != null) { + return subtype; + } + return SubtypeUtils.findLastResortApplicableSubtypeLocked( + explicitlyOrImplicitlyEnabledSubtypes, null, locale, true); + } + + boolean setAdditionalInputMethodSubtypes(@NonNull String imeId, + @NonNull ArrayList<InputMethodSubtype> subtypes, + @NonNull ArrayMap<String, List<InputMethodSubtype>> additionalSubtypeMap, + @NonNull PackageManagerInternal packageManagerInternal, int callingUid) { + final InputMethodInfo imi = mMethodMap.get(imeId); + if (imi == null) { + return false; + } + if (!InputMethodUtils.checkIfPackageBelongsToUid(packageManagerInternal, callingUid, + imi.getPackageName())) { + return false; + } + + if (subtypes.isEmpty()) { + additionalSubtypeMap.remove(imi.getId()); + } else { + additionalSubtypeMap.put(imi.getId(), subtypes); + } + AdditionalSubtypeUtils.save(additionalSubtypeMap, mMethodMap, getCurrentUserId()); + return true; + } + + boolean setEnabledInputMethodSubtypes(@NonNull String imeId, + @NonNull int[] subtypeHashCodes) { + final InputMethodInfo imi = mMethodMap.get(imeId); + if (imi == null) { + return false; + } + + final IntArray validSubtypeHashCodes = new IntArray(subtypeHashCodes.length); + for (int subtypeHashCode : subtypeHashCodes) { + if (subtypeHashCode == NOT_A_SUBTYPE_ID) { + continue; // NOT_A_SUBTYPE_ID must not be saved + } + if (!SubtypeUtils.isValidSubtypeId(imi, subtypeHashCode)) { + continue; // this subtype does not exist in InputMethodInfo. + } + if (validSubtypeHashCodes.indexOf(subtypeHashCode) >= 0) { + continue; // The entry is already added. No need to add anymore. + } + validSubtypeHashCodes.add(subtypeHashCode); + } + + final String originalEnabledImesString = getEnabledInputMethodsStr(); + final String updatedEnabledImesString = updateEnabledImeString( + originalEnabledImesString, imi.getId(), validSubtypeHashCodes); + if (TextUtils.equals(originalEnabledImesString, updatedEnabledImesString)) { + return false; + } + + putEnabledInputMethodsStr(updatedEnabledImesString); + return true; + } + + @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE) + static String updateEnabledImeString(@NonNull String enabledImesString, + @NonNull String imeId, @NonNull IntArray enabledSubtypeHashCodes) { + final TextUtils.SimpleStringSplitter imeSplitter = + new TextUtils.SimpleStringSplitter(INPUT_METHOD_SEPARATOR); + final TextUtils.SimpleStringSplitter imeSubtypeSplitter = + new TextUtils.SimpleStringSplitter(INPUT_METHOD_SUBTYPE_SEPARATOR); + + final StringBuilder sb = new StringBuilder(); + + imeSplitter.setString(enabledImesString); + boolean needsImeSeparator = false; + while (imeSplitter.hasNext()) { + final String nextImsStr = imeSplitter.next(); + imeSubtypeSplitter.setString(nextImsStr); + if (imeSubtypeSplitter.hasNext()) { + if (needsImeSeparator) { + sb.append(INPUT_METHOD_SEPARATOR); + } + if (TextUtils.equals(imeId, imeSubtypeSplitter.next())) { + sb.append(imeId); + for (int i = 0; i < enabledSubtypeHashCodes.size(); ++i) { + sb.append(INPUT_METHOD_SUBTYPE_SEPARATOR); + sb.append(enabledSubtypeHashCodes.get(i)); + } + } else { + sb.append(nextImsStr); + } + needsImeSeparator = true; + } + } + return sb.toString(); + } + + void dumpLocked(final Printer pw, final String prefix) { + pw.println(prefix + "mCurrentUserId=" + mCurrentUserId); + } +} diff --git a/services/core/java/com/android/server/inputmethod/InputMethodSubtypeSwitchingController.java b/services/core/java/com/android/server/inputmethod/InputMethodSubtypeSwitchingController.java index 4439b0683afa..43058080d84d 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodSubtypeSwitchingController.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodSubtypeSwitchingController.java @@ -31,7 +31,6 @@ import android.view.inputmethod.InputMethodInfo; import android.view.inputmethod.InputMethodSubtype; import com.android.internal.annotations.VisibleForTesting; -import com.android.server.inputmethod.InputMethodUtils.InputMethodSettings; import java.util.ArrayList; import java.util.Collections; diff --git a/services/core/java/com/android/server/inputmethod/InputMethodUtils.java b/services/core/java/com/android/server/inputmethod/InputMethodUtils.java index 58a68f2a0166..361cdbbc15bf 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodUtils.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodUtils.java @@ -28,15 +28,10 @@ import android.content.pm.PackageManagerInternal; import android.content.pm.ResolveInfo; import android.content.res.Resources; import android.os.Build; -import android.os.LocaleList; import android.os.UserHandle; import android.provider.Settings; import android.text.TextUtils; -import android.util.ArrayMap; import android.util.ArraySet; -import android.util.IntArray; -import android.util.Pair; -import android.util.Printer; import android.util.Slog; import android.view.inputmethod.InputMethodInfo; import android.view.inputmethod.InputMethodSubtype; @@ -51,10 +46,8 @@ import com.android.server.textservices.TextServicesManagerInternal; import java.io.PrintWriter; import java.util.ArrayList; import java.util.List; -import java.util.Objects; import java.util.StringJoiner; import java.util.function.Consumer; -import java.util.function.Predicate; /** * This class provides random static utility methods for {@link InputMethodManagerService} and its @@ -68,12 +61,11 @@ final class InputMethodUtils { public static final boolean DEBUG = false; static final int NOT_A_SUBTYPE_ID = -1; private static final String TAG = "InputMethodUtils"; - private static final String NOT_A_SUBTYPE_ID_STR = String.valueOf(NOT_A_SUBTYPE_ID); // The string for enabled input method is saved as follows: // example: ("ime0;subtype0;subtype1;subtype2:ime1:ime2;subtype0") - private static final char INPUT_METHOD_SEPARATOR = ':'; - private static final char INPUT_METHOD_SUBTYPE_SEPARATOR = ';'; + static final char INPUT_METHOD_SEPARATOR = ':'; + static final char INPUT_METHOD_SUBTYPE_SEPARATOR = ';'; private InputMethodUtils() { // This utility class is not publicly instantiable. @@ -200,640 +192,6 @@ final class InputMethodUtils { UserHandle.getUserId(uid)); } - /** - * Utility class for putting and getting settings for InputMethod. - * - * This is used in two ways: - * - Singleton instance in {@link InputMethodManagerService}, which is updated on user-switch to - * follow the current user. - * - On-demand instances when we need settings for non-current users. - * - * TODO: Move all putters and getters of settings to this class. - */ - @UserHandleAware - public static class InputMethodSettings { - private final ArrayMap<String, InputMethodInfo> mMethodMap; - - @UserIdInt - private final int mCurrentUserId; - - private static void buildEnabledInputMethodsSettingString( - StringBuilder builder, Pair<String, ArrayList<String>> ime) { - builder.append(ime.first); - // Inputmethod and subtypes are saved in the settings as follows: - // ime0;subtype0;subtype1:ime1;subtype0:ime2:ime3;subtype0;subtype1 - for (String subtypeId: ime.second) { - builder.append(INPUT_METHOD_SUBTYPE_SEPARATOR).append(subtypeId); - } - } - - InputMethodSettings(ArrayMap<String, InputMethodInfo> methodMap, @UserIdInt int userId) { - mMethodMap = methodMap; - mCurrentUserId = userId; - String ime = getSelectedInputMethod(); - String defaultDeviceIme = getSelectedDefaultDeviceInputMethod(); - if (defaultDeviceIme != null && !Objects.equals(ime, defaultDeviceIme)) { - putSelectedInputMethod(defaultDeviceIme); - putSelectedDefaultDeviceInputMethod(null); - } - } - - private void putString(@NonNull String key, @Nullable String str) { - SecureSettingsWrapper.putString(key, str, mCurrentUserId); - } - - @Nullable - private String getString(@NonNull String key, @Nullable String defaultValue) { - return SecureSettingsWrapper.getString(key, defaultValue, mCurrentUserId); - } - - private void putInt(String key, int value) { - SecureSettingsWrapper.putInt(key, value, mCurrentUserId); - } - - private int getInt(String key, int defaultValue) { - return SecureSettingsWrapper.getInt(key, defaultValue, mCurrentUserId); - } - - ArrayList<InputMethodInfo> getEnabledInputMethodListLocked() { - return getEnabledInputMethodListWithFilterLocked(null /* matchingCondition */); - } - - @NonNull - ArrayList<InputMethodInfo> getEnabledInputMethodListWithFilterLocked( - @Nullable Predicate<InputMethodInfo> matchingCondition) { - return createEnabledInputMethodListLocked( - getEnabledInputMethodsAndSubtypeListLocked(), matchingCondition); - } - - List<InputMethodSubtype> getEnabledInputMethodSubtypeListLocked( - InputMethodInfo imi, boolean allowsImplicitlyEnabledSubtypes) { - List<InputMethodSubtype> enabledSubtypes = - getEnabledInputMethodSubtypeListLocked(imi); - if (allowsImplicitlyEnabledSubtypes && enabledSubtypes.isEmpty()) { - enabledSubtypes = SubtypeUtils.getImplicitlyApplicableSubtypesLocked( - SystemLocaleWrapper.get(mCurrentUserId), imi); - } - return InputMethodSubtype.sort(imi, enabledSubtypes); - } - - List<InputMethodSubtype> getEnabledInputMethodSubtypeListLocked(InputMethodInfo imi) { - List<Pair<String, ArrayList<String>>> imsList = - getEnabledInputMethodsAndSubtypeListLocked(); - ArrayList<InputMethodSubtype> enabledSubtypes = new ArrayList<>(); - if (imi != null) { - for (Pair<String, ArrayList<String>> imsPair : imsList) { - InputMethodInfo info = mMethodMap.get(imsPair.first); - if (info != null && info.getId().equals(imi.getId())) { - final int subtypeCount = info.getSubtypeCount(); - for (int i = 0; i < subtypeCount; ++i) { - InputMethodSubtype ims = info.getSubtypeAt(i); - for (String s: imsPair.second) { - if (String.valueOf(ims.hashCode()).equals(s)) { - enabledSubtypes.add(ims); - } - } - } - break; - } - } - } - return enabledSubtypes; - } - - List<Pair<String, ArrayList<String>>> getEnabledInputMethodsAndSubtypeListLocked() { - final String enabledInputMethodsStr = getEnabledInputMethodsStr(); - final TextUtils.SimpleStringSplitter inputMethodSplitter = - new TextUtils.SimpleStringSplitter(INPUT_METHOD_SEPARATOR); - final TextUtils.SimpleStringSplitter subtypeSplitter = - new TextUtils.SimpleStringSplitter(INPUT_METHOD_SUBTYPE_SEPARATOR); - final ArrayList<Pair<String, ArrayList<String>>> imsList = new ArrayList<>(); - if (TextUtils.isEmpty(enabledInputMethodsStr)) { - return imsList; - } - inputMethodSplitter.setString(enabledInputMethodsStr); - while (inputMethodSplitter.hasNext()) { - String nextImsStr = inputMethodSplitter.next(); - subtypeSplitter.setString(nextImsStr); - if (subtypeSplitter.hasNext()) { - ArrayList<String> subtypeHashes = new ArrayList<>(); - // The first element is ime id. - String imeId = subtypeSplitter.next(); - while (subtypeSplitter.hasNext()) { - subtypeHashes.add(subtypeSplitter.next()); - } - imsList.add(new Pair<>(imeId, subtypeHashes)); - } - } - return imsList; - } - - /** - * Build and put a string of EnabledInputMethods with removing specified Id. - * @return the specified id was removed or not. - */ - boolean buildAndPutEnabledInputMethodsStrRemovingIdLocked( - StringBuilder builder, List<Pair<String, ArrayList<String>>> imsList, String id) { - boolean isRemoved = false; - boolean needsAppendSeparator = false; - for (Pair<String, ArrayList<String>> ims: imsList) { - String curId = ims.first; - if (curId.equals(id)) { - // We are disabling this input method, and it is - // currently enabled. Skip it to remove from the - // new list. - isRemoved = true; - } else { - if (needsAppendSeparator) { - builder.append(INPUT_METHOD_SEPARATOR); - } else { - needsAppendSeparator = true; - } - buildEnabledInputMethodsSettingString(builder, ims); - } - } - if (isRemoved) { - // Update the setting with the new list of input methods. - putEnabledInputMethodsStr(builder.toString()); - } - return isRemoved; - } - - private ArrayList<InputMethodInfo> createEnabledInputMethodListLocked( - List<Pair<String, ArrayList<String>>> imsList, - Predicate<InputMethodInfo> matchingCondition) { - final ArrayList<InputMethodInfo> res = new ArrayList<>(); - for (Pair<String, ArrayList<String>> ims: imsList) { - InputMethodInfo info = mMethodMap.get(ims.first); - if (info != null && !info.isVrOnly() - && (matchingCondition == null || matchingCondition.test(info))) { - res.add(info); - } - } - return res; - } - - void putEnabledInputMethodsStr(@Nullable String str) { - if (DEBUG) { - Slog.d(TAG, "putEnabledInputMethodStr: " + str); - } - if (TextUtils.isEmpty(str)) { - // OK to coalesce to null, since getEnabledInputMethodsStr() can take care of the - // empty data scenario. - putString(Settings.Secure.ENABLED_INPUT_METHODS, null); - } else { - putString(Settings.Secure.ENABLED_INPUT_METHODS, str); - } - } - - @NonNull - String getEnabledInputMethodsStr() { - return getString(Settings.Secure.ENABLED_INPUT_METHODS, ""); - } - - private void saveSubtypeHistory( - List<Pair<String, String>> savedImes, String newImeId, String newSubtypeId) { - StringBuilder builder = new StringBuilder(); - boolean isImeAdded = false; - if (!TextUtils.isEmpty(newImeId) && !TextUtils.isEmpty(newSubtypeId)) { - builder.append(newImeId).append(INPUT_METHOD_SUBTYPE_SEPARATOR).append( - newSubtypeId); - isImeAdded = true; - } - for (Pair<String, String> ime: savedImes) { - String imeId = ime.first; - String subtypeId = ime.second; - if (TextUtils.isEmpty(subtypeId)) { - subtypeId = NOT_A_SUBTYPE_ID_STR; - } - if (isImeAdded) { - builder.append(INPUT_METHOD_SEPARATOR); - } else { - isImeAdded = true; - } - builder.append(imeId).append(INPUT_METHOD_SUBTYPE_SEPARATOR).append( - subtypeId); - } - // Remove the last INPUT_METHOD_SEPARATOR - putSubtypeHistoryStr(builder.toString()); - } - - private void addSubtypeToHistory(String imeId, String subtypeId) { - List<Pair<String, String>> subtypeHistory = loadInputMethodAndSubtypeHistoryLocked(); - for (Pair<String, String> ime: subtypeHistory) { - if (ime.first.equals(imeId)) { - if (DEBUG) { - Slog.v(TAG, "Subtype found in the history: " + imeId + ", " - + ime.second); - } - // We should break here - subtypeHistory.remove(ime); - break; - } - } - if (DEBUG) { - Slog.v(TAG, "Add subtype to the history: " + imeId + ", " + subtypeId); - } - saveSubtypeHistory(subtypeHistory, imeId, subtypeId); - } - - private void putSubtypeHistoryStr(@NonNull String str) { - if (DEBUG) { - Slog.d(TAG, "putSubtypeHistoryStr: " + str); - } - if (TextUtils.isEmpty(str)) { - // OK to coalesce to null, since getSubtypeHistoryStr() can take care of the empty - // data scenario. - putString(Settings.Secure.INPUT_METHODS_SUBTYPE_HISTORY, null); - } else { - putString(Settings.Secure.INPUT_METHODS_SUBTYPE_HISTORY, str); - } - } - - Pair<String, String> getLastInputMethodAndSubtypeLocked() { - // Gets the first one from the history - return getLastSubtypeForInputMethodLockedInternal(null); - } - - @Nullable - InputMethodSubtype getLastInputMethodSubtypeLocked() { - final Pair<String, String> lastIme = getLastInputMethodAndSubtypeLocked(); - // TODO: Handle the case of the last IME with no subtypes - if (lastIme == null || TextUtils.isEmpty(lastIme.first) - || TextUtils.isEmpty(lastIme.second)) return null; - final InputMethodInfo lastImi = mMethodMap.get(lastIme.first); - if (lastImi == null) return null; - try { - final int lastSubtypeHash = Integer.parseInt(lastIme.second); - final int lastSubtypeId = SubtypeUtils.getSubtypeIdFromHashCode(lastImi, - lastSubtypeHash); - if (lastSubtypeId < 0 || lastSubtypeId >= lastImi.getSubtypeCount()) { - return null; - } - return lastImi.getSubtypeAt(lastSubtypeId); - } catch (NumberFormatException e) { - return null; - } - } - - String getLastSubtypeForInputMethodLocked(String imeId) { - Pair<String, String> ime = getLastSubtypeForInputMethodLockedInternal(imeId); - if (ime != null) { - return ime.second; - } else { - return null; - } - } - - private Pair<String, String> getLastSubtypeForInputMethodLockedInternal(String imeId) { - List<Pair<String, ArrayList<String>>> enabledImes = - getEnabledInputMethodsAndSubtypeListLocked(); - List<Pair<String, String>> subtypeHistory = loadInputMethodAndSubtypeHistoryLocked(); - for (Pair<String, String> imeAndSubtype : subtypeHistory) { - final String imeInTheHistory = imeAndSubtype.first; - // If imeId is empty, returns the first IME and subtype in the history - if (TextUtils.isEmpty(imeId) || imeInTheHistory.equals(imeId)) { - final String subtypeInTheHistory = imeAndSubtype.second; - final String subtypeHashCode = - getEnabledSubtypeHashCodeForInputMethodAndSubtypeLocked( - enabledImes, imeInTheHistory, subtypeInTheHistory); - if (!TextUtils.isEmpty(subtypeHashCode)) { - if (DEBUG) { - Slog.d(TAG, "Enabled subtype found in the history: " + subtypeHashCode); - } - return new Pair<>(imeInTheHistory, subtypeHashCode); - } - } - } - if (DEBUG) { - Slog.d(TAG, "No enabled IME found in the history"); - } - return null; - } - - private String getEnabledSubtypeHashCodeForInputMethodAndSubtypeLocked(List<Pair<String, - ArrayList<String>>> enabledImes, String imeId, String subtypeHashCode) { - final LocaleList localeList = SystemLocaleWrapper.get(mCurrentUserId); - for (Pair<String, ArrayList<String>> enabledIme: enabledImes) { - if (enabledIme.first.equals(imeId)) { - final ArrayList<String> explicitlyEnabledSubtypes = enabledIme.second; - final InputMethodInfo imi = mMethodMap.get(imeId); - if (explicitlyEnabledSubtypes.size() == 0) { - // If there are no explicitly enabled subtypes, applicable subtypes are - // enabled implicitly. - // If IME is enabled and no subtypes are enabled, applicable subtypes - // are enabled implicitly, so needs to treat them to be enabled. - if (imi != null && imi.getSubtypeCount() > 0) { - List<InputMethodSubtype> implicitlyEnabledSubtypes = - SubtypeUtils.getImplicitlyApplicableSubtypesLocked(localeList, - imi); - final int numSubtypes = implicitlyEnabledSubtypes.size(); - for (int i = 0; i < numSubtypes; ++i) { - final InputMethodSubtype st = implicitlyEnabledSubtypes.get(i); - if (String.valueOf(st.hashCode()).equals(subtypeHashCode)) { - return subtypeHashCode; - } - } - } - } else { - for (String s: explicitlyEnabledSubtypes) { - if (s.equals(subtypeHashCode)) { - // If both imeId and subtypeId are enabled, return subtypeId. - try { - final int hashCode = Integer.parseInt(subtypeHashCode); - // Check whether the subtype id is valid or not - if (SubtypeUtils.isValidSubtypeId(imi, hashCode)) { - return s; - } else { - return NOT_A_SUBTYPE_ID_STR; - } - } catch (NumberFormatException e) { - return NOT_A_SUBTYPE_ID_STR; - } - } - } - } - // If imeId was enabled but subtypeId was disabled. - return NOT_A_SUBTYPE_ID_STR; - } - } - // If both imeId and subtypeId are disabled, return null - return null; - } - - private List<Pair<String, String>> loadInputMethodAndSubtypeHistoryLocked() { - ArrayList<Pair<String, String>> imsList = new ArrayList<>(); - final String subtypeHistoryStr = getSubtypeHistoryStr(); - if (TextUtils.isEmpty(subtypeHistoryStr)) { - return imsList; - } - final TextUtils.SimpleStringSplitter inputMethodSplitter = - new TextUtils.SimpleStringSplitter(INPUT_METHOD_SEPARATOR); - final TextUtils.SimpleStringSplitter subtypeSplitter = - new TextUtils.SimpleStringSplitter(INPUT_METHOD_SUBTYPE_SEPARATOR); - inputMethodSplitter.setString(subtypeHistoryStr); - while (inputMethodSplitter.hasNext()) { - String nextImsStr = inputMethodSplitter.next(); - subtypeSplitter.setString(nextImsStr); - if (subtypeSplitter.hasNext()) { - String subtypeId = NOT_A_SUBTYPE_ID_STR; - // The first element is ime id. - String imeId = subtypeSplitter.next(); - while (subtypeSplitter.hasNext()) { - subtypeId = subtypeSplitter.next(); - break; - } - imsList.add(new Pair<>(imeId, subtypeId)); - } - } - return imsList; - } - - @NonNull - private String getSubtypeHistoryStr() { - final String history = getString(Settings.Secure.INPUT_METHODS_SUBTYPE_HISTORY, ""); - if (DEBUG) { - Slog.d(TAG, "getSubtypeHistoryStr: " + history); - } - return history; - } - - void putSelectedInputMethod(String imeId) { - if (DEBUG) { - Slog.d(TAG, "putSelectedInputMethodStr: " + imeId + ", " - + mCurrentUserId); - } - putString(Settings.Secure.DEFAULT_INPUT_METHOD, imeId); - } - - void putSelectedSubtype(int subtypeId) { - if (DEBUG) { - Slog.d(TAG, "putSelectedInputMethodSubtypeStr: " + subtypeId + ", " - + mCurrentUserId); - } - putInt(Settings.Secure.SELECTED_INPUT_METHOD_SUBTYPE, subtypeId); - } - - @Nullable - String getSelectedInputMethod() { - final String imi = getString(Settings.Secure.DEFAULT_INPUT_METHOD, null); - if (DEBUG) { - Slog.d(TAG, "getSelectedInputMethodStr: " + imi); - } - return imi; - } - - @Nullable - String getSelectedDefaultDeviceInputMethod() { - final String imi = getString(Settings.Secure.DEFAULT_DEVICE_INPUT_METHOD, null); - if (DEBUG) { - Slog.d(TAG, "getSelectedDefaultDeviceInputMethodStr: " + imi + ", " - + mCurrentUserId); - } - return imi; - } - - void putSelectedDefaultDeviceInputMethod(String imeId) { - if (DEBUG) { - Slog.d(TAG, "putSelectedDefaultDeviceInputMethodStr: " + imeId + ", " - + mCurrentUserId); - } - putString(Settings.Secure.DEFAULT_DEVICE_INPUT_METHOD, imeId); - } - - void putDefaultVoiceInputMethod(String imeId) { - if (DEBUG) { - Slog.d(TAG, "putDefaultVoiceInputMethodStr: " + imeId + ", " + mCurrentUserId); - } - putString(Settings.Secure.DEFAULT_VOICE_INPUT_METHOD, imeId); - } - - @Nullable - String getDefaultVoiceInputMethod() { - final String imi = getString(Settings.Secure.DEFAULT_VOICE_INPUT_METHOD, null); - if (DEBUG) { - Slog.d(TAG, "getDefaultVoiceInputMethodStr: " + imi); - } - return imi; - } - - boolean isSubtypeSelected() { - return getSelectedInputMethodSubtypeHashCode() != NOT_A_SUBTYPE_ID; - } - - private int getSelectedInputMethodSubtypeHashCode() { - return getInt(Settings.Secure.SELECTED_INPUT_METHOD_SUBTYPE, NOT_A_SUBTYPE_ID); - } - - @UserIdInt - public int getCurrentUserId() { - return mCurrentUserId; - } - - int getSelectedInputMethodSubtypeId(String selectedImiId) { - final InputMethodInfo imi = mMethodMap.get(selectedImiId); - if (imi == null) { - return NOT_A_SUBTYPE_ID; - } - final int subtypeHashCode = getSelectedInputMethodSubtypeHashCode(); - return SubtypeUtils.getSubtypeIdFromHashCode(imi, subtypeHashCode); - } - - void saveCurrentInputMethodAndSubtypeToHistory(String curMethodId, - InputMethodSubtype currentSubtype) { - String subtypeId = NOT_A_SUBTYPE_ID_STR; - if (currentSubtype != null) { - subtypeId = String.valueOf(currentSubtype.hashCode()); - } - if (canAddToLastInputMethod(currentSubtype)) { - addSubtypeToHistory(curMethodId, subtypeId); - } - } - - /** - * A variant of {@link InputMethodManagerService#getCurrentInputMethodSubtypeLocked()} for - * non-current users. - * - * <p>TODO: Address code duplication between this and - * {@link InputMethodManagerService#getCurrentInputMethodSubtypeLocked()}.</p> - * - * @return {@link InputMethodSubtype} if exists. {@code null} otherwise. - */ - @Nullable - InputMethodSubtype getCurrentInputMethodSubtypeForNonCurrentUsers() { - final String selectedMethodId = getSelectedInputMethod(); - if (selectedMethodId == null) { - return null; - } - final InputMethodInfo imi = mMethodMap.get(selectedMethodId); - if (imi == null || imi.getSubtypeCount() == 0) { - return null; - } - - final int subtypeHashCode = getSelectedInputMethodSubtypeHashCode(); - if (subtypeHashCode != InputMethodUtils.NOT_A_SUBTYPE_ID) { - final int subtypeIndex = SubtypeUtils.getSubtypeIdFromHashCode(imi, - subtypeHashCode); - if (subtypeIndex >= 0) { - return imi.getSubtypeAt(subtypeIndex); - } - } - - // 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); - // If there is only one explicitly or implicitly enabled subtype, just returns it. - if (explicitlyOrImplicitlyEnabledSubtypes.isEmpty()) { - return null; - } - if (explicitlyOrImplicitlyEnabledSubtypes.size() == 1) { - return explicitlyOrImplicitlyEnabledSubtypes.get(0); - } - final String locale = SystemLocaleWrapper.get(mCurrentUserId).get(0).toString(); - final InputMethodSubtype subtype = SubtypeUtils.findLastResortApplicableSubtypeLocked( - explicitlyOrImplicitlyEnabledSubtypes, SubtypeUtils.SUBTYPE_MODE_KEYBOARD, - locale, true); - if (subtype != null) { - return subtype; - } - return SubtypeUtils.findLastResortApplicableSubtypeLocked( - explicitlyOrImplicitlyEnabledSubtypes, null, locale, true); - } - - boolean setAdditionalInputMethodSubtypes(@NonNull String imeId, - @NonNull ArrayList<InputMethodSubtype> subtypes, - @NonNull ArrayMap<String, List<InputMethodSubtype>> additionalSubtypeMap, - @NonNull PackageManagerInternal packageManagerInternal, int callingUid) { - final InputMethodInfo imi = mMethodMap.get(imeId); - if (imi == null) { - return false; - } - if (!InputMethodUtils.checkIfPackageBelongsToUid(packageManagerInternal, callingUid, - imi.getPackageName())) { - return false; - } - - if (subtypes.isEmpty()) { - additionalSubtypeMap.remove(imi.getId()); - } else { - additionalSubtypeMap.put(imi.getId(), subtypes); - } - AdditionalSubtypeUtils.save(additionalSubtypeMap, mMethodMap, getCurrentUserId()); - return true; - } - - boolean setEnabledInputMethodSubtypes(@NonNull String imeId, - @NonNull int[] subtypeHashCodes) { - final InputMethodInfo imi = mMethodMap.get(imeId); - if (imi == null) { - return false; - } - - final IntArray validSubtypeHashCodes = new IntArray(subtypeHashCodes.length); - for (int subtypeHashCode : subtypeHashCodes) { - if (subtypeHashCode == NOT_A_SUBTYPE_ID) { - continue; // NOT_A_SUBTYPE_ID must not be saved - } - if (!SubtypeUtils.isValidSubtypeId(imi, subtypeHashCode)) { - continue; // this subtype does not exist in InputMethodInfo. - } - if (validSubtypeHashCodes.indexOf(subtypeHashCode) >= 0) { - continue; // The entry is already added. No need to add anymore. - } - validSubtypeHashCodes.add(subtypeHashCode); - } - - final String originalEnabledImesString = getEnabledInputMethodsStr(); - final String updatedEnabledImesString = updateEnabledImeString( - originalEnabledImesString, imi.getId(), validSubtypeHashCodes); - if (TextUtils.equals(originalEnabledImesString, updatedEnabledImesString)) { - return false; - } - - putEnabledInputMethodsStr(updatedEnabledImesString); - return true; - } - - @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE) - static String updateEnabledImeString(@NonNull String enabledImesString, - @NonNull String imeId, @NonNull IntArray enabledSubtypeHashCodes) { - final TextUtils.SimpleStringSplitter imeSplitter = - new TextUtils.SimpleStringSplitter(INPUT_METHOD_SEPARATOR); - final TextUtils.SimpleStringSplitter imeSubtypeSplitter = - new TextUtils.SimpleStringSplitter(INPUT_METHOD_SUBTYPE_SEPARATOR); - - final StringBuilder sb = new StringBuilder(); - - imeSplitter.setString(enabledImesString); - boolean needsImeSeparator = false; - while (imeSplitter.hasNext()) { - final String nextImsStr = imeSplitter.next(); - imeSubtypeSplitter.setString(nextImsStr); - if (imeSubtypeSplitter.hasNext()) { - if (needsImeSeparator) { - sb.append(INPUT_METHOD_SEPARATOR); - } - if (TextUtils.equals(imeId, imeSubtypeSplitter.next())) { - sb.append(imeId); - for (int i = 0; i < enabledSubtypeHashCodes.size(); ++i) { - sb.append(INPUT_METHOD_SUBTYPE_SEPARATOR); - sb.append(enabledSubtypeHashCodes.get(i)); - } - } else { - sb.append(nextImsStr); - } - needsImeSeparator = true; - } - } - return sb.toString(); - } - - public void dumpLocked(final Printer pw, final String prefix) { - pw.println(prefix + "mCurrentUserId=" + mCurrentUserId); - } - } - static boolean isSoftInputModeStateVisibleAllowed(int targetSdkVersion, @StartInputFlags int startInputFlags) { if (targetSdkVersion < Build.VERSION_CODES.P) { diff --git a/services/core/java/com/android/server/media/MediaFeatureFlagManager.java b/services/core/java/com/android/server/media/MediaFeatureFlagManager.java deleted file mode 100644 index f90f64a19301..000000000000 --- a/services/core/java/com/android/server/media/MediaFeatureFlagManager.java +++ /dev/null @@ -1,94 +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.server.media; - -import android.annotation.StringDef; -import android.app.ActivityThread; -import android.app.Application; -import android.provider.DeviceConfig; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -/* package */ class MediaFeatureFlagManager { - - /** - * Namespace for media better together features. - */ - private static final String NAMESPACE_MEDIA_BETTER_TOGETHER = "media_better_together"; - - @StringDef( - prefix = "FEATURE_", - value = { - FEATURE_SCANNING_MINIMUM_PACKAGE_IMPORTANCE - }) - @Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) - @Retention(RetentionPolicy.SOURCE) - /* package */ @interface MediaFeatureFlag {} - - /** - * Whether to use IMPORTANCE_FOREGROUND (i.e. 100) or IMPORTANCE_FOREGROUND_SERVICE (i.e. 125) - * as the minimum package importance for scanning. - */ - /* package */ static final @MediaFeatureFlag String - FEATURE_SCANNING_MINIMUM_PACKAGE_IMPORTANCE = "scanning_package_minimum_importance"; - - private static final MediaFeatureFlagManager sInstance = new MediaFeatureFlagManager(); - - private MediaFeatureFlagManager() { - // Empty to prevent instantiation. - } - - /* package */ static MediaFeatureFlagManager getInstance() { - return sInstance; - } - - /** - * Returns a boolean value from {@link DeviceConfig} from the system_time namespace, or - * {@code defaultValue} if there is no explicit value set. - */ - public boolean getBoolean(@MediaFeatureFlag String key, boolean defaultValue) { - return DeviceConfig.getBoolean(NAMESPACE_MEDIA_BETTER_TOGETHER, key, defaultValue); - } - - /** - * Returns an int value from {@link DeviceConfig} from the system_time namespace, or {@code - * defaultValue} if there is no explicit value set. - */ - public int getInt(@MediaFeatureFlag String key, int defaultValue) { - return DeviceConfig.getInt(NAMESPACE_MEDIA_BETTER_TOGETHER, key, defaultValue); - } - - /** - * Adds a listener to react for changes in media feature flags values. Future calls to this - * method with the same listener will replace the old namespace and executor. - * - * @param onPropertiesChangedListener The listener to add. - */ - public void addOnPropertiesChangedListener( - DeviceConfig.OnPropertiesChangedListener onPropertiesChangedListener) { - Application currentApplication = ActivityThread.currentApplication(); - if (currentApplication != null) { - DeviceConfig.addOnPropertiesChangedListener( - NAMESPACE_MEDIA_BETTER_TOGETHER, - currentApplication.getMainExecutor(), - onPropertiesChangedListener); - } - } -} diff --git a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java index e048522eee53..28a1c7ad0540 100644 --- a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java +++ b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java @@ -16,7 +16,7 @@ package com.android.server.media; -import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND_SERVICE; +import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND; import static android.content.Intent.ACTION_SCREEN_OFF; import static android.content.Intent.ACTION_SCREEN_ON; import static android.media.MediaRoute2ProviderService.REASON_UNKNOWN_ERROR; @@ -24,7 +24,6 @@ import static android.media.MediaRouter2Utils.getOriginalId; import static android.media.MediaRouter2Utils.getProviderId; import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage; -import static com.android.server.media.MediaFeatureFlagManager.FEATURE_SCANNING_MINIMUM_PACKAGE_IMPORTANCE; import android.Manifest; import android.annotation.NonNull; @@ -55,7 +54,6 @@ import android.os.Looper; import android.os.PowerManager; import android.os.RemoteException; import android.os.UserHandle; -import android.provider.DeviceConfig; import android.text.TextUtils; import android.util.ArrayMap; import android.util.Log; @@ -97,11 +95,7 @@ class MediaRouter2ServiceImpl { // in MediaRouter2, remove this constant and replace the usages with the real request IDs. private static final long DUMMY_REQUEST_ID = -1; - private static int sPackageImportanceForScanning = - MediaFeatureFlagManager.getInstance() - .getInt( - FEATURE_SCANNING_MINIMUM_PACKAGE_IMPORTANCE, - IMPORTANCE_FOREGROUND_SERVICE); + private static final int REQUIRED_PACKAGE_IMPORTANCE_FOR_SCANNING = IMPORTANCE_FOREGROUND; /** * Contains the list of bluetooth permissions that are required to do system routing. @@ -159,7 +153,7 @@ class MediaRouter2ServiceImpl { mContext = context; mActivityManager = mContext.getSystemService(ActivityManager.class); mActivityManager.addOnUidImportanceListener(mOnUidImportanceListener, - sPackageImportanceForScanning); + REQUIRED_PACKAGE_IMPORTANCE_FOR_SCANNING); mPowerManager = mContext.getSystemService(PowerManager.class); mUserManagerInternal = LocalServices.getService(UserManagerInternal.class); @@ -171,9 +165,6 @@ class MediaRouter2ServiceImpl { } mContext.getPackageManager().addOnPermissionsChangeListener(this::onPermissionsChanged); - - MediaFeatureFlagManager.getInstance() - .addOnPropertiesChangedListener(this::onDeviceConfigChange); } /** @@ -1443,8 +1434,13 @@ class MediaRouter2ServiceImpl { return; } - Slog.i(TAG, TextUtils.formatSimple( - "startScan | manager: %d", managerRecord.mManagerId)); + Slog.i( + TAG, + TextUtils.formatSimple( + "startScan | manager: %d, ownerPackageName: %s, targetPackageName: %s", + managerRecord.mManagerId, + managerRecord.mOwnerPackageName, + managerRecord.mTargetPackageName)); managerRecord.startScan(); } @@ -1457,8 +1453,13 @@ class MediaRouter2ServiceImpl { return; } - Slog.i(TAG, TextUtils.formatSimple( - "stopScan | manager: %d", managerRecord.mManagerId)); + Slog.i( + TAG, + TextUtils.formatSimple( + "stopScan | manager: %d, ownerPackageName: %s, targetPackageName: %s", + managerRecord.mManagerId, + managerRecord.mOwnerPackageName, + managerRecord.mTargetPackageName)); managerRecord.stopScan(); } @@ -1725,13 +1726,6 @@ class MediaRouter2ServiceImpl { // End of locked methods that are used by both MediaRouter2 and MediaRouter2Manager. - private void onDeviceConfigChange(@NonNull DeviceConfig.Properties properties) { - sPackageImportanceForScanning = - properties.getInt( - /* name */ FEATURE_SCANNING_MINIMUM_PACKAGE_IMPORTANCE, - /* defaultValue */ IMPORTANCE_FOREGROUND_SERVICE); - } - static long toUniqueRequestId(int requesterId, int originalRequestId) { return ((long) requesterId << 32) | originalRequestId; } @@ -3170,7 +3164,7 @@ class MediaRouter2ServiceImpl { record -> service.mActivityManager.getPackageImportance( record.mPackageName) - <= sPackageImportanceForScanning) + <= REQUIRED_PACKAGE_IMPORTANCE_FOR_SCANNING) .collect(Collectors.toList()); } @@ -3187,7 +3181,7 @@ class MediaRouter2ServiceImpl { manager.mIsScanning && service.mActivityManager.getPackageImportance( manager.mOwnerPackageName) - <= sPackageImportanceForScanning); + <= REQUIRED_PACKAGE_IMPORTANCE_FOR_SCANNING); } private MediaRoute2Provider findProvider(@Nullable String providerId) { diff --git a/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java b/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java index f6571d94d554..550aed51c8e2 100644 --- a/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java +++ b/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java @@ -304,7 +304,7 @@ public final class MediaProjectionManagerService extends SystemService } @VisibleForTesting - void addCallback(final IMediaProjectionWatcherCallback callback) { + MediaProjectionInfo addCallback(final IMediaProjectionWatcherCallback callback) { IBinder.DeathRecipient deathRecipient = new IBinder.DeathRecipient() { @Override public void binderDied() { @@ -314,6 +314,7 @@ public final class MediaProjectionManagerService extends SystemService synchronized (mLock) { mCallbackDelegate.add(callback); linkDeathRecipientLocked(callback, deathRecipient); + return mProjectionGrant != null ? mProjectionGrant.getProjectionInfo() : null; } } @@ -786,11 +787,11 @@ public final class MediaProjectionManagerService extends SystemService @Override //Binder call @EnforcePermission(MANAGE_MEDIA_PROJECTION) - public void addCallback(final IMediaProjectionWatcherCallback callback) { + public MediaProjectionInfo addCallback(final IMediaProjectionWatcherCallback callback) { addCallback_enforcePermission(); final long token = Binder.clearCallingIdentity(); try { - MediaProjectionManagerService.this.addCallback(callback); + return MediaProjectionManagerService.this.addCallback(callback); } finally { Binder.restoreCallingIdentity(token); } @@ -1244,7 +1245,7 @@ public final class MediaProjectionManagerService extends SystemService } public MediaProjectionInfo getProjectionInfo() { - return new MediaProjectionInfo(packageName, userHandle); + return new MediaProjectionInfo(packageName, userHandle, mLaunchCookie); } boolean requiresForegroundService() { diff --git a/services/core/java/com/android/server/net/NetworkPolicyLogger.java b/services/core/java/com/android/server/net/NetworkPolicyLogger.java index 4d19eade5a05..d7188c7f10c6 100644 --- a/services/core/java/com/android/server/net/NetworkPolicyLogger.java +++ b/services/core/java/com/android/server/net/NetworkPolicyLogger.java @@ -42,7 +42,6 @@ import android.util.ArraySet; import android.util.Log; import android.util.Slog; -import com.android.internal.annotations.Keep; import com.android.internal.util.IndentingPrintWriter; import com.android.internal.util.RingBuffer; import com.android.server.am.ProcessList; @@ -414,7 +413,7 @@ public class NetworkPolicyLogger { private static final Date sDate = new Date(); public LogBuffer(int capacity) { - super(Data.class, capacity); + super(Data::new, Data[]::new, capacity); } public void uidStateChanged(int uid, int procState, long procStateSeq, @@ -690,12 +689,8 @@ public class NetworkPolicyLogger { /** * Container class for all networkpolicy events data. - * - * Note: This class needs to be public for RingBuffer class to be able to create - * new instances of this. */ - @Keep - public static final class Data { + private static final class Data { public int type; public long timeStamp; diff --git a/services/core/java/com/android/server/notification/DefaultDeviceEffectsApplier.java b/services/core/java/com/android/server/notification/DefaultDeviceEffectsApplier.java index 71a6b5ed0581..ab650afe68a7 100644 --- a/services/core/java/com/android/server/notification/DefaultDeviceEffectsApplier.java +++ b/services/core/java/com/android/server/notification/DefaultDeviceEffectsApplier.java @@ -16,7 +16,8 @@ package com.android.server.notification; -import static android.app.UiModeManager.MODE_NIGHT_CUSTOM_TYPE_BEDTIME; +import static android.app.UiModeManager.MODE_ATTENTION_THEME_OVERLAY_NIGHT; +import static android.app.UiModeManager.MODE_ATTENTION_THEME_OVERLAY_OFF; import android.app.UiModeManager; import android.app.WallpaperManager; @@ -128,10 +129,9 @@ class DefaultDeviceEffectsApplier implements DeviceEffectsApplier { private void updateNightModeImmediately(boolean useNightMode) { Binder.withCleanCallingIdentity(() -> { - // TODO: b/314285749 - Placeholder; use real APIs when available. - mUiModeManager.setNightModeCustomType(MODE_NIGHT_CUSTOM_TYPE_BEDTIME); - mUiModeManager.setNightModeActivatedForCustomMode(MODE_NIGHT_CUSTOM_TYPE_BEDTIME, - useNightMode); + mUiModeManager.setAttentionModeThemeOverlay( + useNightMode ? MODE_ATTENTION_THEME_OVERLAY_NIGHT + : MODE_ATTENTION_THEME_OVERLAY_OFF); }); } diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java index 9ddc362769f6..2ae040a69583 100755 --- a/services/core/java/com/android/server/notification/NotificationManagerService.java +++ b/services/core/java/com/android/server/notification/NotificationManagerService.java @@ -5931,8 +5931,7 @@ public class NotificationManagerService extends SystemService { newVisualEffects, policy.priorityConversationSenders); if (shouldApplyAsImplicitRule) { - mZenModeHelper.applyGlobalPolicyAsImplicitZenRule(pkg, callingUid, policy, - origin); + mZenModeHelper.applyGlobalPolicyAsImplicitZenRule(pkg, callingUid, policy); } else { ZenLog.traceSetNotificationPolicy(pkg, applicationInfo.targetSdkVersion, policy); @@ -12103,6 +12102,7 @@ public class NotificationManagerService extends SystemService { return true; } + long token = Binder.clearCallingIdentity(); try { if (mPackageManager.checkUidPermission(RECEIVE_SENSITIVE_NOTIFICATIONS, uid) == PERMISSION_GRANTED || mPackageManagerInternal.isPlatformSigned(pkg)) { @@ -12129,6 +12129,8 @@ public class NotificationManagerService extends SystemService { } } catch (RemoteException e) { Slog.e(TAG, "Failed to check trusted status of listener", e); + } finally { + Binder.restoreCallingIdentity(token); } return false; } diff --git a/services/core/java/com/android/server/notification/ZenAdapters.java b/services/core/java/com/android/server/notification/ZenAdapters.java index 91df04c4f2cb..37b263c3e3bd 100644 --- a/services/core/java/com/android/server/notification/ZenAdapters.java +++ b/services/core/java/com/android/server/notification/ZenAdapters.java @@ -59,9 +59,7 @@ class ZenAdapters { } if (Flags.modesApi()) { - zenPolicyBuilder.allowChannels( - policy.allowPriorityChannels() - ? ZenPolicy.CHANNEL_TYPE_PRIORITY : ZenPolicy.CHANNEL_TYPE_NONE); + zenPolicyBuilder.allowPriorityChannels(policy.allowPriorityChannels()); } return zenPolicyBuilder.build(); diff --git a/services/core/java/com/android/server/notification/ZenModeEventLogger.java b/services/core/java/com/android/server/notification/ZenModeEventLogger.java index 0145577fb945..a90efe6f54f8 100644 --- a/services/core/java/com/android/server/notification/ZenModeEventLogger.java +++ b/services/core/java/com/android/server/notification/ZenModeEventLogger.java @@ -18,6 +18,8 @@ package com.android.server.notification; import static android.app.NotificationManager.Policy.STATE_CHANNELS_BYPASSING_DND; import static android.provider.Settings.Global.ZEN_MODE_OFF; +import static android.service.notification.NotificationServiceProto.CHANNEL_POLICY_PRIORITY; +import static android.service.notification.NotificationServiceProto.CHANNEL_POLICY_NONE; import static android.service.notification.NotificationServiceProto.RULE_TYPE_AUTOMATIC; import static android.service.notification.NotificationServiceProto.RULE_TYPE_MANUAL; import static android.service.notification.NotificationServiceProto.RULE_TYPE_UNKNOWN; @@ -551,8 +553,8 @@ class ZenModeEventLogger { if (Flags.modesApi()) { proto.write(DNDPolicyProto.ALLOW_CHANNELS, mNewPolicy.allowPriorityChannels() - ? ZenPolicy.CHANNEL_TYPE_PRIORITY - : ZenPolicy.CHANNEL_TYPE_NONE); + ? CHANNEL_POLICY_PRIORITY + : CHANNEL_POLICY_NONE); } } else { Log.wtf(TAG, "attempted to write zen mode log event with null policy"); diff --git a/services/core/java/com/android/server/notification/ZenModeHelper.java b/services/core/java/com/android/server/notification/ZenModeHelper.java index afbf08d9b77d..93ffd974bb80 100644 --- a/services/core/java/com/android/server/notification/ZenModeHelper.java +++ b/services/core/java/com/android/server/notification/ZenModeHelper.java @@ -32,6 +32,7 @@ import static android.service.notification.ZenModeConfig.UPDATE_ORIGIN_SYSTEM_OR import static android.service.notification.ZenModeConfig.UPDATE_ORIGIN_USER; import static com.android.internal.util.FrameworkStatsLog.DND_MODE_RULE; +import static com.android.internal.util.Preconditions.checkArgument; import android.annotation.DrawableRes; import android.annotation.NonNull; @@ -427,6 +428,7 @@ public class ZenModeHelper { public String addAutomaticZenRule(String pkg, AutomaticZenRule automaticZenRule, @ConfigChangeOrigin int origin, String reason, int callingUid) { + requirePublicOrigin("addAutomaticZenRule", origin); if (!ZenModeConfig.SYSTEM_AUTHORITY.equals(pkg)) { PackageItemInfo component = getServiceInfo(automaticZenRule.getOwner()); if (component == null) { @@ -525,6 +527,7 @@ public class ZenModeHelper { public boolean updateAutomaticZenRule(String ruleId, AutomaticZenRule automaticZenRule, @ConfigChangeOrigin int origin, String reason, int callingUid) { + requirePublicOrigin("updateAutomaticZenRule", origin); ZenModeConfig newConfig; synchronized (mConfigLock) { if (mConfig == null) return false; @@ -602,7 +605,11 @@ public class ZenModeHelper { rule = newImplicitZenRule(callingPkg); newConfig.automaticRules.put(rule.id, rule); } - rule.zenMode = zenMode; + // If the user has changed the rule's *zenMode*, then don't let app overwrite it. + // We allow the update if the user has only changed other aspects of the rule. + if ((rule.userModifiedFields & AutomaticZenRule.FIELD_INTERRUPTION_FILTER) == 0) { + rule.zenMode = zenMode; + } rule.snoozing = false; rule.condition = new Condition(rule.conditionId, mContext.getString(R.string.zen_mode_implicit_activated), @@ -625,7 +632,7 @@ public class ZenModeHelper { * {@link Global#ZEN_MODE_IMPORTANT_INTERRUPTIONS}. */ void applyGlobalPolicyAsImplicitZenRule(String callingPkg, int callingUid, - NotificationManager.Policy policy, @ConfigChangeOrigin int origin) { + NotificationManager.Policy policy) { if (!android.app.Flags.modesApi()) { Log.wtf(TAG, "applyGlobalPolicyAsImplicitZenRule called with flag off!"); return; @@ -641,10 +648,17 @@ public class ZenModeHelper { rule.zenMode = Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS; newConfig.automaticRules.put(rule.id, rule); } - // TODO: b/308673679 - Keep user customization of this rule! - rule.zenPolicy = ZenAdapters.notificationPolicyToZenPolicy(policy); - setConfigLocked(newConfig, /* triggeringComponent= */ null, origin, - "applyGlobalPolicyAsImplicitZenRule", callingUid); + // 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) { + updatePolicy( + rule, + ZenAdapters.notificationPolicyToZenPolicy(policy), + /* updateBitmask= */ false); + + setConfigLocked(newConfig, /* triggeringComponent= */ null, UPDATE_ORIGIN_APP, + "applyGlobalPolicyAsImplicitZenRule", callingUid); + } } } @@ -726,6 +740,7 @@ public class ZenModeHelper { boolean removeAutomaticZenRule(String id, @ConfigChangeOrigin int origin, String reason, int callingUid) { + requirePublicOrigin("removeAutomaticZenRule", origin); ZenModeConfig newConfig; synchronized (mConfigLock) { if (mConfig == null) return false; @@ -758,6 +773,7 @@ public class ZenModeHelper { boolean removeAutomaticZenRules(String packageName, @ConfigChangeOrigin int origin, String reason, int callingUid) { + requirePublicOrigin("removeAutomaticZenRules", origin); ZenModeConfig newConfig; synchronized (mConfigLock) { if (mConfig == null) return false; @@ -806,6 +822,7 @@ public class ZenModeHelper { void setAutomaticZenRuleState(String id, Condition condition, @ConfigChangeOrigin int origin, int callingUid) { + requirePublicOrigin("setAutomaticZenRuleState", origin); ZenModeConfig newConfig; synchronized (mConfigLock) { if (mConfig == null) return; @@ -819,6 +836,7 @@ public class ZenModeHelper { void setAutomaticZenRuleState(Uri ruleDefinition, Condition condition, @ConfigChangeOrigin int origin, int callingUid) { + requirePublicOrigin("setAutomaticZenRuleState", origin); ZenModeConfig newConfig; synchronized (mConfigLock) { if (mConfig == null) return; @@ -988,7 +1006,7 @@ public class ZenModeHelper { return null; } - void populateZenRule(String pkg, AutomaticZenRule automaticZenRule, ZenRule rule, + private void populateZenRule(String pkg, AutomaticZenRule automaticZenRule, ZenRule rule, @ConfigChangeOrigin int origin, boolean isNew) { if (Flags.modesApi()) { // These values can always be edited by the app, so we apply changes immediately. @@ -1053,11 +1071,9 @@ public class ZenModeHelper { rule.zenMode = newZenMode; // Updates the bitmask and values for all policy fields, based on the origin. - rule.zenPolicy = updatePolicy(rule.zenPolicy, automaticZenRule.getZenPolicy(), - updateBitmask); + updatePolicy(rule, automaticZenRule.getZenPolicy(), updateBitmask); // Updates the bitmask and values for all device effect fields, based on the origin. - rule.zenDeviceEffects = updateZenDeviceEffects( - rule.zenDeviceEffects, automaticZenRule.getDeviceEffects(), + updateZenDeviceEffects(rule, automaticZenRule.getDeviceEffects(), origin == UPDATE_ORIGIN_APP, updateBitmask); } else { if (rule.enabled != automaticZenRule.isEnabled()) { @@ -1069,13 +1085,6 @@ public class ZenModeHelper { rule.enabled = automaticZenRule.isEnabled(); rule.modified = automaticZenRule.isModified(); rule.zenPolicy = automaticZenRule.getZenPolicy(); - if (Flags.modesApi()) { - rule.zenDeviceEffects = updateZenDeviceEffects( - rule.zenDeviceEffects, - automaticZenRule.getDeviceEffects(), - origin == UPDATE_ORIGIN_APP, - origin == UPDATE_ORIGIN_USER); - } rule.zenMode = NotificationManager.zenModeFromInterruptionFilter( automaticZenRule.getInterruptionFilter(), Global.ZEN_MODE_OFF); rule.configurationActivity = automaticZenRule.getConfigurationActivity(); @@ -1099,28 +1108,28 @@ public class ZenModeHelper { } /** - * Modifies {@link ZenPolicy} that is being stored as part of a new or updated ZenRule. - * Returns a policy based on {@code oldPolicy}, but with fields updated to match - * {@code newPolicy} where they differ, and updating the internal user-modified bitmask to - * track these changes, if applicable based on {@code origin}. + * 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). */ - @Nullable - private ZenPolicy updatePolicy(@Nullable ZenPolicy oldPolicy, @Nullable ZenPolicy newPolicy, - boolean updateBitmask) { - // If the update is to make the policy null, we don't need to update the bitmask, - // because it won't be stored anywhere anyway. + private void updatePolicy(ZenRule zenRule, @Nullable ZenPolicy newPolicy, + boolean updateBitmask) { if (newPolicy == null) { - return null; + // TODO: b/319242206 - Treat as newPolicy == default policy and continue below. + zenRule.zenPolicy = null; + return; } // If oldPolicy is null, we compare against the default policy when determining which // fields in the bitmask should be marked as updated. - if (oldPolicy == null) { - oldPolicy = mDefaultConfig.toZenPolicy(); - } + ZenPolicy oldPolicy = + zenRule.zenPolicy != null ? zenRule.zenPolicy : mDefaultConfig.toZenPolicy(); + + zenRule.zenPolicy = newPolicy; - int userModifiedFields = oldPolicy.getUserModifiedFields(); if (updateBitmask) { + int userModifiedFields = zenRule.zenPolicyUserModifiedFields; if (oldPolicy.getPriorityMessageSenders() != newPolicy.getPriorityMessageSenders()) { userModifiedFields |= ZenPolicy.FIELD_MESSAGES; } @@ -1131,7 +1140,7 @@ public class ZenModeHelper { != newPolicy.getPriorityConversationSenders()) { userModifiedFields |= ZenPolicy.FIELD_CONVERSATIONS; } - if (oldPolicy.getAllowedChannels() != newPolicy.getAllowedChannels()) { + if (oldPolicy.getPriorityChannels() != newPolicy.getPriorityChannels()) { userModifiedFields |= ZenPolicy.FIELD_ALLOW_CHANNELS; } if (oldPolicy.getPriorityCategoryReminders() @@ -1178,66 +1187,47 @@ public class ZenModeHelper { != newPolicy.getVisualEffectNotificationList()) { userModifiedFields |= ZenPolicy.FIELD_VISUAL_EFFECT_NOTIFICATION_LIST; } + zenRule.zenPolicyUserModifiedFields = userModifiedFields; } - - // After all bitmask changes have been made, sets the bitmask. - return new ZenPolicy.Builder(newPolicy).setUserModifiedFields(userModifiedFields).build(); } /** - * Modifies {@link ZenDeviceEffects} that are being stored as part of a new or updated ZenRule. - * Returns a {@link ZenDeviceEffects} based on {@code oldEffects}, but with fields updated to - * match {@code newEffects} where they differ, and updating the internal user-modified bitmask - * to track these changes, if applicable based on {@code origin}. - * <ul> - * <li> Apps cannot turn on hidden effects (those tagged as {@code @hide}) since they are - * intended for platform-specific rules (e.g. wearables). If it's a new rule, we blank them - * out; if it's an update, we preserve the previous values. - * </ul> + * Modifies the {@link ZenDeviceEffects} associated to a new or updated ZenRule. + * + * <p>The new value is {@code newEffects}, 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>Apps cannot turn on hidden effects (those tagged as {@code @hide}), so those fields are + * treated especially: for a new rule, they are blanked out; for an updated rule, previous + * values are preserved. */ - @Nullable - private static ZenDeviceEffects updateZenDeviceEffects(@Nullable ZenDeviceEffects oldEffects, - @Nullable ZenDeviceEffects newEffects, - boolean isFromApp, - boolean updateBitmask) { + private static void updateZenDeviceEffects(ZenRule zenRule, + @Nullable ZenDeviceEffects newEffects, boolean isFromApp, boolean updateBitmask) { if (newEffects == null) { - return null; + zenRule.zenDeviceEffects = null; + return; } - // Since newEffects is not null, we want to adopt all the new provided device effects. - ZenDeviceEffects.Builder builder = new ZenDeviceEffects.Builder(newEffects); + ZenDeviceEffects oldEffects = zenRule.zenDeviceEffects != null + ? zenRule.zenDeviceEffects + : new ZenDeviceEffects.Builder().build(); if (isFromApp) { - if (oldEffects != null) { - // We can do this because we know we don't need to update the bitmask FROM_APP. - return builder - .setShouldDisableAutoBrightness(oldEffects.shouldDisableAutoBrightness()) - .setShouldDisableTapToWake(oldEffects.shouldDisableTapToWake()) - .setShouldDisableTiltToWake(oldEffects.shouldDisableTiltToWake()) - .setShouldDisableTouch(oldEffects.shouldDisableTouch()) - .setShouldMinimizeRadioUsage(oldEffects.shouldMinimizeRadioUsage()) - .setShouldMaximizeDoze(oldEffects.shouldMaximizeDoze()) - .build(); - } else { - return builder - .setShouldDisableAutoBrightness(false) - .setShouldDisableTapToWake(false) - .setShouldDisableTiltToWake(false) - .setShouldDisableTouch(false) - .setShouldMinimizeRadioUsage(false) - .setShouldMaximizeDoze(false) - .build(); - } + // Don't allow apps to toggle hidden effects. + newEffects = new ZenDeviceEffects.Builder(newEffects) + .setShouldDisableAutoBrightness(oldEffects.shouldDisableAutoBrightness()) + .setShouldDisableTapToWake(oldEffects.shouldDisableTapToWake()) + .setShouldDisableTiltToWake(oldEffects.shouldDisableTiltToWake()) + .setShouldDisableTouch(oldEffects.shouldDisableTouch()) + .setShouldMinimizeRadioUsage(oldEffects.shouldMinimizeRadioUsage()) + .setShouldMaximizeDoze(oldEffects.shouldMaximizeDoze()) + .build(); } - // If oldEffects is null, we compare against the default device effects object when - // determining which fields in the bitmask should be marked as updated. - if (oldEffects == null) { - oldEffects = new ZenDeviceEffects.Builder().build(); - } + zenRule.zenDeviceEffects = newEffects; - int userModifiedFields = oldEffects.getUserModifiedFields(); if (updateBitmask) { + int userModifiedFields = zenRule.zenDeviceEffectsUserModifiedFields; if (oldEffects.shouldDisplayGrayscale() != newEffects.shouldDisplayGrayscale()) { userModifiedFields |= ZenDeviceEffects.FIELD_GRAYSCALE; } @@ -1270,11 +1260,8 @@ public class ZenModeHelper { if (oldEffects.shouldMaximizeDoze() != newEffects.shouldMaximizeDoze()) { userModifiedFields |= ZenDeviceEffects.FIELD_MAXIMIZE_DOZE; } + zenRule.zenDeviceEffectsUserModifiedFields = userModifiedFields; } - - // Since newEffects is not null, we want to adopt all the new provided device effects. - // Set the usermodifiedFields value separately, to reflect the updated bitmask. - return builder.setUserModifiedFields(userModifiedFields).build(); } private AutomaticZenRule zenRuleToAutomaticZenRule(ZenRule rule) { @@ -1293,7 +1280,6 @@ public class ZenModeHelper { .setOwner(rule.component) .setConfigurationActivity(rule.configurationActivity) .setTriggerDescription(rule.triggerDescription) - .setUserModifiedFields(rule.userModifiedFields) .build(); } else { azr = new AutomaticZenRule(rule.name, rule.component, @@ -2369,6 +2355,19 @@ public class ZenModeHelper { return null; } } + + /** Checks that the {@code origin} supplied to a ZenModeHelper "API" method makes sense. */ + private static void requirePublicOrigin(String method, @ConfigChangeOrigin int origin) { + if (!Flags.modesApi()) { + return; + } + checkArgument(origin == UPDATE_ORIGIN_APP || origin == UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI + || origin == UPDATE_ORIGIN_USER, + "Expected one of UPDATE_ORIGIN_APP, UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, or " + + "UPDATE_ORIGIN_USER for %s, but received '%s'.", + method, origin); + } + private final class Metrics extends Callback { private static final String COUNTER_MODE_PREFIX = "dnd_mode_"; private static final String COUNTER_TYPE_PREFIX = "dnd_type_"; diff --git a/services/core/java/com/android/server/os/NativeTombstoneManager.java b/services/core/java/com/android/server/os/NativeTombstoneManager.java index 9ce3cb3abe4a..f6e7ef3d50e9 100644 --- a/services/core/java/com/android/server/os/NativeTombstoneManager.java +++ b/services/core/java/com/android/server/os/NativeTombstoneManager.java @@ -41,13 +41,14 @@ import android.system.Os; import android.system.StructStat; import android.util.Slog; import android.util.SparseArray; +import android.util.proto.ProtoInputStream; +import android.util.proto.ProtoParseException; import com.android.internal.annotations.GuardedBy; import com.android.server.BootReceiver; import com.android.server.ServiceThread; import com.android.server.os.TombstoneProtos.Cause; import com.android.server.os.TombstoneProtos.Tombstone; -import com.android.server.os.protobuf.CodedInputStream; import libcore.io.IoUtils; @@ -129,21 +130,18 @@ public final class NativeTombstoneManager { return; } + String processName = "UNKNOWN"; final boolean isProtoFile = filename.endsWith(".pb"); - if (!isProtoFile) { - return; - } + File protoPath = isProtoFile ? path : new File(path.getAbsolutePath() + ".pb"); - Optional<ParsedTombstone> parsedTombstone = handleProtoTombstone(path, true); + Optional<TombstoneFile> parsedTombstone = handleProtoTombstone(protoPath, isProtoFile); if (parsedTombstone.isPresent()) { - BootReceiver.addTombstoneToDropBox( - mContext, path, parsedTombstone.get().getTombstone(), - parsedTombstone.get().getProcessName(), mTmpFileLock); + processName = parsedTombstone.get().getProcessName(); } + BootReceiver.addTombstoneToDropBox(mContext, path, isProtoFile, processName, mTmpFileLock); } - private Optional<ParsedTombstone> handleProtoTombstone( - File path, boolean addToList) { + private Optional<TombstoneFile> handleProtoTombstone(File path, boolean addToList) { final String filename = path.getName(); if (!filename.endsWith(".pb")) { Slog.w(TAG, "unexpected tombstone name: " + path); @@ -173,7 +171,7 @@ public final class NativeTombstoneManager { return Optional.empty(); } - final Optional<ParsedTombstone> parsedTombstone = TombstoneFile.parse(pfd); + final Optional<TombstoneFile> parsedTombstone = TombstoneFile.parse(pfd); if (!parsedTombstone.isPresent()) { IoUtils.closeQuietly(pfd); return Optional.empty(); @@ -186,7 +184,7 @@ public final class NativeTombstoneManager { previous.dispose(); } - mTombstones.put(number, parsedTombstone.get().getTombstoneFile()); + mTombstones.put(number, parsedTombstone.get()); } } @@ -334,27 +332,6 @@ public final class NativeTombstoneManager { } } - static class ParsedTombstone { - TombstoneFile mTombstoneFile; - Tombstone mTombstone; - ParsedTombstone(TombstoneFile tombstoneFile, Tombstone tombstone) { - mTombstoneFile = tombstoneFile; - mTombstone = tombstone; - } - - public String getProcessName() { - return mTombstoneFile.getProcessName(); - } - - public TombstoneFile getTombstoneFile() { - return mTombstoneFile; - } - - public Tombstone getTombstone() { - return mTombstone; - } - } - static class TombstoneFile { final ParcelFileDescriptor mPfd; @@ -437,21 +414,67 @@ public final class NativeTombstoneManager { } } - static Optional<ParsedTombstone> parse(ParcelFileDescriptor pfd) { - Tombstone tombstoneProto; - try (FileInputStream is = new FileInputStream(pfd.getFileDescriptor())) { - final byte[] tombstoneBytes = is.readAllBytes(); + static Optional<TombstoneFile> parse(ParcelFileDescriptor pfd) { + final FileInputStream is = new FileInputStream(pfd.getFileDescriptor()); + final ProtoInputStream stream = new ProtoInputStream(is); + + int pid = 0; + int uid = 0; + String processName = null; + String crashReason = ""; + String selinuxLabel = ""; + + try { + while (stream.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + switch (stream.getFieldNumber()) { + case (int) Tombstone.PID: + pid = stream.readInt(Tombstone.PID); + break; + + case (int) Tombstone.UID: + uid = stream.readInt(Tombstone.UID); + break; + + case (int) Tombstone.COMMAND_LINE: + if (processName == null) { + processName = stream.readString(Tombstone.COMMAND_LINE); + } + break; - tombstoneProto = Tombstone.parseFrom( - CodedInputStream.newInstance(tombstoneBytes)); - } catch (IOException ex) { + case (int) Tombstone.CAUSES: + if (!crashReason.equals("")) { + // Causes appear in decreasing order of likelihood. For now we only + // want the most likely crash reason here, so ignore all others. + break; + } + long token = stream.start(Tombstone.CAUSES); + cause: + while (stream.nextField() != ProtoInputStream.NO_MORE_FIELDS) { + switch (stream.getFieldNumber()) { + case (int) Cause.HUMAN_READABLE: + crashReason = stream.readString(Cause.HUMAN_READABLE); + break cause; + + default: + break; + } + } + stream.end(token); + break; + + case (int) Tombstone.SELINUX_LABEL: + selinuxLabel = stream.readString(Tombstone.SELINUX_LABEL); + break; + + default: + break; + } + } + } catch (IOException | ProtoParseException ex) { Slog.e(TAG, "Failed to parse tombstone", ex); return Optional.empty(); } - int pid = tombstoneProto.getPid(); - int uid = tombstoneProto.getUid(); - if (!UserHandle.isApp(uid)) { Slog.e(TAG, "Tombstone's UID (" + uid + ") not an app, ignoring"); return Optional.empty(); @@ -468,7 +491,6 @@ public final class NativeTombstoneManager { final int userId = UserHandle.getUserId(uid); final int appId = UserHandle.getAppId(uid); - String selinuxLabel = tombstoneProto.getSelinuxLabel(); if (!selinuxLabel.startsWith("u:r:untrusted_app")) { Slog.e(TAG, "Tombstone has invalid selinux label (" + selinuxLabel + "), ignoring"); return Optional.empty(); @@ -480,30 +502,11 @@ public final class NativeTombstoneManager { result.mAppId = appId; result.mPid = pid; result.mUid = uid; - result.mProcessName = getCmdLineProcessName(tombstoneProto); + result.mProcessName = processName == null ? "" : processName; result.mTimestampMs = timestampMs; - result.mCrashReason = getCrashReason(tombstoneProto); + result.mCrashReason = crashReason; - return Optional.of(new ParsedTombstone(result, tombstoneProto)); - } - - private static String getCmdLineProcessName(Tombstone tombstoneProto) { - for (String cmdline : tombstoneProto.getCommandLineList()) { - if (cmdline != null) { - return cmdline; - } - } - return ""; - } - - private static String getCrashReason(Tombstone tombstoneProto) { - for (Cause cause : tombstoneProto.getCausesList()) { - if (cause.getHumanReadable() != null - && !cause.getHumanReadable().equals("")) { - return cause.getHumanReadable(); - } - } - return ""; + return Optional.of(result); } public IParcelFileDescriptorRetriever getPfdRetriever() { diff --git a/services/core/java/com/android/server/pm/ApexManager.java b/services/core/java/com/android/server/pm/ApexManager.java index 659c36c56047..5d71439e8f46 100644 --- a/services/core/java/com/android/server/pm/ApexManager.java +++ b/services/core/java/com/android/server/pm/ApexManager.java @@ -276,7 +276,8 @@ public abstract class ApexManager { * Returns list of {@code packageName} of apks inside the given apex. * @param apexPackageName Package name of the apk container of apex */ - abstract List<String> getApksInApex(String apexPackageName); + @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) + public abstract List<String> getApksInApex(String apexPackageName); /** * Returns the apex module name for the given package name, if the package is an APEX. Otherwise @@ -751,8 +752,9 @@ public abstract class ApexManager { } } + @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) @Override - List<String> getApksInApex(String apexPackageName) { + public List<String> getApksInApex(String apexPackageName) { synchronized (mLock) { Preconditions.checkState(mPackageNameToApexModuleName != null, "APEX packages have not been scanned"); diff --git a/services/core/java/com/android/server/pm/BackgroundInstallControlService.java b/services/core/java/com/android/server/pm/BackgroundInstallControlService.java index 7f0aadce3143..c110fb67b54f 100644 --- a/services/core/java/com/android/server/pm/BackgroundInstallControlService.java +++ b/services/core/java/com/android/server/pm/BackgroundInstallControlService.java @@ -75,11 +75,9 @@ public class BackgroundInstallControlService extends SystemService { private static final int MSG_PACKAGE_ADDED = 1; private static final int MSG_PACKAGE_REMOVED = 2; - private final Context mContext; private final BinderService mBinderService; private final PackageManager mPackageManager; private final PackageManagerInternal mPackageManagerInternal; - private final UsageStatsManagerInternal mUsageStatsManagerInternal; private final PermissionManagerServiceInternal mPermissionManager; private final Handler mHandler; private final File mDiskFile; @@ -99,14 +97,14 @@ public class BackgroundInstallControlService extends SystemService { @VisibleForTesting BackgroundInstallControlService(@NonNull Injector injector) { super(injector.getContext()); - mContext = injector.getContext(); mPackageManager = injector.getPackageManager(); mPackageManagerInternal = injector.getPackageManagerInternal(); mPermissionManager = injector.getPermissionManager(); mHandler = new EventHandler(injector.getLooper(), this); mDiskFile = injector.getDiskFile(); - mUsageStatsManagerInternal = injector.getUsageStatsManagerInternal(); - mUsageStatsManagerInternal.registerListener( + UsageStatsManagerInternal usageStatsManagerInternal = + injector.getUsageStatsManagerInternal(); + usageStatsManagerInternal.registerListener( (userId, event) -> mHandler.obtainMessage(MSG_USAGE_EVENT_RECEIVED, userId, diff --git a/services/core/java/com/android/server/pm/DataLoaderManagerService.java b/services/core/java/com/android/server/pm/DataLoaderManagerService.java index 888e1c26206c..c25cea65376b 100644 --- a/services/core/java/com/android/server/pm/DataLoaderManagerService.java +++ b/services/core/java/com/android/server/pm/DataLoaderManagerService.java @@ -47,7 +47,6 @@ import java.util.List; public class DataLoaderManagerService extends SystemService { private static final String TAG = "DataLoaderManager"; private final Context mContext; - private final HandlerThread mThread; private final Handler mHandler; private final DataLoaderManagerBinderService mBinderService; private final SparseArray<DataLoaderServiceConnection> mServiceConnections = @@ -57,10 +56,10 @@ public class DataLoaderManagerService extends SystemService { super(context); mContext = context; - mThread = new HandlerThread(TAG); - mThread.start(); + HandlerThread thread = new HandlerThread(TAG); + thread.start(); - mHandler = new Handler(mThread.getLooper()); + mHandler = new Handler(thread.getLooper()); mBinderService = new DataLoaderManagerBinderService(); } diff --git a/services/core/java/com/android/server/pm/IPackageManagerBase.java b/services/core/java/com/android/server/pm/IPackageManagerBase.java index e3bbd2d8d17e..f987d4ae8999 100644 --- a/services/core/java/com/android/server/pm/IPackageManagerBase.java +++ b/services/core/java/com/android/server/pm/IPackageManagerBase.java @@ -107,9 +107,6 @@ public abstract class IPackageManagerBase extends IPackageManager.Stub { @Nullable private final ComponentName mInstantAppResolverSettingsComponent; - @NonNull - private final String mRequiredSupplementalProcessPackage; - @Nullable private final String mServicesExtensionPackageName; @@ -125,7 +122,6 @@ public abstract class IPackageManagerBase extends IPackageManager.Stub { @NonNull PackageInstallerService installerService, @NonNull PackageProperty packageProperty, @NonNull ComponentName resolveComponentName, @Nullable ComponentName instantAppResolverSettingsComponent, - @NonNull String requiredSupplementalProcessPackage, @Nullable String servicesExtensionPackageName, @Nullable String sharedSystemSharedLibraryPackageName) { mService = service; @@ -140,7 +136,6 @@ public abstract class IPackageManagerBase extends IPackageManager.Stub { mPackageProperty = packageProperty; mResolveComponentName = resolveComponentName; mInstantAppResolverSettingsComponent = instantAppResolverSettingsComponent; - mRequiredSupplementalProcessPackage = requiredSupplementalProcessPackage; mServicesExtensionPackageName = servicesExtensionPackageName; mSharedSystemSharedLibraryPackageName = sharedSystemSharedLibraryPackageName; } diff --git a/services/core/java/com/android/server/pm/Installer.java b/services/core/java/com/android/server/pm/Installer.java index d5471cb01527..34903d1ed47d 100644 --- a/services/core/java/com/android/server/pm/Installer.java +++ b/services/core/java/com/android/server/pm/Installer.java @@ -1183,8 +1183,7 @@ public class Installer extends SystemService { * Returns an auth token for the provided writable FD. * * @param authFd a file descriptor to proof that the caller can write to the file. - * @param appUid uid of the calling app. - * @param userId id of the user whose app file to enable fs-verity. + * @param uid uid of the calling app. * * @return authToken, or null if a remote call shouldn't be continued. See {@link * #checkBeforeRemote}. @@ -1192,13 +1191,12 @@ public class Installer extends SystemService { * @throws InstallerException if the remote call failed. */ public IInstalld.IFsveritySetupAuthToken createFsveritySetupAuthToken( - ParcelFileDescriptor authFd, int appUid, @UserIdInt int userId) - throws InstallerException { + ParcelFileDescriptor authFd, int uid) throws InstallerException { if (!checkBeforeRemote()) { return null; } try { - return mInstalld.createFsveritySetupAuthToken(authFd, appUid, userId); + return mInstalld.createFsveritySetupAuthToken(authFd, uid); } catch (Exception e) { throw InstallerException.from(e); } diff --git a/services/core/java/com/android/server/pm/LauncherAppsService.java b/services/core/java/com/android/server/pm/LauncherAppsService.java index 991555495ad2..ac826afc1d22 100644 --- a/services/core/java/com/android/server/pm/LauncherAppsService.java +++ b/services/core/java/com/android/server/pm/LauncherAppsService.java @@ -32,6 +32,8 @@ import static android.content.pm.LauncherApps.FLAG_CACHE_BUBBLE_SHORTCUTS; import static android.content.pm.LauncherApps.FLAG_CACHE_NOTIFICATION_SHORTCUTS; import static android.content.pm.LauncherApps.FLAG_CACHE_PEOPLE_TILE_SHORTCUTS; +import static com.android.server.pm.PackageArchiver.isArchivingEnabled; + import android.annotation.AppIdInt; import android.annotation.NonNull; import android.annotation.Nullable; @@ -56,7 +58,6 @@ import android.content.IntentSender; import android.content.LocusId; import android.content.pm.ActivityInfo; import android.content.pm.ApplicationInfo; -import android.content.pm.Flags; import android.content.pm.ILauncherApps; import android.content.pm.IOnAppsChangedListener; import android.content.pm.IPackageInstallerCallback; @@ -507,7 +508,8 @@ public class LauncherAppsService extends SystemService { if (!canAccessProfile(userId, "cannot get shouldHideFromSuggestions")) { return false; } - if (Flags.archiving() && packageName != null && isPackageArchived(packageName, user)) { + if (isArchivingEnabled() && packageName != null + && isPackageArchived(packageName, user)) { return true; } if (mPackageManagerInternal.filterAppAccess( @@ -530,7 +532,7 @@ public class LauncherAppsService extends SystemService { .addCategory(Intent.CATEGORY_LAUNCHER) .setPackage(packageName), user); - if (Flags.archiving()) { + if (isArchivingEnabled()) { launcherActivities = getActivitiesForArchivedApp(packageName, user, launcherActivities); } @@ -701,7 +703,7 @@ public class LauncherAppsService extends SystemService { callingUid, user.getIdentifier()); if (activityInfo == null) { - if (Flags.archiving()) { + if (isArchivingEnabled()) { return getMatchingArchivedAppActivityInfo(component, user); } return null; @@ -984,7 +986,7 @@ public class LauncherAppsService extends SystemService { long callingFlag = PackageManager.MATCH_DIRECT_BOOT_AWARE | PackageManager.MATCH_DIRECT_BOOT_UNAWARE; - if (Flags.archiving()) { + if (isArchivingEnabled()) { callingFlag |= PackageManager.MATCH_ARCHIVED_PACKAGES; } final PackageInfo info = @@ -1457,7 +1459,7 @@ public class LauncherAppsService extends SystemService { if (!canAccessProfile(user.getIdentifier(), "Cannot check component")) { return false; } - if (Flags.archiving() && component != null && component.getPackageName() != null) { + if (isArchivingEnabled() && component != null && component.getPackageName() != null) { List<LauncherActivityInfoInternal> archiveActivities = generateLauncherActivitiesForArchivedApp(component.getPackageName(), user); if (!archiveActivities.isEmpty()) { @@ -1788,7 +1790,7 @@ public class LauncherAppsService extends SystemService { } if (!canLaunch && includeArchivedApps - && Flags.archiving() + && isArchivingEnabled() && getMatchingArchivedAppActivityInfo(component, user) != null) { launchIntent.setPackage(null); launchIntent.setComponent(component); diff --git a/services/core/java/com/android/server/pm/PackageArchiver.java b/services/core/java/com/android/server/pm/PackageArchiver.java index 3e5759a88213..09a91eda483a 100644 --- a/services/core/java/com/android/server/pm/PackageArchiver.java +++ b/services/core/java/com/android/server/pm/PackageArchiver.java @@ -51,12 +51,13 @@ import android.content.Intent; import android.content.IntentSender; import android.content.pm.ApplicationInfo; import android.content.pm.ArchivedActivityParcel; +import android.content.pm.ArchivedPackageInfo; import android.content.pm.ArchivedPackageParcel; +import android.content.pm.Flags; import android.content.pm.LauncherActivityInfo; import android.content.pm.LauncherApps; import android.content.pm.PackageInstaller; import android.content.pm.PackageManager; -import android.content.pm.PackageManager.DeleteFlags; import android.content.pm.ParceledListSlice; import android.content.pm.ResolveInfo; import android.content.pm.VersionedPackage; @@ -77,6 +78,7 @@ import android.os.ParcelableException; import android.os.Process; import android.os.RemoteException; import android.os.SELinux; +import android.os.SystemProperties; import android.os.UserHandle; import android.text.TextUtils; import android.util.ExceptionUtils; @@ -172,12 +174,15 @@ public class PackageArchiver { return userState.getArchiveState() != null && !userState.isInstalled(); } + public static boolean isArchivingEnabled() { + return Flags.archiving() || SystemProperties.getBoolean("pm.archiving.enabled", false); + } + void requestArchive( @NonNull String packageName, @NonNull String callerPackageName, @NonNull IntentSender intentSender, - @NonNull UserHandle userHandle, - @DeleteFlags int flags) { + @NonNull UserHandle userHandle) { Objects.requireNonNull(packageName); Objects.requireNonNull(callerPackageName); Objects.requireNonNull(intentSender); @@ -217,7 +222,7 @@ public class PackageArchiver { new VersionedPackage(packageName, PackageManager.VERSION_CODE_HIGHEST), callerPackageName, - DELETE_ARCHIVE | DELETE_KEEP_DATA | flags, + DELETE_ARCHIVE | DELETE_KEEP_DATA, intentSender, userId, binderUid); @@ -402,23 +407,30 @@ public class PackageArchiver { installerPackage, /* flags= */ 0, userId); if (installerInfo == null) { // Should never happen because we just fetched the installerInfo. - Slog.e(TAG, "Couldnt find installer " + installerPackage); + Slog.e(TAG, "Couldn't find installer " + installerPackage); return null; } + final int iconSize = mContext.getSystemService( + ActivityManager.class).getLauncherLargeIconSize(); + + var info = new ArchivedPackageInfo(archivedPackage); try { - var packageName = archivedPackage.packageName; - var mainActivities = archivedPackage.archivedActivities; - List<ArchiveActivityInfo> archiveActivityInfos = new ArrayList<>(mainActivities.length); - for (int i = 0, size = mainActivities.length; i < size; ++i) { - var mainActivity = mainActivities[i]; - Path iconPath = storeIconForParcel(packageName, mainActivity, userId, i); + var packageName = info.getPackageName(); + var mainActivities = info.getLauncherActivities(); + List<ArchiveActivityInfo> archiveActivityInfos = new ArrayList<>(mainActivities.size()); + for (int i = 0, size = mainActivities.size(); i < size; ++i) { + var mainActivity = mainActivities.get(i); + Path iconPath = storeDrawable( + packageName, mainActivity.getIcon(), userId, i, iconSize); + Path monochromePath = storeDrawable( + packageName, mainActivity.getMonochromeIcon(), userId, i, iconSize); ArchiveActivityInfo activityInfo = new ArchiveActivityInfo( - mainActivity.title, - mainActivity.originalComponentName, + mainActivity.getLabel().toString(), + mainActivity.getComponentName(), iconPath, - null); + monochromePath); archiveActivityInfos.add(activityInfo); } @@ -452,21 +464,6 @@ public class PackageArchiver { return new ArchiveState(archiveActivityInfos, installerTitle); } - // TODO(b/298452477) Handle monochrome icons. - private static Path storeIconForParcel(String packageName, ArchivedActivityParcel mainActivity, - @UserIdInt int userId, int index) throws IOException { - if (mainActivity.iconBitmap == null) { - return null; - } - File iconsDir = createIconsDir(packageName, userId); - File iconFile = new File(iconsDir, index + ".png"); - try (FileOutputStream out = new FileOutputStream(iconFile)) { - out.write(mainActivity.iconBitmap); - out.flush(); - } - return iconFile.toPath(); - } - @VisibleForTesting Path storeIcon(String packageName, LauncherActivityInfo mainActivity, @UserIdInt int userId, int index, int iconSize) throws IOException { @@ -475,9 +472,18 @@ public class PackageArchiver { // The app doesn't define an icon. No need to store anything. return null; } + return storeDrawable(packageName, mainActivity.getIcon(/* density= */ 0), userId, index, + iconSize); + } + + private static Path storeDrawable(String packageName, @Nullable Drawable iconDrawable, + @UserIdInt int userId, int index, int iconSize) throws IOException { + if (iconDrawable == null) { + return null; + } File iconsDir = createIconsDir(packageName, userId); File iconFile = new File(iconsDir, index + ".png"); - Bitmap icon = drawableToBitmap(mainActivity.getIcon(/* density= */ 0), iconSize); + Bitmap icon = drawableToBitmap(iconDrawable, iconSize); try (FileOutputStream out = new FileOutputStream(iconFile)) { // Note: Quality is ignored for PNGs. if (!icon.compress(Bitmap.CompressFormat.PNG, /* quality= */ 100, out)) { diff --git a/services/core/java/com/android/server/pm/PackageInstallerService.java b/services/core/java/com/android/server/pm/PackageInstallerService.java index fdcd28b0ed02..7bf9fe7aa7e2 100644 --- a/services/core/java/com/android/server/pm/PackageInstallerService.java +++ b/services/core/java/com/android/server/pm/PackageInstallerService.java @@ -30,6 +30,7 @@ import static android.content.pm.PackageManager.INSTALL_UNARCHIVE_DRAFT; import static android.os.Process.INVALID_UID; import static android.os.Process.SYSTEM_UID; +import static com.android.server.pm.PackageArchiver.isArchivingEnabled; import static com.android.server.pm.PackageManagerService.SHELL_PACKAGE_NAME; import static org.xmlpull.v1.XmlPullParser.END_DOCUMENT; @@ -826,7 +827,7 @@ public class PackageInstallerService extends IPackageInstaller.Stub implements } params.installFlags &= ~PackageManager.INSTALL_UNARCHIVE; - if (Flags.archiving() && params.appPackageName != null) { + if (isArchivingEnabled() && params.appPackageName != null) { PackageStateInternal ps = mPm.snapshotComputer().getPackageStateInternal( params.appPackageName, SYSTEM_UID); if (ps != null @@ -1034,7 +1035,7 @@ public class PackageInstallerService extends IPackageInstaller.Stub implements private int getExistingDraftSessionIdInternal(int installerUid, SessionParams sessionParams, int userId) { String appPackageName = sessionParams.appPackageName; - if (!Flags.archiving() || installerUid == INVALID_UID || appPackageName == null) { + if (!isArchivingEnabled() || installerUid == INVALID_UID || appPackageName == null) { return SessionInfo.INVALID_ID; } @@ -1407,14 +1408,11 @@ public class PackageInstallerService extends IPackageInstaller.Stub implements final PackageDeleteObserverAdapter adapter = new PackageDeleteObserverAdapter(mContext, statusReceiver, versionedPackage.getPackageName(), canSilentlyInstallPackage, userId, mPackageArchiver, flags); - final boolean shouldShowConfirmationDialog = - (flags & PackageManager.DELETE_SHOW_DIALOG) != 0; - if (!shouldShowConfirmationDialog - && mContext.checkCallingOrSelfPermission(Manifest.permission.DELETE_PACKAGES) - == PackageManager.PERMISSION_GRANTED) { + if (mContext.checkCallingOrSelfPermission(Manifest.permission.DELETE_PACKAGES) + == PackageManager.PERMISSION_GRANTED) { // Sweet, call straight through! mPm.deletePackageVersioned(versionedPackage, adapter.getBinder(), userId, flags); - } else if (!shouldShowConfirmationDialog && canSilentlyInstallPackage) { + } else if (canSilentlyInstallPackage) { // Allow the device owner and affiliated profile owner to silently delete packages // Need to clear the calling identity to get DELETE_PACKAGES permission final long ident = Binder.clearCallingIdentity(); @@ -1656,10 +1654,8 @@ public class PackageInstallerService extends IPackageInstaller.Stub implements @NonNull String packageName, @NonNull String callerPackageName, @NonNull IntentSender intentSender, - @NonNull UserHandle userHandle, - @DeleteFlags int flags) { - mPackageArchiver.requestArchive(packageName, callerPackageName, intentSender, - userHandle, flags); + @NonNull UserHandle userHandle) { + mPackageArchiver.requestArchive(packageName, callerPackageName, intentSender, userHandle); } @Override diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java index 5225529ef001..c5b5a761497d 100644 --- a/services/core/java/com/android/server/pm/PackageManagerService.java +++ b/services/core/java/com/android/server/pm/PackageManagerService.java @@ -4659,8 +4659,7 @@ public class PackageManagerService implements PackageSender, TestUtilityService mPreferredActivityHelper, mResolveIntentHelper, mDomainVerificationManager, mDomainVerificationConnection, mInstallerService, mPackageProperty, mResolveComponentName, mInstantAppResolverSettingsComponent, - mRequiredSdkSandboxPackage, mServicesExtensionPackageName, - mSharedSystemSharedLibraryPackageName); + mServicesExtensionPackageName, mSharedSystemSharedLibraryPackageName); } @Override diff --git a/services/core/java/com/android/server/pm/PackageManagerShellCommand.java b/services/core/java/com/android/server/pm/PackageManagerShellCommand.java index 5724ee0d94e9..ca00c84da724 100644 --- a/services/core/java/com/android/server/pm/PackageManagerShellCommand.java +++ b/services/core/java/com/android/server/pm/PackageManagerShellCommand.java @@ -4645,7 +4645,7 @@ class PackageManagerShellCommand extends ShellCommand { try { mInterface.getPackageInstaller().requestArchive(packageName, /* callerPackageName= */ "", receiver.getIntentSender(), - new UserHandle(translatedUserId), 0); + new UserHandle(translatedUserId)); } catch (Exception e) { pw.println("Failure [" + e.getMessage() + "]"); return 1; diff --git a/services/core/java/com/android/server/pm/ProtectedPackages.java b/services/core/java/com/android/server/pm/ProtectedPackages.java index 98533725371f..524252c1469f 100644 --- a/services/core/java/com/android/server/pm/ProtectedPackages.java +++ b/services/core/java/com/android/server/pm/ProtectedPackages.java @@ -57,11 +57,8 @@ public class ProtectedPackages { @GuardedBy("this") private final SparseArray<Set<String>> mOwnerProtectedPackages = new SparseArray<>(); - private final Context mContext; - public ProtectedPackages(Context context) { - mContext = context; - mDeviceProvisioningPackage = mContext.getResources().getString( + mDeviceProvisioningPackage = context.getResources().getString( R.string.config_deviceProvisioningPackage); } diff --git a/services/core/java/com/android/server/pm/RemovePackageHelper.java b/services/core/java/com/android/server/pm/RemovePackageHelper.java index 01b25df1cee2..70352be01096 100644 --- a/services/core/java/com/android/server/pm/RemovePackageHelper.java +++ b/services/core/java/com/android/server/pm/RemovePackageHelper.java @@ -67,7 +67,6 @@ final class RemovePackageHelper { private final PackageManagerService mPm; private final IncrementalManager mIncrementalManager; private final Installer mInstaller; - private final UserManagerInternal mUserManagerInternal; private final PermissionManagerServiceInternal mPermissionManager; private final SharedLibrariesImpl mSharedLibraries; private final AppDataHelper mAppDataHelper; @@ -79,7 +78,6 @@ final class RemovePackageHelper { mPm = pm; mIncrementalManager = mPm.mInjector.getIncrementalManager(); mInstaller = mPm.mInjector.getInstaller(); - mUserManagerInternal = mPm.mInjector.getUserManagerInternal(); mPermissionManager = mPm.mInjector.getPermissionManagerServiceInternal(); mSharedLibraries = mPm.mInjector.getSharedLibrariesImpl(); mAppDataHelper = appDataHelper; @@ -486,8 +484,6 @@ final class RemovePackageHelper { synchronized (mPm.mInstallLock) { cleanUpResourcesLI(codeFile, instructionSets); } - // TODO: open logging to help debug, will delete or add debug flag - Slog.d(TAG, "cleanUpResources for " + codeFile); if (packageName == null) { return; } diff --git a/services/core/java/com/android/server/pm/ShortcutNonPersistentUser.java b/services/core/java/com/android/server/pm/ShortcutNonPersistentUser.java index 7f6f684e0b68..aa52522cfe46 100644 --- a/services/core/java/com/android/server/pm/ShortcutNonPersistentUser.java +++ b/services/core/java/com/android/server/pm/ShortcutNonPersistentUser.java @@ -31,7 +31,6 @@ import java.io.PrintWriter; * The access to it must be guarded with the shortcut manager lock. */ public class ShortcutNonPersistentUser { - private final ShortcutService mService; private final int mUserId; @@ -49,8 +48,7 @@ public class ShortcutNonPersistentUser { */ private final ArraySet<String> mHostPackageSet = new ArraySet<>(); - public ShortcutNonPersistentUser(ShortcutService service, int userId) { - mService = service; + public ShortcutNonPersistentUser(int userId) { mUserId = userId; } diff --git a/services/core/java/com/android/server/pm/ShortcutPackage.java b/services/core/java/com/android/server/pm/ShortcutPackage.java index e993d9e5b724..d644235b8714 100644 --- a/services/core/java/com/android/server/pm/ShortcutPackage.java +++ b/services/core/java/com/android/server/pm/ShortcutPackage.java @@ -33,6 +33,7 @@ import android.app.appsearch.SearchResult; import android.app.appsearch.SearchResults; import android.app.appsearch.SearchSpec; import android.app.appsearch.SetSchemaRequest; +import android.app.usage.UsageStatsManagerInternal; import android.content.ComponentName; import android.content.Intent; import android.content.IntentFilter; @@ -47,6 +48,7 @@ import android.graphics.drawable.Icon; import android.os.Binder; import android.os.PersistableBundle; import android.os.StrictMode; +import android.os.SystemClock; import android.text.format.Formatter; import android.util.ArrayMap; import android.util.ArraySet; @@ -192,6 +194,9 @@ class ShortcutPackage extends ShortcutPackageItem { private long mLastKnownForegroundElapsedTime; @GuardedBy("mLock") + private long mLastReportedTime; + + @GuardedBy("mLock") private boolean mIsAppSearchSchemaUpToDate; private ShortcutPackage(ShortcutUser shortcutUser, @@ -1673,6 +1678,26 @@ class ShortcutPackage extends ShortcutPackageItem { return condition[0]; } + void reportShortcutUsed(@NonNull final UsageStatsManagerInternal usageStatsManagerInternal, + @NonNull final String shortcutId) { + synchronized (mLock) { + final long currentTS = SystemClock.elapsedRealtime(); + final ShortcutService s = mShortcutUser.mService; + if (currentTS - mLastReportedTime > s.mSaveDelayMillis) { + mLastReportedTime = currentTS; + } else { + return; + } + final long token = s.injectClearCallingIdentity(); + try { + usageStatsManagerInternal.reportShortcutUsage(getPackageName(), shortcutId, + getPackageUserId()); + } finally { + s.injectRestoreCallingIdentity(token); + } + } + } + public void dump(@NonNull PrintWriter pw, @NonNull String prefix, DumpFilter filter) { pw.println(); diff --git a/services/core/java/com/android/server/pm/ShortcutService.java b/services/core/java/com/android/server/pm/ShortcutService.java index 446c6293aa35..c23d2abf0853 100644 --- a/services/core/java/com/android/server/pm/ShortcutService.java +++ b/services/core/java/com/android/server/pm/ShortcutService.java @@ -371,7 +371,7 @@ public class ShortcutService extends IShortcutService.Stub { private CompressFormat mIconPersistFormat; private int mIconPersistQuality; - private int mSaveDelayMillis; + int mSaveDelayMillis; private final IPackageManager mIPackageManager; private final PackageManagerInternal mPackageManagerInternal; @@ -1378,7 +1378,7 @@ public class ShortcutService extends IShortcutService.Stub { ShortcutNonPersistentUser getNonPersistentUserLocked(@UserIdInt int userId) { ShortcutNonPersistentUser ret = mShortcutNonPersistentUsers.get(userId); if (ret == null) { - ret = new ShortcutNonPersistentUser(this, userId); + ret = new ShortcutNonPersistentUser(userId); mShortcutNonPersistentUsers.put(userId, ret); } return ret; @@ -2291,7 +2291,7 @@ public class ShortcutService extends IShortcutService.Stub { packageShortcutsChanged(ps, changedShortcuts, removedShortcuts); - reportShortcutUsedInternal(packageName, shortcut.getId(), userId); + ps.reportShortcutUsed(mUsageStatsManagerInternal, shortcut.getId()); verifyStates(); } @@ -2695,25 +2695,17 @@ public class ShortcutService extends IShortcutService.Stub { Slog.d(TAG, String.format("reportShortcutUsed: Shortcut %s package %s used on user %d", shortcutId, packageName, userId)); } + final ShortcutPackage ps; synchronized (mLock) { throwIfUserLockedL(userId); - final ShortcutPackage ps = getPackageShortcutsForPublisherLocked(packageName, userId); + ps = getPackageShortcutsForPublisherLocked(packageName, userId); if (ps.findShortcutById(shortcutId) == null) { Log.w(TAG, String.format("reportShortcutUsed: package %s doesn't have shortcut %s", packageName, shortcutId)); return; } } - reportShortcutUsedInternal(packageName, shortcutId, userId); - } - - private void reportShortcutUsedInternal(String packageName, String shortcutId, int userId) { - final long token = injectClearCallingIdentity(); - try { - mUsageStatsManagerInternal.reportShortcutUsage(packageName, shortcutId, userId); - } finally { - injectRestoreCallingIdentity(token); - } + ps.reportShortcutUsed(mUsageStatsManagerInternal, shortcutId); } @Override @@ -5202,13 +5194,11 @@ public class ShortcutService extends IShortcutService.Stub { } // Injection point. - @VisibleForTesting long injectClearCallingIdentity() { return Binder.clearCallingIdentity(); } // Injection point. - @VisibleForTesting void injectRestoreCallingIdentity(long token) { Binder.restoreCallingIdentity(token); } diff --git a/services/core/java/com/android/server/pm/UserDataPreparer.java b/services/core/java/com/android/server/pm/UserDataPreparer.java index 4c42c2dd0850..1d414011cff3 100644 --- a/services/core/java/com/android/server/pm/UserDataPreparer.java +++ b/services/core/java/com/android/server/pm/UserDataPreparer.java @@ -141,7 +141,7 @@ class UserDataPreparer { // If internal storage of the system user fails to prepare on first boot, then // things are *really* broken, so we might as well reboot to recovery right away. try { - Log.wtf(TAG, "prepareUserData failed for user " + userId, e); + Log.e(TAG, "prepareUserData failed for user " + userId, e); if (isNewUser && userId == UserHandle.USER_SYSTEM && volumeUuid == null) { RecoverySystem.rebootPromptAndWipeUserData(mContext, "failed to prepare internal storage for system user"); diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java index c1b74898e5ae..54055904d090 100644 --- a/services/core/java/com/android/server/pm/UserManagerService.java +++ b/services/core/java/com/android/server/pm/UserManagerService.java @@ -16,6 +16,8 @@ package com.android.server.pm; +import static android.content.Intent.ACTION_SCREEN_OFF; +import static android.content.Intent.ACTION_SCREEN_ON; import static android.content.Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS; import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK; import static android.os.UserManager.DEV_CREATE_OVERRIDE_PROPERTY; @@ -41,11 +43,13 @@ import android.annotation.ColorRes; import android.annotation.DrawableRes; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.RequiresPermission; import android.annotation.StringRes; import android.annotation.UserIdInt; import android.app.ActivityManager; import android.app.ActivityManagerInternal; import android.app.ActivityManagerNative; +import android.app.ActivityOptions; import android.app.BroadcastOptions; import android.app.IActivityManager; import android.app.IStopUserCallback; @@ -73,8 +77,10 @@ import android.content.pm.UserProperties; import android.content.pm.parsing.FrameworkParsingPackageUtils; import android.content.res.Configuration; import android.content.res.Resources; +import android.database.ContentObserver; import android.graphics.Bitmap; import android.multiuser.Flags; +import android.net.Uri; import android.os.Binder; import android.os.Build; import android.os.Bundle; @@ -82,6 +88,7 @@ import android.os.Debug; import android.os.Environment; import android.os.FileUtils; import android.os.Handler; +import android.os.HandlerExecutor; import android.os.IBinder; import android.os.IProgressListener; import android.os.IUserManager; @@ -90,6 +97,7 @@ import android.os.Message; import android.os.ParcelFileDescriptor; import android.os.Parcelable; import android.os.PersistableBundle; +import android.os.PowerManager; import android.os.Process; import android.os.RemoteException; import android.os.ResultReceiver; @@ -173,6 +181,8 @@ import java.util.List; import java.util.Objects; import java.util.Set; import java.util.concurrent.CountDownLatch; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; @@ -295,6 +305,12 @@ public class UserManagerService extends IUserManager.Stub { private static final long BOOT_USER_SET_TIMEOUT_MS = 300_000; + /** + * The time duration (in milliseconds) post device inactivity after which the private space + * should be auto-locked if the corresponding settings option is selected by the user. + */ + private static final long PRIVATE_SPACE_AUTO_LOCK_INACTIVITY_TIMEOUT_MS = 5 * 60 * 1000; + // Tron counters private static final String TRON_GUEST_CREATED = "users_guest_created"; private static final String TRON_USER_CREATED = "users_user_created"; @@ -320,6 +336,8 @@ public class UserManagerService extends IUserManager.Stub { private final Handler mHandler; + private final ThreadPoolExecutor mInternalExecutor; + private final File mUsersDir; private final File mUserListFile; @@ -521,6 +539,36 @@ public class UserManagerService extends IUserManager.Stub { private final LockPatternUtils mLockPatternUtils; + private KeyguardManager.KeyguardLockedStateListener mKeyguardLockedStateListener; + + /** Token to identify and remove already scheduled private space auto-lock messages */ + private static final Object PRIVATE_SPACE_AUTO_LOCK_MESSAGE_TOKEN = new Object(); + + /** Content observer to get callbacks for privte space autolock settings changes */ + private final SettingsObserver mPrivateSpaceAutoLockSettingsObserver; + + private final class SettingsObserver extends ContentObserver { + SettingsObserver(Handler handler) { + super(handler); + } + + @Override + public void onChange(boolean selfChange, Uri uri) { + if (isAutoLockForPrivateSpaceEnabled()) { + final String path = uri.getLastPathSegment(); + if (TextUtils.equals(path, Settings.Secure.PRIVATE_SPACE_AUTO_LOCK)) { + int autoLockPreference = + Settings.Secure.getIntForUser(mContext.getContentResolver(), + Settings.Secure.PRIVATE_SPACE_AUTO_LOCK, + Settings.Secure.PRIVATE_SPACE_AUTO_LOCK_NEVER, + getMainUserIdUnchecked()); + Slog.i(LOG_TAG, "Auto-lock settings changed to " + autoLockPreference); + setOrUpdateAutoLockPreferenceForPrivateProfile(autoLockPreference); + } + } + } + } + private final String ACTION_DISABLE_QUIET_MODE_AFTER_UNLOCK = "com.android.server.pm.DISABLE_QUIET_MODE_AFTER_UNLOCK"; @@ -533,12 +581,168 @@ public class UserManagerService extends IUserManager.Stub { final IntentSender target = intent.getParcelableExtra(Intent.EXTRA_INTENT, android.content.IntentSender.class); final int userId = intent.getIntExtra(Intent.EXTRA_USER_ID, UserHandle.USER_NULL); final String callingPackage = intent.getStringExtra(Intent.EXTRA_PACKAGE_NAME); - // Call setQuietModeEnabled on bg thread to avoid ANR - BackgroundThread.getHandler().post(() -> - setQuietModeEnabled(userId, false, target, callingPackage)); + setQuietModeEnabledAsync(userId, false, target, callingPackage); } }; + /** Checks if the device inactivity broadcast receiver is already registered*/ + private boolean mIsDeviceInactivityBroadcastReceiverRegistered = false; + + private final BroadcastReceiver mDeviceInactivityBroadcastReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (isAutoLockForPrivateSpaceEnabled()) { + if (ACTION_SCREEN_OFF.equals(intent.getAction())) { + maybeScheduleMessageToAutoLockPrivateSpace(); + } else if (ACTION_SCREEN_ON.equals(intent.getAction())) { + // Remove any queued messages since the device is interactive again + mHandler.removeCallbacksAndMessages(PRIVATE_SPACE_AUTO_LOCK_MESSAGE_TOKEN); + } + } + } + }; + + @VisibleForTesting + void maybeScheduleMessageToAutoLockPrivateSpace() { + // No action needed if auto-lock on inactivity not selected + int privateSpaceAutoLockPreference = + Settings.Secure.getIntForUser(mContext.getContentResolver(), + Settings.Secure.PRIVATE_SPACE_AUTO_LOCK, + Settings.Secure.PRIVATE_SPACE_AUTO_LOCK_NEVER, + getMainUserIdUnchecked()); + if (privateSpaceAutoLockPreference + != Settings.Secure.PRIVATE_SPACE_AUTO_LOCK_AFTER_INACTIVITY) { + return; + } + int privateProfileUserId = getPrivateProfileUserId(); + if (privateProfileUserId != UserHandle.USER_NULL) { + scheduleMessageToAutoLockPrivateSpace(privateProfileUserId, + PRIVATE_SPACE_AUTO_LOCK_MESSAGE_TOKEN, + PRIVATE_SPACE_AUTO_LOCK_INACTIVITY_TIMEOUT_MS); + } + } + + @VisibleForTesting + void scheduleMessageToAutoLockPrivateSpace(int userId, Object token, + long delayInMillis) { + mHandler.postDelayed(() -> { + final PowerManager powerManager = mContext.getSystemService(PowerManager.class); + if (powerManager != null && !powerManager.isInteractive()) { + Slog.i(LOG_TAG, "Auto-locking private space with user-id " + userId); + setQuietModeEnabledAsync(userId, true, + /* target */ null, mContext.getPackageName()); + } else { + Slog.i(LOG_TAG, "Device is interactive, skipping auto-lock"); + } + }, token, delayInMillis); + } + + @RequiresPermission(Manifest.permission.SUBSCRIBE_TO_KEYGUARD_LOCKED_STATE) + private void initializeAndRegisterKeyguardLockedStateListener() { + mKeyguardLockedStateListener = this::tryAutoLockingPrivateSpaceOnKeyguardChanged; + // Register with keyguard to send locked state events to the listener initialized above + try { + final KeyguardManager keyguardManager = + mContext.getSystemService(KeyguardManager.class); + Slog.i(LOG_TAG, "Adding keyguard locked state listener"); + keyguardManager.addKeyguardLockedStateListener(new HandlerExecutor(mHandler), + mKeyguardLockedStateListener); + } catch (Exception e) { + Slog.e(LOG_TAG, "Error adding keyguard locked listener ", e); + } + } + + @VisibleForTesting + @RequiresPermission(Manifest.permission.SUBSCRIBE_TO_KEYGUARD_LOCKED_STATE) + void setOrUpdateAutoLockPreferenceForPrivateProfile( + @Settings.Secure.PrivateSpaceAutoLockOption int autoLockPreference) { + int privateProfileUserId = getPrivateProfileUserId(); + if (privateProfileUserId == UserHandle.USER_NULL) { + Slog.e(LOG_TAG, "Auto-lock preference updated but private space user not found"); + return; + } + + if (autoLockPreference == Settings.Secure.PRIVATE_SPACE_AUTO_LOCK_AFTER_INACTIVITY) { + // Register inactivity broadcast + if (!mIsDeviceInactivityBroadcastReceiverRegistered) { + Slog.i(LOG_TAG, "Registering device inactivity broadcast receivers"); + mContext.registerReceiver(mDeviceInactivityBroadcastReceiver, + new IntentFilter(ACTION_SCREEN_OFF), + null, mHandler); + + mContext.registerReceiver(mDeviceInactivityBroadcastReceiver, + new IntentFilter(ACTION_SCREEN_ON), + null, mHandler); + + mIsDeviceInactivityBroadcastReceiverRegistered = true; + } + } else { + // Unregister device inactivity broadcasts + if (mIsDeviceInactivityBroadcastReceiverRegistered) { + Slog.i(LOG_TAG, "Removing device inactivity broadcast receivers"); + mHandler.removeCallbacksAndMessages(PRIVATE_SPACE_AUTO_LOCK_MESSAGE_TOKEN); + mContext.unregisterReceiver(mDeviceInactivityBroadcastReceiver); + mIsDeviceInactivityBroadcastReceiverRegistered = false; + } + } + + if (autoLockPreference == Settings.Secure.PRIVATE_SPACE_AUTO_LOCK_ON_DEVICE_LOCK) { + // Initialize and add keyguard state listener + initializeAndRegisterKeyguardLockedStateListener(); + } else { + // Remove keyguard state listener + try { + final KeyguardManager keyguardManager = + mContext.getSystemService(KeyguardManager.class); + Slog.i(LOG_TAG, "Removing keyguard locked state listener"); + keyguardManager.removeKeyguardLockedStateListener(mKeyguardLockedStateListener); + } catch (Exception e) { + Slog.e(LOG_TAG, "Error adding keyguard locked state listener ", e); + } + } + } + + @VisibleForTesting + void tryAutoLockingPrivateSpaceOnKeyguardChanged(boolean isKeyguardLocked) { + if (isAutoLockForPrivateSpaceEnabled()) { + int autoLockPreference = Settings.Secure.getIntForUser(mContext.getContentResolver(), + Settings.Secure.PRIVATE_SPACE_AUTO_LOCK, + Settings.Secure.PRIVATE_SPACE_AUTO_LOCK_NEVER, + getMainUserIdUnchecked()); + boolean isAutoLockOnDeviceLockSelected = + autoLockPreference == Settings.Secure.PRIVATE_SPACE_AUTO_LOCK_ON_DEVICE_LOCK; + if (isKeyguardLocked && isAutoLockOnDeviceLockSelected) { + int privateProfileUserId = getPrivateProfileUserId(); + if (privateProfileUserId != UserHandle.USER_NULL) { + Slog.i(LOG_TAG, "Auto-locking private space with user-id " + + privateProfileUserId); + setQuietModeEnabledAsync(privateProfileUserId, + /* enableQuietMode */true, /* target */ null, + mContext.getPackageName()); + } + } + } + } + + @VisibleForTesting + void setQuietModeEnabledAsync(@UserIdInt int userId, boolean enableQuietMode, + IntentSender target, @Nullable String callingPackage) { + if (android.multiuser.Flags.moveQuietModeOperationsToSeparateThread()) { + // Call setQuietModeEnabled on a separate thread. Calling this operation on the main + // thread can cause ANRs, posting on a BackgroundThread can result in delays + Slog.d(LOG_TAG, "Calling setQuietModeEnabled for user " + userId + + " on a separate thread"); + mInternalExecutor.execute(() -> setQuietModeEnabled(userId, enableQuietMode, target, + callingPackage)); + } else { + // Call setQuietModeEnabled on bg thread to avoid ANR + BackgroundThread.getHandler().post( + () -> setQuietModeEnabled(userId, enableQuietMode, target, + callingPackage) + ); + } + } + /** * Cache the owner name string, since it could be read repeatedly on a critical code path * but hit by slow IO. This could be eliminated once we have the cached UserInfo in place. @@ -587,7 +791,10 @@ public class UserManagerService extends IUserManager.Stub { public void onFinished(int id, Bundle extras) { mHandler.post(() -> { try { - mContext.startIntentSender(mTarget, null, 0, 0, 0); + ActivityOptions activityOptions = + ActivityOptions.makeBasic().setPendingIntentBackgroundActivityStartMode( + ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED); + mContext.startIntentSender(mTarget, null, 0, 0, 0, activityOptions.toBundle()); } catch (IntentSender.SendIntentException e) { Slog.e(LOG_TAG, "Failed to start the target in the callback", e); } @@ -762,6 +969,8 @@ public class UserManagerService extends IUserManager.Stub { mPackagesLock = packagesLock; mUsers = users != null ? users : new SparseArray<>(); mHandler = new MainHandler(); + mInternalExecutor = new ThreadPoolExecutor(/* corePoolSize */ 0, /* maximumPoolSize */ 1, + /* keepAliveTime */ 1, TimeUnit.SECONDS, new LinkedBlockingQueue<>()); mUserVisibilityMediator = new UserVisibilityMediator(mHandler); mUserDataPreparer = userDataPreparer; mUserTypes = UserTypeFactory.getUserTypes(); @@ -786,9 +995,15 @@ public class UserManagerService extends IUserManager.Stub { mLockPatternUtils = new LockPatternUtils(mContext); mUserStates.put(UserHandle.USER_SYSTEM, UserState.STATE_BOOTING); mUser0Allocations = DBG_ALLOCATION ? new AtomicInteger() : null; + mPrivateSpaceAutoLockSettingsObserver = new SettingsObserver(mHandler); emulateSystemUserModeIfNeeded(); } + private static boolean isAutoLockForPrivateSpaceEnabled() { + return android.os.Flags.allowPrivateProfile() + && Flags.supportAutolockForPrivateSpace(); + } + void systemReady() { mAppOpsService = IAppOpsService.Stub.asInterface( ServiceManager.getService(Context.APP_OPS_SERVICE)); @@ -805,6 +1020,20 @@ public class UserManagerService extends IUserManager.Stub { new IntentFilter(Intent.ACTION_CONFIGURATION_CHANGED), null, mHandler); + if (isAutoLockForPrivateSpaceEnabled()) { + + int mainUserId = getMainUserIdUnchecked(); + + mContext.getContentResolver().registerContentObserverAsUser(Settings.Secure.getUriFor( + Settings.Secure.PRIVATE_SPACE_AUTO_LOCK), false, + mPrivateSpaceAutoLockSettingsObserver, UserHandle.of(mainUserId)); + + setOrUpdateAutoLockPreferenceForPrivateProfile( + Settings.Secure.getIntForUser(mContext.getContentResolver(), + Settings.Secure.PRIVATE_SPACE_AUTO_LOCK, + Settings.Secure.PRIVATE_SPACE_AUTO_LOCK_NEVER, mainUserId)); + } + markEphemeralUsersForRemoval(); } @@ -969,6 +1198,18 @@ public class UserManagerService extends IUserManager.Stub { return UserHandle.USER_NULL; } + private @UserIdInt int getPrivateProfileUserId() { + synchronized (mUsersLock) { + for (int userId : getUserIds()) { + UserInfo userInfo = getUserInfoLU(userId); + if (userInfo != null && userInfo.isPrivateProfile()) { + return userInfo.id; + } + } + } + return UserHandle.USER_NULL; + } + @Override public void setBootUser(@UserIdInt int userId) { checkCreateUsersPermission("Set boot user"); diff --git a/services/core/java/com/android/server/pm/VerifyingSession.java b/services/core/java/com/android/server/pm/VerifyingSession.java index c6435aeaba4d..f0ff85df13d1 100644 --- a/services/core/java/com/android/server/pm/VerifyingSession.java +++ b/services/core/java/com/android/server/pm/VerifyingSession.java @@ -356,6 +356,11 @@ final class VerifyingSession { if (verifierUser == UserHandle.ALL) { verifierUser = UserHandle.of(mPm.mUserManager.getCurrentUserId()); } + // TODO(b/300965895): Remove when inconsistencies loading classpaths from apex for + // user > 1 are fixed. + if (pkgLite.isSdkLibrary) { + verifierUser = UserHandle.SYSTEM; + } final int verifierUserId = verifierUser.getIdentifier(); List<String> requiredVerifierPackages = new ArrayList<>( diff --git a/services/core/java/com/android/server/pm/permission/PermissionManagerService.java b/services/core/java/com/android/server/pm/permission/PermissionManagerService.java index 5d710d272fc9..40f226435194 100644 --- a/services/core/java/com/android/server/pm/permission/PermissionManagerService.java +++ b/services/core/java/com/android/server/pm/permission/PermissionManagerService.java @@ -29,6 +29,7 @@ import static android.app.AppOpsManager.MODE_IGNORED; import static android.app.AppOpsManager.OP_BLUETOOTH_CONNECT; import static android.content.pm.ApplicationInfo.AUTO_REVOKE_DISALLOWED; import static android.content.pm.ApplicationInfo.AUTO_REVOKE_DISCOURAGED; +import static android.permission.flags.Flags.serverSideAttributionRegistration; import static com.android.server.pm.PackageManagerService.PLATFORM_PACKAGE_NAME; @@ -76,7 +77,6 @@ import com.android.internal.util.Preconditions; import com.android.internal.util.function.QuadFunction; import com.android.internal.util.function.TriFunction; import com.android.server.LocalServices; -import com.android.server.pm.UserManagerInternal; import com.android.server.pm.UserManagerService; import com.android.server.pm.permission.PermissionManagerServiceInternal.HotwordDetectionServiceProvider; import com.android.server.pm.pkg.AndroidPackage; @@ -113,9 +113,6 @@ public class PermissionManagerService extends IPermissionManager.Stub { /** Internal connection to the package manager */ private final PackageManagerInternal mPackageManagerInt; - /** Internal connection to the user manager */ - private final UserManagerInternal mUserManagerInt; - /** Map of OneTimePermissionUserManagers keyed by userId */ @GuardedBy("mLock") @NonNull @@ -147,7 +144,6 @@ public class PermissionManagerService extends IPermissionManager.Stub { mContext = context; mPackageManagerInt = LocalServices.getService(PackageManagerInternal.class); - mUserManagerInt = LocalServices.getService(UserManagerInternal.class); mAppOpsManager = context.getSystemService(AppOpsManager.class); mAttributionSourceRegistry = new AttributionSourceRegistry(context); @@ -439,10 +435,27 @@ public class PermissionManagerService extends IPermissionManager.Stub { } } + /** + * Reference propagation over binder is affected by the ownership of the object. So if + * the token is owned by client, references to the token on client side won't be + * propagated to the server and the token may still be garbage collected on server side. + * But if the token is owned by server, references to the token on client side will now + * be propagated to the server since it's a foreign object to the client, and that will + * keep the token referenced on the server side as long as the client is alive and + * holding it. + */ @Override - public void registerAttributionSource(@NonNull AttributionSourceState source) { - mAttributionSourceRegistry - .registerAttributionSource(new AttributionSource(source)); + public IBinder registerAttributionSource(@NonNull AttributionSourceState source) { + if (serverSideAttributionRegistration()) { + Binder token = new Binder(); + mAttributionSourceRegistry + .registerAttributionSource(new AttributionSource(source).withToken(token)); + return token; + } else { + mAttributionSourceRegistry + .registerAttributionSource(new AttributionSource(source)); + return source.token; + } } @Override @@ -1080,12 +1093,10 @@ public class PermissionManagerService extends IPermissionManager.Stub { private static final AtomicInteger sAttributionChainIds = new AtomicInteger(0); private final @NonNull Context mContext; - private final @NonNull AppOpsManager mAppOpsManager; private final @NonNull PermissionManagerServiceInternal mPermissionManagerServiceInternal; PermissionCheckerService(@NonNull Context context) { mContext = context; - mAppOpsManager = mContext.getSystemService(AppOpsManager.class); mPermissionManagerServiceInternal = LocalServices.getService(PermissionManagerServiceInternal.class); } @@ -1218,7 +1229,6 @@ public class PermissionManagerService extends IPermissionManager.Stub { @Nullable String message, boolean forDataDelivery, boolean startDataDelivery, boolean fromDatasource, int attributedOp) { PermissionInfo permissionInfo = sPlatformPermissions.get(permission); - if (permissionInfo == null) { try { permissionInfo = context.getPackageManager().getPermissionInfo(permission, 0); @@ -1346,8 +1356,8 @@ public class PermissionManagerService extends IPermissionManager.Stub { // If the call is from a datasource we need to vet only the chain before it. This // way we can avoid the datasource creating an attribution context for every call. - if (!(fromDatasource && current.equals(attributionSource)) - && next != null && !current.isTrusted(context)) { + boolean isDatasource = fromDatasource && current.equals(attributionSource); + if (!isDatasource && next != null && !current.isTrusted(context)) { return PermissionChecker.PERMISSION_HARD_DENIED; } diff --git a/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java b/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java index 3afba39ad4af..6a5736269e51 100644 --- a/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java +++ b/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java @@ -279,7 +279,6 @@ public class PermissionManagerServiceImpl implements PermissionManagerServiceInt @NonNull private final int[] mGlobalGids; - private final HandlerThread mHandlerThread; private final Handler mHandler; private final Context mContext; private final MetricsLogger mMetricsLogger = new MetricsLogger(); @@ -432,10 +431,10 @@ public class PermissionManagerServiceImpl implements PermissionManagerServiceInt } } - mHandlerThread = new ServiceThread(TAG, + HandlerThread handlerThread = new ServiceThread(TAG, Process.THREAD_PRIORITY_BACKGROUND, true /*allowIo*/); - mHandlerThread.start(); - mHandler = new Handler(mHandlerThread.getLooper()); + handlerThread.start(); + mHandler = new Handler(handlerThread.getLooper()); Watchdog.getInstance().addThread(mHandler); SystemConfig systemConfig = SystemConfig.getInstance(); diff --git a/services/core/java/com/android/server/power/PowerManagerService.java b/services/core/java/com/android/server/power/PowerManagerService.java index e2269535d931..a172de0bb0ff 100644 --- a/services/core/java/com/android/server/power/PowerManagerService.java +++ b/services/core/java/com/android/server/power/PowerManagerService.java @@ -4413,8 +4413,8 @@ public final class PowerManagerService extends SystemService private boolean setPowerModeInternal(int mode, boolean enabled) { // Maybe filter the event. - if (mBatterySaverStateMachine == null || (mode == Mode.LAUNCH && enabled - && mBatterySaverStateMachine.getBatterySaverController().isLaunchBoostDisabled())) { + if (mode == Mode.LAUNCH && enabled && mBatterySaverStateMachine != null + && mBatterySaverStateMachine.getBatterySaverController().isLaunchBoostDisabled()) { return false; } return mNativeWrapper.nativeSetPowerMode(mode, enabled); diff --git a/services/core/java/com/android/server/recoverysystem/RecoverySystemService.java b/services/core/java/com/android/server/recoverysystem/RecoverySystemService.java index 375ef6150830..a4dbce6b2631 100644 --- a/services/core/java/com/android/server/recoverysystem/RecoverySystemService.java +++ b/services/core/java/com/android/server/recoverysystem/RecoverySystemService.java @@ -65,6 +65,7 @@ import com.android.internal.widget.LockSettingsInternal; import com.android.internal.widget.RebootEscrowListener; import com.android.server.LocalServices; import com.android.server.SystemService; +import com.android.server.Watchdog; import com.android.server.pm.ApexManager; import com.android.server.recoverysystem.hal.BootControlHIDL; @@ -112,6 +113,9 @@ public class RecoverySystemService extends IRecoverySystem.Stub implements Reboo private static final int SOCKET_CONNECTION_MAX_RETRY = 30; + /** How long to pause the watchdog for when rebooting the device. */ + private static final int REBOOT_WATCHDOG_PAUSE_DURATION_MS = 20_000; + static final String REQUEST_LSKF_TIMESTAMP_PREF_SUFFIX = "_request_lskf_timestamp"; static final String REQUEST_LSKF_COUNT_PREF_SUFFIX = "_request_lskf_count"; @@ -899,7 +903,8 @@ public class RecoverySystemService extends IRecoverySystem.Stub implements Reboo // Clear the metrics prefs after a successful RoR reboot. mInjector.getMetricsPrefs().deletePrefsFile(); - + Watchdog.getInstance().pauseWatchingCurrentThreadFor( + REBOOT_WATCHDOG_PAUSE_DURATION_MS, "reboot can be slow"); PowerManager pm = mInjector.getPowerManager(); pm.reboot(reason); return RESUME_ON_REBOOT_REBOOT_ERROR_UNSPECIFIED; diff --git a/services/core/java/com/android/server/security/FileIntegrityService.java b/services/core/java/com/android/server/security/FileIntegrityService.java index a49df50c1b92..bb4876bd5f56 100644 --- a/services/core/java/com/android/server/security/FileIntegrityService.java +++ b/services/core/java/com/android/server/security/FileIntegrityService.java @@ -157,7 +157,7 @@ public class FileIntegrityService extends SystemService { Objects.requireNonNull(authFd); try { var authToken = getStorageManagerInternal().createFsveritySetupAuthToken(authFd, - Binder.getCallingUid(), Binder.getCallingUserHandle().getIdentifier()); + Binder.getCallingUid()); // fs-verity setup requires no writable fd to the file. Release the dup now that // it's passed. authFd.close(); 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 0467d0cd351d..7ab075e2f3a7 100644 --- a/services/core/java/com/android/server/tv/interactive/TvInteractiveAppManagerService.java +++ b/services/core/java/com/android/server/tv/interactive/TvInteractiveAppManagerService.java @@ -38,7 +38,16 @@ import android.media.tv.BroadcastInfoRequest; import android.media.tv.BroadcastInfoResponse; import android.media.tv.TvRecordingInfo; import android.media.tv.TvTrackInfo; +import android.media.tv.ad.ITvAdClient; import android.media.tv.ad.ITvAdManager; +import android.media.tv.ad.ITvAdManagerCallback; +import android.media.tv.ad.ITvAdService; +import android.media.tv.ad.ITvAdServiceCallback; +import android.media.tv.ad.ITvAdSession; +import android.media.tv.ad.ITvAdSessionCallback; +import android.media.tv.ad.TvAdService; +import android.media.tv.ad.TvAdServiceInfo; +import android.media.tv.flags.Flags; import android.media.tv.interactive.AppLinkInfo; import android.media.tv.interactive.ITvInteractiveAppClient; import android.media.tv.interactive.ITvInteractiveAppManager; @@ -110,6 +119,8 @@ public class TvInteractiveAppManagerService extends SystemService { @GuardedBy("mLock") private boolean mGetServiceListCalled = false; @GuardedBy("mLock") + private boolean mGetAdServiceListCalled = false; + @GuardedBy("mLock") private boolean mGetAppLinkInfoListCalled = false; private final UserManager mUserManager; @@ -256,6 +267,141 @@ public class TvInteractiveAppManagerService extends SystemService { } @GuardedBy("mLock") + private void buildTvAdServiceListLocked(int userId, String[] updatedPackages) { + if (!Flags.enableAdServiceFw()) { + return; + } + UserState userState = getOrCreateUserStateLocked(userId); + userState.mPackageSet.clear(); + + if (DEBUG) { + Slogf.d(TAG, "buildTvAdServiceListLocked"); + } + PackageManager pm = mContext.getPackageManager(); + List<ResolveInfo> services = pm.queryIntentServicesAsUser( + new Intent(TvAdService.SERVICE_INTERFACE), + PackageManager.GET_SERVICES | PackageManager.GET_META_DATA, + userId); + List<TvAdServiceInfo> serviceList = new ArrayList<>(); + + for (ResolveInfo ri : services) { + ServiceInfo si = ri.serviceInfo; + if (!android.Manifest.permission.BIND_TV_AD_SERVICE.equals(si.permission)) { + Slog.w(TAG, "Skipping TV AD service " + si.name + + ": it does not require the permission " + + android.Manifest.permission.BIND_TV_AD_SERVICE); + continue; + } + + ComponentName component = new ComponentName(si.packageName, si.name); + try { + TvAdServiceInfo info = new TvAdServiceInfo(mContext, component); + serviceList.add(info); + } catch (Exception e) { + Slogf.e(TAG, "failed to load TV AD service " + si.name, e); + continue; + } + userState.mPackageSet.add(si.packageName); + } + + // sort the service list by service id + Collections.sort(serviceList, Comparator.comparing(TvAdServiceInfo::getId)); + Map<String, TvAdServiceState> adServiceMap = new HashMap<>(); + for (TvAdServiceInfo info : serviceList) { + String serviceId = info.getId(); + if (DEBUG) { + Slogf.d(TAG, "add " + serviceId); + } + TvAdServiceState adServiceState = userState.mAdServiceMap.get(serviceId); + if (adServiceState == null) { + adServiceState = new TvAdServiceState(); + } + adServiceState.mInfo = info; + adServiceState.mUid = getAdServiceUid(info); + adServiceState.mComponentName = info.getComponent(); + adServiceMap.put(serviceId, adServiceState); + } + + for (String serviceId : adServiceMap.keySet()) { + if (!userState.mAdServiceMap.containsKey(serviceId)) { + notifyAdServiceAddedLocked(userState, serviceId); + } else if (updatedPackages != null) { + // Notify the package updates + ComponentName component = adServiceMap.get(serviceId).mInfo.getComponent(); + for (String updatedPackage : updatedPackages) { + if (component.getPackageName().equals(updatedPackage)) { + updateAdServiceConnectionLocked(component, userId); + notifyAdServiceUpdatedLocked(userState, serviceId); + break; + } + } + } + } + + for (String serviceId : userState.mAdServiceMap.keySet()) { + if (!adServiceMap.containsKey(serviceId)) { + TvAdServiceInfo info = userState.mAdServiceMap.get(serviceId).mInfo; + AdServiceState serviceState = userState.mAdServiceStateMap.get(info.getComponent()); + if (serviceState != null) { + abortPendingCreateAdSessionRequestsLocked(serviceState, serviceId, userId); + } + notifyAdServiceRemovedLocked(userState, serviceId); + } + } + + userState.mIAppMap.clear(); + userState.mAdServiceMap = adServiceMap; + } + + @GuardedBy("mLock") + private void notifyAdServiceAddedLocked(UserState userState, String serviceId) { + if (DEBUG) { + Slog.d(TAG, "notifyAdServiceAddedLocked(serviceId=" + serviceId + ")"); + } + int n = userState.mAdCallbacks.beginBroadcast(); + for (int i = 0; i < n; ++i) { + try { + userState.mAdCallbacks.getBroadcastItem(i).onAdServiceAdded(serviceId); + } catch (RemoteException e) { + Slog.e(TAG, "failed to report added AD service to callback", e); + } + } + userState.mAdCallbacks.finishBroadcast(); + } + + @GuardedBy("mLock") + private void notifyAdServiceRemovedLocked(UserState userState, String serviceId) { + if (DEBUG) { + Slog.d(TAG, "notifyAdServiceRemovedLocked(serviceId=" + serviceId + ")"); + } + int n = userState.mAdCallbacks.beginBroadcast(); + for (int i = 0; i < n; ++i) { + try { + userState.mAdCallbacks.getBroadcastItem(i).onAdServiceRemoved(serviceId); + } catch (RemoteException e) { + Slog.e(TAG, "failed to report removed AD service to callback", e); + } + } + userState.mAdCallbacks.finishBroadcast(); + } + + @GuardedBy("mLock") + private void notifyAdServiceUpdatedLocked(UserState userState, String serviceId) { + if (DEBUG) { + Slog.d(TAG, "notifyAdServiceUpdatedLocked(serviceId=" + serviceId + ")"); + } + int n = userState.mAdCallbacks.beginBroadcast(); + for (int i = 0; i < n; ++i) { + try { + userState.mAdCallbacks.getBroadcastItem(i).onAdServiceUpdated(serviceId); + } catch (RemoteException e) { + Slog.e(TAG, "failed to report updated AD service to callback", e); + } + } + userState.mAdCallbacks.finishBroadcast(); + } + + @GuardedBy("mLock") private void notifyInteractiveAppServiceAddedLocked(UserState userState, String iAppServiceId) { if (DEBUG) { Slog.d(TAG, "notifyInteractiveAppServiceAddedLocked(iAppServiceId=" @@ -340,6 +486,16 @@ public class TvInteractiveAppManagerService extends SystemService { } } + private int getAdServiceUid(TvAdServiceInfo info) { + try { + return getContext().getPackageManager().getApplicationInfo( + info.getServiceInfo().packageName, 0).uid; + } catch (PackageManager.NameNotFoundException e) { + Slogf.w(TAG, "Unable to get UID for " + info, e); + return Process.INVALID_UID; + } + } + @Override public void onStart() { if (DEBUG) { @@ -357,6 +513,7 @@ public class TvInteractiveAppManagerService extends SystemService { synchronized (mLock) { buildTvInteractiveAppServiceListLocked(mCurrentUserId, null); buildAppLinkInfoLocked(mCurrentUserId); + buildTvAdServiceListLocked(mCurrentUserId, null); } } } @@ -372,6 +529,14 @@ public class TvInteractiveAppManagerService extends SystemService { } } } + private void buildTvAdServiceList(String[] packages) { + int userId = getChangingUserId(); + synchronized (mLock) { + if (mCurrentUserId == userId || mRunningProfiles.contains(userId)) { + buildTvAdServiceListLocked(userId, packages); + } + } + } @Override public void onPackageUpdateFinished(String packageName, int uid) { @@ -379,6 +544,7 @@ public class TvInteractiveAppManagerService extends SystemService { // This callback is invoked when the TV interactive App service is reinstalled. // In this case, isReplacing() always returns true. buildTvInteractiveAppServiceList(new String[] { packageName }); + buildTvAdServiceList(new String[] { packageName }); } @Override @@ -390,6 +556,7 @@ public class TvInteractiveAppManagerService extends SystemService { // available. if (isReplacing()) { buildTvInteractiveAppServiceList(packages); + buildTvAdServiceList(packages); } } @@ -403,6 +570,7 @@ public class TvInteractiveAppManagerService extends SystemService { } if (isReplacing()) { buildTvInteractiveAppServiceList(packages); + buildTvAdServiceList(packages); } } @@ -418,6 +586,7 @@ public class TvInteractiveAppManagerService extends SystemService { return; } buildTvInteractiveAppServiceList(null); + buildTvAdServiceList(null); } @Override @@ -476,6 +645,7 @@ public class TvInteractiveAppManagerService extends SystemService { mCurrentUserId = userId; buildTvInteractiveAppServiceListLocked(userId, null); buildAppLinkInfoLocked(userId); + buildTvAdServiceListLocked(userId, null); } } @@ -562,6 +732,7 @@ public class TvInteractiveAppManagerService extends SystemService { mRunningProfiles.add(userId); buildTvInteractiveAppServiceListLocked(userId, null); buildAppLinkInfoLocked(userId); + buildTvAdServiceListLocked(userId, null); } @GuardedBy("mLock") @@ -619,7 +790,19 @@ public class TvInteractiveAppManagerService extends SystemService { Slog.e(TAG, "error in onSessionReleased", e); } } - removeSessionStateLocked(state.mSessionToken, state.mUserId); + removeAdSessionStateLocked(state.mSessionToken, state.mUserId); + } + + @GuardedBy("mLock") + private void clearAdSessionAndNotifyClientLocked(AdSessionState state) { + if (state.mClient != null) { + try { + state.mClient.onSessionReleased(state.mSeq); + } catch (RemoteException e) { + Slog.e(TAG, "error in onSessionReleased", e); + } + } + removeAdSessionStateLocked(state.mSessionToken, state.mUserId); } private int resolveCallingUserId(int callingPid, int callingUid, int requestedUserId, @@ -655,6 +838,44 @@ public class TvInteractiveAppManagerService extends SystemService { } @GuardedBy("mLock") + private AdSessionState getAdSessionStateLocked( + IBinder sessionToken, int callingUid, int userId) { + UserState userState = getOrCreateUserStateLocked(userId); + return getAdSessionStateLocked(sessionToken, callingUid, userState); + } + + @GuardedBy("mLock") + private AdSessionState getAdSessionStateLocked(IBinder sessionToken, int callingUid, + UserState userState) { + AdSessionState sessionState = userState.mAdSessionStateMap.get(sessionToken); + if (sessionState == null) { + throw new SessionNotFoundException("Session state not found for token " + sessionToken); + } + // Only the application that requested this session or the system can access it. + if (callingUid != Process.SYSTEM_UID && callingUid != sessionState.mCallingUid) { + throw new SecurityException("Illegal access to the session with token " + sessionToken + + " from uid " + callingUid); + } + return sessionState; + } + + @GuardedBy("mLock") + private ITvAdSession getAdSessionLocked( + IBinder sessionToken, int callingUid, int userId) { + return getAdSessionLocked(getAdSessionStateLocked(sessionToken, callingUid, userId)); + } + + @GuardedBy("mLock") + private ITvAdSession getAdSessionLocked(AdSessionState sessionState) { + ITvAdSession session = sessionState.mSession; + if (session == null) { + throw new IllegalStateException("Session not yet created for token " + + sessionState.mSessionToken); + } + return session; + } + + @GuardedBy("mLock") private SessionState getSessionStateLocked(IBinder sessionToken, int callingUid, int userId) { UserState userState = getOrCreateUserStateLocked(userId); return getSessionStateLocked(sessionToken, callingUid, userState); @@ -691,10 +912,200 @@ public class TvInteractiveAppManagerService extends SystemService { return session; } private final class TvAdBinderService extends ITvAdManager.Stub { + + @Override + public List<TvAdServiceInfo> getTvAdServiceList(int userId) { + final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(), + Binder.getCallingUid(), userId, "getTvAdServiceList"); + final long identity = Binder.clearCallingIdentity(); + try { + synchronized (mLock) { + if (!mGetAdServiceListCalled) { + buildTvAdServiceListLocked(userId, null); + mGetAdServiceListCalled = true; + } + UserState userState = getOrCreateUserStateLocked(resolvedUserId); + List<TvAdServiceInfo> adServiceList = new ArrayList<>(); + for (TvAdServiceState state : userState.mAdServiceMap.values()) { + adServiceList.add(state.mInfo); + } + return adServiceList; + } + } finally { + Binder.restoreCallingIdentity(identity); + } + } + + @Override + public void createSession(final ITvAdClient client, final String serviceId, String type, + int seq, int userId) { + final int callingUid = Binder.getCallingUid(); + final int callingPid = Binder.getCallingPid(); + final int resolvedUserId = resolveCallingUserId(callingPid, callingUid, + userId, "createSession"); + final long identity = Binder.clearCallingIdentity(); + + try { + synchronized (mLock) { + if (userId != mCurrentUserId && !mRunningProfiles.contains(userId)) { + // Only current user and its running profiles can create sessions. + // Let the client get onConnectionFailed callback for this case. + sendAdSessionTokenToClientLocked(client, serviceId, null, null, seq); + return; + } + UserState userState = getOrCreateUserStateLocked(resolvedUserId); + TvAdServiceState adState = userState.mAdMap.get(serviceId); + if (adState == null) { + Slogf.w(TAG, "Failed to find state for serviceId=" + serviceId); + sendAdSessionTokenToClientLocked(client, serviceId, null, null, seq); + return; + } + AdServiceState serviceState = + userState.mAdServiceStateMap.get(adState.mComponentName); + if (serviceState == null) { + int tasUid = PackageManager.getApplicationInfoAsUserCached( + adState.mComponentName.getPackageName(), 0, resolvedUserId).uid; + serviceState = new AdServiceState( + adState.mComponentName, serviceId, resolvedUserId); + userState.mAdServiceStateMap.put(adState.mComponentName, serviceState); + } + // Send a null token immediately while reconnecting. + if (serviceState.mReconnecting) { + sendAdSessionTokenToClientLocked(client, serviceId, null, null, seq); + return; + } + + // Create a new session token and a session state. + IBinder sessionToken = new Binder(); + AdSessionState sessionState = new AdSessionState(sessionToken, serviceId, type, + adState.mComponentName, client, seq, callingUid, + callingPid, resolvedUserId); + + // Add them to the global session state map of the current user. + userState.mAdSessionStateMap.put(sessionToken, sessionState); + + // Also, add them to the session state map of the current service. + serviceState.mSessionTokens.add(sessionToken); + + if (serviceState.mService != null) { + if (!createAdSessionInternalLocked(serviceState.mService, sessionToken, + resolvedUserId)) { + removeAdSessionStateLocked(sessionToken, resolvedUserId); + } + } else { + updateAdServiceConnectionLocked(adState.mComponentName, resolvedUserId); + } + } + } finally { + Binder.restoreCallingIdentity(identity); + } + } + + @Override + public void releaseSession(IBinder sessionToken, int userId) { + if (DEBUG) { + Slogf.d(TAG, "releaseSession(sessionToken=" + sessionToken + ")"); + } + final int callingUid = Binder.getCallingUid(); + final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(), callingUid, + userId, "releaseSession"); + final long identity = Binder.clearCallingIdentity(); + try { + synchronized (mLock) { + releaseSessionLocked(sessionToken, callingUid, resolvedUserId); + } + } finally { + Binder.restoreCallingIdentity(identity); + } + } + + @Override + public void setSurface(IBinder sessionToken, Surface surface, int userId) { + final int callingUid = Binder.getCallingUid(); + final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(), callingUid, + userId, "setSurface"); + AdSessionState sessionState = null; + final long identity = Binder.clearCallingIdentity(); + try { + synchronized (mLock) { + try { + sessionState = getAdSessionStateLocked(sessionToken, callingUid, + resolvedUserId); + getAdSessionLocked(sessionState).setSurface(surface); + } catch (RemoteException | SessionNotFoundException e) { + Slogf.e(TAG, "error in setSurface", e); + } + } + } finally { + if (surface != null) { + // surface is not used in TvInteractiveAppManagerService. + surface.release(); + } + Binder.restoreCallingIdentity(identity); + } + } + + @Override + public void dispatchSurfaceChanged(IBinder sessionToken, int format, int width, + int height, int userId) { + final int callingUid = Binder.getCallingUid(); + final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(), callingUid, + userId, "dispatchSurfaceChanged"); + final long identity = Binder.clearCallingIdentity(); + try { + synchronized (mLock) { + try { + AdSessionState sessionState = getAdSessionStateLocked( + sessionToken, callingUid, resolvedUserId); + getAdSessionLocked(sessionState).dispatchSurfaceChanged(format, width, + height); + } catch (RemoteException | SessionNotFoundException e) { + Slogf.e(TAG, "error in dispatchSurfaceChanged", e); + } + } + } finally { + Binder.restoreCallingIdentity(identity); + } + } + @Override public void startAdService(IBinder sessionToken, int userId) { } + @Override + public void registerCallback(final ITvAdManagerCallback callback, int userId) { + int callingPid = Binder.getCallingPid(); + int callingUid = Binder.getCallingUid(); + final int resolvedUserId = resolveCallingUserId(callingPid, callingUid, userId, + "registerCallback"); + final long identity = Binder.clearCallingIdentity(); + try { + synchronized (mLock) { + final UserState userState = getOrCreateUserStateLocked(resolvedUserId); + if (!userState.mAdCallbacks.register(callback)) { + Slog.e(TAG, "client process has already died"); + } + } + } finally { + Binder.restoreCallingIdentity(identity); + } + } + + @Override + public void unregisterCallback(ITvAdManagerCallback callback, int userId) { + final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(), + Binder.getCallingUid(), userId, "unregisterCallback"); + final long identity = Binder.clearCallingIdentity(); + try { + synchronized (mLock) { + UserState userState = getOrCreateUserStateLocked(resolvedUserId); + userState.mAdCallbacks.unregister(callback); + } + } finally { + Binder.restoreCallingIdentity(identity); + } + } + } private final class BinderService extends ITvInteractiveAppManager.Stub { @@ -927,7 +1338,7 @@ public class TvInteractiveAppManagerService extends SystemService { final long identity = Binder.clearCallingIdentity(); try { synchronized (mLock) { - releaseSessionLocked(sessionToken, callingUid, resolvedUserId); + releaseAdSessionLocked(sessionToken, callingUid, resolvedUserId); } } finally { Binder.restoreCallingIdentity(identity); @@ -1471,6 +1882,32 @@ public class TvInteractiveAppManagerService extends SystemService { } @Override + public void sendSelectedTrackInfo(IBinder sessionToken, List<TvTrackInfo> tracks, + int userId) { + if (DEBUG) { + Slogf.d(TAG, "sendSelectedTrackInfo(tracks=%s)", tracks.toString()); + } + final int callingUid = Binder.getCallingUid(); + final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(), callingUid, + userId, "sendSelectedTrackInfo"); + SessionState sessionState = null; + final long identity = Binder.clearCallingIdentity(); + try { + synchronized (mLock) { + try { + sessionState = getSessionStateLocked(sessionToken, callingUid, + resolvedUserId); + getSessionLocked(sessionState).sendSelectedTrackInfo(tracks); + } catch (RemoteException | SessionNotFoundException e) { + Slogf.e(TAG, "error in sendSelectedTrackInfo", e); + } + } + } finally { + Binder.restoreCallingIdentity(identity); + } + } + + @Override public void sendCurrentTvInputId(IBinder sessionToken, String inputId, int userId) { if (DEBUG) { Slogf.d(TAG, "sendCurrentTvInputId(inputId=%s)", inputId); @@ -2134,6 +2571,17 @@ public class TvInteractiveAppManagerService extends SystemService { } @GuardedBy("mLock") + private void sendAdSessionTokenToClientLocked( + ITvAdClient client, String serviceId, IBinder sessionToken, + InputChannel channel, int seq) { + try { + client.onSessionCreated(serviceId, sessionToken, channel, seq); + } catch (RemoteException e) { + Slogf.e(TAG, "error in onSessionCreated", e); + } + } + + @GuardedBy("mLock") private boolean createSessionInternalLocked( ITvInteractiveAppService service, IBinder sessionToken, int userId) { UserState userState = getOrCreateUserStateLocked(userId); @@ -2163,6 +2611,58 @@ public class TvInteractiveAppManagerService extends SystemService { } @GuardedBy("mLock") + private boolean createAdSessionInternalLocked( + ITvAdService service, IBinder sessionToken, int userId) { + UserState userState = getOrCreateUserStateLocked(userId); + AdSessionState sessionState = userState.mAdSessionStateMap.get(sessionToken); + if (DEBUG) { + Slogf.d(TAG, "createAdSessionInternalLocked(iAppServiceId=" + + sessionState.mAdServiceId + ")"); + } + InputChannel[] channels = InputChannel.openInputChannelPair(sessionToken.toString()); + + // Set up a callback to send the session token. + ITvAdSessionCallback callback = new AdSessionCallback(sessionState, channels); + + boolean created = true; + // Create a session. When failed, send a null token immediately. + try { + service.createSession( + channels[1], callback, sessionState.mAdServiceId, sessionState.mType); + } catch (RemoteException e) { + Slogf.e(TAG, "error in createSession", e); + sendAdSessionTokenToClientLocked(sessionState.mClient, sessionState.mAdServiceId, null, + null, sessionState.mSeq); + created = false; + } + channels[1].dispose(); + return created; + } + + @GuardedBy("mLock") + @Nullable + private AdSessionState releaseAdSessionLocked( + IBinder sessionToken, int callingUid, int userId) { + AdSessionState sessionState = null; + try { + sessionState = getAdSessionStateLocked(sessionToken, callingUid, userId); + UserState userState = getOrCreateUserStateLocked(userId); + if (sessionState.mSession != null) { + sessionState.mSession.asBinder().unlinkToDeath(sessionState, 0); + sessionState.mSession.release(); + } + } catch (RemoteException | SessionNotFoundException e) { + Slogf.e(TAG, "error in releaseSession", e); + } finally { + if (sessionState != null) { + sessionState.mSession = null; + } + } + removeAdSessionStateLocked(sessionToken, userId); + return sessionState; + } + + @GuardedBy("mLock") @Nullable private SessionState releaseSessionLocked(IBinder sessionToken, int callingUid, int userId) { SessionState sessionState = null; @@ -2215,6 +2715,36 @@ public class TvInteractiveAppManagerService extends SystemService { } @GuardedBy("mLock") + private void removeAdSessionStateLocked(IBinder sessionToken, int userId) { + UserState userState = getOrCreateUserStateLocked(userId); + + // Remove the session state from the global session state map of the current user. + AdSessionState sessionState = userState.mAdSessionStateMap.remove(sessionToken); + + if (sessionState == null) { + Slogf.e(TAG, "sessionState null, no more remove session action!"); + return; + } + + // Also remove the session token from the session token list of the current client and + // service. + ClientState clientState = userState.mClientStateMap.get(sessionState.mClient.asBinder()); + if (clientState != null) { + clientState.mSessionTokens.remove(sessionToken); + if (clientState.isEmpty()) { + userState.mClientStateMap.remove(sessionState.mClient.asBinder()); + sessionState.mClient.asBinder().unlinkToDeath(clientState, 0); + } + } + + AdServiceState serviceState = userState.mAdServiceStateMap.get(sessionState.mComponent); + if (serviceState != null) { + serviceState.mSessionTokens.remove(sessionToken); + } + updateAdServiceConnectionLocked(sessionState.mComponent, userId); + } + + @GuardedBy("mLock") private void abortPendingCreateSessionRequestsLocked(ServiceState serviceState, String iAppServiceId, int userId) { // Let clients know the create session requests are failed. @@ -2237,6 +2767,28 @@ public class TvInteractiveAppManagerService extends SystemService { } @GuardedBy("mLock") + private void abortPendingCreateAdSessionRequestsLocked(AdServiceState serviceState, + String serviceId, int userId) { + // Let clients know the create session requests are failed. + UserState userState = getOrCreateUserStateLocked(userId); + List<AdSessionState> sessionsToAbort = new ArrayList<>(); + for (IBinder sessionToken : serviceState.mSessionTokens) { + AdSessionState sessionState = userState.mAdSessionStateMap.get(sessionToken); + if (sessionState.mSession == null + && (serviceState == null + || sessionState.mAdServiceId.equals(serviceId))) { + sessionsToAbort.add(sessionState); + } + } + for (AdSessionState sessionState : sessionsToAbort) { + removeAdSessionStateLocked(sessionState.mSessionToken, sessionState.mUserId); + sendAdSessionTokenToClientLocked(sessionState.mClient, + sessionState.mAdServiceId, null, null, sessionState.mSeq); + } + updateAdServiceConnectionLocked(serviceState.mComponent, userId); + } + + @GuardedBy("mLock") private void updateServiceConnectionLocked(ComponentName component, int userId) { UserState userState = getOrCreateUserStateLocked(userId); ServiceState serviceState = userState.mServiceStateMap.get(component); @@ -2284,10 +2836,64 @@ public class TvInteractiveAppManagerService extends SystemService { } } + @GuardedBy("mLock") + private void updateAdServiceConnectionLocked(ComponentName component, int userId) { + UserState userState = getOrCreateUserStateLocked(userId); + AdServiceState serviceState = userState.mAdServiceStateMap.get(component); + if (serviceState == null) { + return; + } + if (serviceState.mReconnecting) { + if (!serviceState.mSessionTokens.isEmpty()) { + // wait until all the sessions are removed. + return; + } + serviceState.mReconnecting = false; + } + + boolean shouldBind = (!serviceState.mSessionTokens.isEmpty()) + || (!serviceState.mPendingAppLinkCommand.isEmpty()); + + if (serviceState.mService == 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.mBound) { + // We have already bound to the service so we don't try to bind again until after we + // unbind later on. + return; + } + if (DEBUG) { + Slogf.d(TAG, "bindServiceAsUser(service=" + component + ", userId=" + userId + ")"); + } + + Intent i = new Intent(TvAdService.SERVICE_INTERFACE).setComponent(component); + serviceState.mBound = mContext.bindServiceAsUser( + i, serviceState.mConnection, + Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE_WHILE_AWAKE, + new UserHandle(userId)); + } else if (serviceState.mService != 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) { + Slogf.d(TAG, "unbindService(service=" + component + ")"); + } + mContext.unbindService(serviceState.mConnection); + userState.mAdServiceStateMap.remove(component); + } + } + private static final class UserState { private final int mUserId; + // A mapping from the TV AD service ID to its TvAdServiceState. + private Map<String, TvAdServiceState> mAdMap = new HashMap<>(); + // A mapping from the name of a TV Interactive App service to its state. + private final Map<ComponentName, AdServiceState> mAdServiceStateMap = new HashMap<>(); + // A mapping from the token of a TV Interactive App session to its state. + private final Map<IBinder, AdSessionState> mAdSessionStateMap = new HashMap<>(); // A mapping from the TV Interactive App ID to its TvInteractiveAppState. private Map<String, TvInteractiveAppState> mIAppMap = new HashMap<>(); + // A mapping from the TV AD service ID to its TvAdServiceState. + private Map<String, TvAdServiceState> mAdServiceMap = new HashMap<>(); // A mapping from the token of a client to its state. private final Map<IBinder, ClientState> mClientStateMap = new HashMap<>(); // A mapping from the name of a TV Interactive App service to its state. @@ -2299,6 +2905,8 @@ public class TvInteractiveAppManagerService extends SystemService { private final Set<String> mPackageSet = new HashSet<>(); // A list of all app link infos. private final List<AppLinkInfo> mAppLinkInfoList = new ArrayList<>(); + private final RemoteCallbackList<ITvAdManagerCallback> mAdCallbacks = + new RemoteCallbackList<>(); // A list of callbacks. private final RemoteCallbackList<ITvInteractiveAppManagerCallback> mCallbacks = @@ -2317,7 +2925,16 @@ public class TvInteractiveAppManagerService extends SystemService { private int mIAppNumber; } + private static final class TvAdServiceState { + private String mAdServiceId; + private ComponentName mComponentName; + private TvAdServiceInfo mInfo; + private int mUid; + private int mAdNumber; + } + private final class SessionState implements IBinder.DeathRecipient { + // TODO: rename SessionState and reorganize classes / methods of this file private final IBinder mSessionToken; private ITvInteractiveAppSession mSession; private final String mIAppServiceId; @@ -2359,6 +2976,49 @@ public class TvInteractiveAppManagerService extends SystemService { } } + private final class AdSessionState implements IBinder.DeathRecipient { + private final IBinder mSessionToken; + private ITvAdSession mSession; + private final String mAdServiceId; + + private final String mType; + private final ITvAdClient mClient; + private final int mSeq; + private final ComponentName mComponent; + + // The UID of the application that created the session. + // The application is usually the TV app. + private final int mCallingUid; + + // The PID of the application that created the session. + // The application is usually the TV app. + private final int mCallingPid; + + private final int mUserId; + + private AdSessionState(IBinder sessionToken, String serviceId, String type, + ComponentName componentName, ITvAdClient client, int seq, + int callingUid, int callingPid, int userId) { + mSessionToken = sessionToken; + mAdServiceId = serviceId; + mType = type; + mComponent = componentName; + mClient = client; + mSeq = seq; + mCallingUid = callingUid; + mCallingPid = callingPid; + mUserId = userId; + } + + @Override + public void binderDied() { + synchronized (mLock) { + mSession = null; + clearAdSessionAndNotifyClientLocked(this); + } + } + } + private final class ClientState implements IBinder.DeathRecipient { private final List<IBinder> mSessionTokens = new ArrayList<>(); @@ -2429,6 +3089,29 @@ public class TvInteractiveAppManagerService extends SystemService { } } + private final class AdServiceState { + private final List<IBinder> mSessionTokens = new ArrayList<>(); + private final ServiceConnection mConnection; + private final ComponentName mComponent; + private final String mAdServiceId; + private final List<Bundle> mPendingAppLinkCommand = new ArrayList<>(); + + private ITvAdService mService; + private AdServiceCallback mCallback; + private boolean mBound; + private boolean mReconnecting; + + private AdServiceState(ComponentName component, String tasId, int userId) { + mComponent = component; + mConnection = new AdServiceConnection(component, userId); + mAdServiceId = tasId; + } + + private void addPendingAppLinkCommand(Bundle command) { + mPendingAppLinkCommand.add(command); + } + } + private final class InteractiveAppServiceConnection implements ServiceConnection { private final ComponentName mComponent; private final int mUserId; @@ -2542,6 +3225,98 @@ public class TvInteractiveAppManagerService extends SystemService { } } + private final class AdServiceConnection implements ServiceConnection { + private final ComponentName mComponent; + private final int mUserId; + + private AdServiceConnection(ComponentName component, int userId) { + mComponent = component; + mUserId = userId; + } + + @Override + public void onServiceConnected(ComponentName component, IBinder service) { + if (DEBUG) { + Slogf.d(TAG, "onServiceConnected(component=" + component + ")"); + } + synchronized (mLock) { + UserState userState = getUserStateLocked(mUserId); + if (userState == null) { + // The user was removed while connecting. + mContext.unbindService(this); + return; + } + AdServiceState serviceState = userState.mAdServiceStateMap.get(mComponent); + serviceState.mService = ITvAdService.Stub.asInterface(service); + + // Register a callback, if we need to. + if (serviceState.mCallback == null) { + serviceState.mCallback = new AdServiceCallback(mComponent, mUserId); + try { + serviceState.mService.registerCallback(serviceState.mCallback); + } catch (RemoteException e) { + Slog.e(TAG, "error in registerCallback", e); + } + } + + if (!serviceState.mPendingAppLinkCommand.isEmpty()) { + for (Iterator<Bundle> it = serviceState.mPendingAppLinkCommand.iterator(); + it.hasNext(); ) { + Bundle command = it.next(); + final long identity = Binder.clearCallingIdentity(); + try { + serviceState.mService.sendAppLinkCommand(command); + it.remove(); + } catch (RemoteException e) { + Slogf.e(TAG, "error in sendAppLinkCommand(" + command + + ") when onServiceConnected", e); + } finally { + Binder.restoreCallingIdentity(identity); + } + } + } + + List<IBinder> tokensToBeRemoved = new ArrayList<>(); + + // And create sessions, if any. + for (IBinder sessionToken : serviceState.mSessionTokens) { + if (!createAdSessionInternalLocked( + serviceState.mService, sessionToken, mUserId)) { + tokensToBeRemoved.add(sessionToken); + } + } + + for (IBinder sessionToken : tokensToBeRemoved) { + removeAdSessionStateLocked(sessionToken, mUserId); + } + } + } + + @Override + public void onServiceDisconnected(ComponentName component) { + if (DEBUG) { + Slogf.d(TAG, "onServiceDisconnected(component=" + component + ")"); + } + if (!mComponent.equals(component)) { + throw new IllegalArgumentException("Mismatched ComponentName: " + + mComponent + " (expected), " + component + " (actual)."); + } + synchronized (mLock) { + UserState userState = getOrCreateUserStateLocked(mUserId); + AdServiceState serviceState = userState.mAdServiceStateMap.get(mComponent); + if (serviceState != null) { + serviceState.mReconnecting = true; + serviceState.mBound = false; + serviceState.mService = null; + serviceState.mCallback = null; + + abortPendingCreateAdSessionRequestsLocked(serviceState, null, mUserId); + } + } + } + } + + private final class ServiceCallback extends ITvInteractiveAppServiceCallback.Stub { private final ComponentName mComponent; private final int mUserId; @@ -2567,6 +3342,17 @@ public class TvInteractiveAppManagerService extends SystemService { } } + + private final class AdServiceCallback extends ITvAdServiceCallback.Stub { + private final ComponentName mComponent; + private final int mUserId; + + AdServiceCallback(ComponentName component, int userId) { + mComponent = component; + mUserId = userId; + } + } + private final class SessionCallback extends ITvInteractiveAppSessionCallback.Stub { private final SessionState mSessionState; private final InputChannel[] mInputChannels; @@ -2798,6 +3584,23 @@ public class TvInteractiveAppManagerService extends SystemService { } @Override + public void onRequestSelectedTrackInfo() { + synchronized (mLock) { + if (DEBUG) { + Slogf.d(TAG, "onRequestSelectedTrackInfo"); + } + if (mSessionState.mSession == null || mSessionState.mClient == null) { + return; + } + try { + mSessionState.mClient.onRequestSelectedTrackInfo(mSessionState.mSeq); + } catch (RemoteException e) { + Slogf.e(TAG, "error in onRequestSelectedTrackInfo", e); + } + } + } + + @Override public void onRequestCurrentTvInputId() { synchronized (mLock) { if (DEBUG) { @@ -3110,6 +3913,85 @@ public class TvInteractiveAppManagerService extends SystemService { } } + private final class AdSessionCallback extends ITvAdSessionCallback.Stub { + private final AdSessionState mSessionState; + private final InputChannel[] mInputChannels; + + AdSessionCallback(AdSessionState sessionState, InputChannel[] channels) { + mSessionState = sessionState; + mInputChannels = channels; + } + + @Override + public void onSessionCreated(ITvAdSession session) { + if (DEBUG) { + Slogf.d(TAG, "onSessionCreated(adServiceId=" + + mSessionState.mAdServiceId + ")"); + } + synchronized (mLock) { + mSessionState.mSession = session; + if (session != null && addAdSessionTokenToClientStateLocked(session)) { + sendAdSessionTokenToClientLocked( + mSessionState.mClient, + mSessionState.mAdServiceId, + mSessionState.mSessionToken, + mInputChannels[0], + mSessionState.mSeq); + } else { + removeAdSessionStateLocked(mSessionState.mSessionToken, mSessionState.mUserId); + sendAdSessionTokenToClientLocked(mSessionState.mClient, + mSessionState.mAdServiceId, null, null, mSessionState.mSeq); + } + mInputChannels[0].dispose(); + } + } + + @Override + public void onLayoutSurface(int left, int top, int right, int bottom) { + synchronized (mLock) { + if (DEBUG) { + Slogf.d(TAG, "onLayoutSurface (left=" + left + ", top=" + top + + ", right=" + right + ", bottom=" + bottom + ",)"); + } + if (mSessionState.mSession == null || mSessionState.mClient == null) { + return; + } + try { + mSessionState.mClient.onLayoutSurface(left, top, right, bottom, + mSessionState.mSeq); + } catch (RemoteException e) { + Slogf.e(TAG, "error in onLayoutSurface", e); + } + } + } + + @GuardedBy("mLock") + private boolean addAdSessionTokenToClientStateLocked(ITvAdSession session) { + try { + session.asBinder().linkToDeath(mSessionState, 0); + } catch (RemoteException e) { + Slogf.e(TAG, "session process has already died", e); + return false; + } + + IBinder clientToken = mSessionState.mClient.asBinder(); + UserState userState = getOrCreateUserStateLocked(mSessionState.mUserId); + ClientState clientState = userState.mClientStateMap.get(clientToken); + if (clientState == null) { + clientState = new ClientState(clientToken, mSessionState.mUserId); + try { + clientToken.linkToDeath(clientState, 0); + } catch (RemoteException e) { + Slogf.e(TAG, "client process has already died", e); + return false; + } + userState.mClientStateMap.put(clientToken, clientState); + } + clientState.mSessionTokens.add(mSessionState.mSessionToken); + return true; + } + } + private static class SessionNotFoundException extends IllegalArgumentException { SessionNotFoundException(String name) { super(name); diff --git a/services/core/java/com/android/server/uri/UriGrantsManagerInternal.java b/services/core/java/com/android/server/uri/UriGrantsManagerInternal.java index e54b40eb334c..03c75e018dab 100644 --- a/services/core/java/com/android/server/uri/UriGrantsManagerInternal.java +++ b/services/core/java/com/android/server/uri/UriGrantsManagerInternal.java @@ -37,7 +37,17 @@ public interface UriGrantsManagerInternal { void revokeUriPermission(String targetPackage, int callingUid, GrantUri grantUri, final int modeFlags); - boolean checkUriPermission(GrantUri grantUri, int uid, final int modeFlags); + /** + * Check if the uid has permission to the URI in grantUri. + * + * @param isFullAccessForContentUri If true, the URI has to be a content URI + * and the method will consider full access. + * Otherwise, the method will only consider + * URI grants. + */ + boolean checkUriPermission(GrantUri grantUri, int uid, int modeFlags, + boolean isFullAccessForContentUri); + int checkGrantUriPermission( int callingUid, String targetPkg, Uri uri, int modeFlags, int userId); diff --git a/services/core/java/com/android/server/uri/UriGrantsManagerService.java b/services/core/java/com/android/server/uri/UriGrantsManagerService.java index e501b9dc9959..ce2cbed0c9a9 100644 --- a/services/core/java/com/android/server/uri/UriGrantsManagerService.java +++ b/services/core/java/com/android/server/uri/UriGrantsManagerService.java @@ -25,6 +25,7 @@ import static android.content.Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION; import static android.content.Intent.FLAG_GRANT_PREFIX_URI_PERMISSION; import static android.content.pm.PackageManager.MATCH_ANY_USER; import static android.content.pm.PackageManager.MATCH_DEBUG_TRIAGED_MISSING; +import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_AUTO; import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_AWARE; import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_UNAWARE; import static android.content.pm.PackageManager.MATCH_UNINSTALLED_PACKAGES; @@ -1103,7 +1104,8 @@ public class UriGrantsManagerService extends IUriGrantsManager.Stub implements */ private int checkGrantUriPermissionUnlocked(int callingUid, String targetPkg, GrantUri grantUri, int modeFlags, int lastTargetUid) { - if (!Intent.isAccessUriMode(modeFlags)) { + if (!isContentUriWithAccessModeFlags(grantUri, modeFlags, + /* logAction */ "grant URI permission")) { return -1; } @@ -1111,12 +1113,6 @@ public class UriGrantsManagerService extends IUriGrantsManager.Stub implements if (DEBUG) Slog.v(TAG, "Checking grant " + targetPkg + " permission to " + grantUri); } - // If this is not a content: uri, we can't do anything with it. - if (!ContentResolver.SCHEME_CONTENT.equals(grantUri.uri.getScheme())) { - if (DEBUG) Slog.v(TAG, "Can't grant URI permission for non-content URI: " + grantUri); - return -1; - } - // Bail early if system is trying to hand out permissions directly; it // must always grant permissions on behalf of someone explicit. final int callingAppId = UserHandle.getAppId(callingUid); @@ -1137,7 +1133,7 @@ public class UriGrantsManagerService extends IUriGrantsManager.Stub implements final String authority = grantUri.uri.getAuthority(); final ProviderInfo pi = getProviderInfo(authority, grantUri.sourceUserId, - MATCH_DEBUG_TRIAGED_MISSING, callingUid); + MATCH_DIRECT_BOOT_AUTO, callingUid); if (pi == null) { Slog.w(TAG, "No content provider found for permission check: " + grantUri.uri.toSafeString()); @@ -1285,6 +1281,65 @@ public class UriGrantsManagerService extends IUriGrantsManager.Stub implements return targetUid; } + private boolean isContentUriWithAccessModeFlags(GrantUri grantUri, int modeFlags, + String logAction) { + if (!Intent.isAccessUriMode(modeFlags)) { + if (DEBUG) Slog.v(TAG, "Mode flags are not access URI mode flags: " + modeFlags); + return false; + } + + if (!ContentResolver.SCHEME_CONTENT.equals(grantUri.uri.getScheme())) { + if (DEBUG) { + Slog.v(TAG, "Can't " + logAction + " on non-content URI: " + grantUri); + } + return false; + } + + return true; + } + + /** Check if the uid has permission to the content URI in grantUri. */ + private boolean checkContentUriPermissionFullUnlocked(GrantUri grantUri, int uid, + int modeFlags) { + if (uid < 0) { + throw new IllegalArgumentException("Uid must be positive for the content URI " + + "permission check of " + grantUri.uri.toSafeString()); + } + + if (!isContentUriWithAccessModeFlags(grantUri, modeFlags, + /* logAction */ "check content URI permission")) { + throw new IllegalArgumentException("The URI must be a content URI and the mode " + + "flags must be at least read and/or write for the content URI permission " + + "check of " + grantUri.uri.toSafeString()); + } + + final int appId = UserHandle.getAppId(uid); + if ((appId == SYSTEM_UID) || (appId == ROOT_UID)) { + return true; + } + + // Retrieve the URI's content provider + final String authority = grantUri.uri.getAuthority(); + ProviderInfo pi = getProviderInfo(authority, grantUri.sourceUserId, MATCH_DIRECT_BOOT_AUTO, + uid); + + if (pi == null) { + Slog.w(TAG, "No content provider found for content URI permission check: " + + grantUri.uri.toSafeString()); + return false; + } + + // Check if it has general permission to the URI's content provider + if (checkHoldingPermissionsUnlocked(pi, grantUri, uid, modeFlags)) { + return true; + } + + // Check if it has explicitly granted permissions to the URI + synchronized (mLock) { + return checkUriPermissionLocked(grantUri, uid, modeFlags); + } + } + /** * @param userId The userId in which the uri is to be resolved. */ @@ -1482,7 +1537,12 @@ public class UriGrantsManagerService extends IUriGrantsManager.Stub implements } @Override - public boolean checkUriPermission(GrantUri grantUri, int uid, int modeFlags) { + public boolean checkUriPermission(GrantUri grantUri, int uid, int modeFlags, + boolean isFullAccessForContentUri) { + if (isFullAccessForContentUri) { + return UriGrantsManagerService.this.checkContentUriPermissionFullUnlocked(grantUri, + uid, modeFlags); + } synchronized (mLock) { return UriGrantsManagerService.this.checkUriPermissionLocked(grantUri, uid, modeFlags); diff --git a/services/core/java/com/android/server/webkit/WebViewUpdateServiceImpl.java b/services/core/java/com/android/server/webkit/WebViewUpdateServiceImpl.java index 60dc4ff224bc..d95b431752ec 100644 --- a/services/core/java/com/android/server/webkit/WebViewUpdateServiceImpl.java +++ b/services/core/java/com/android/server/webkit/WebViewUpdateServiceImpl.java @@ -348,6 +348,10 @@ class WebViewUpdateServiceImpl implements WebViewUpdateServiceInterface { private void pinWebviewIfRequired(ApplicationInfo appInfo) { PinnerService pinnerService = LocalServices.getService(PinnerService.class); + if (pinnerService == null) { + // This happens in unit tests which do not have services. + return; + } int webviewPinQuota = pinnerService.getWebviewPinQuota(); if (webviewPinQuota <= 0) { return; diff --git a/services/core/java/com/android/server/webkit/WebViewUpdateServiceImpl2.java b/services/core/java/com/android/server/webkit/WebViewUpdateServiceImpl2.java index 29782d9b8b88..f4fb1a108663 100644 --- a/services/core/java/com/android/server/webkit/WebViewUpdateServiceImpl2.java +++ b/services/core/java/com/android/server/webkit/WebViewUpdateServiceImpl2.java @@ -159,11 +159,28 @@ class WebViewUpdateServiceImpl2 implements WebViewUpdateServiceInterface { } } + private boolean shouldTriggerRepairLocked() { + if (mCurrentWebViewPackage == null) { + return true; + } + WebViewProviderInfo defaultProvider = getDefaultWebViewPackage(); + if (mCurrentWebViewPackage.packageName.equals(defaultProvider.packageName)) { + List<UserPackage> userPackages = + mSystemInterface.getPackageInfoForProviderAllUsers( + mContext, defaultProvider); + return !isInstalledAndEnabledForAllUsers(userPackages); + } else { + return false; + } + } + @Override public void prepareWebViewInSystemServer() { try { + boolean repairNeeded = true; synchronized (mLock) { mCurrentWebViewPackage = findPreferredWebViewPackage(); + repairNeeded = shouldTriggerRepairLocked(); String userSetting = mSystemInterface.getUserChosenWebViewProvider(mContext); if (userSetting != null && !userSetting.equals(mCurrentWebViewPackage.packageName)) { @@ -177,26 +194,25 @@ class WebViewUpdateServiceImpl2 implements WebViewUpdateServiceInterface { } onWebViewProviderChanged(mCurrentWebViewPackage); } + + if (repairNeeded) { + // We didn't find a valid WebView implementation. Try explicitly re-enabling the + // default package for all users in case it was disabled, even if we already did the + // one-time migration before. If this actually changes the state, we will see the + // PackageManager broadcast shortly and try again. + WebViewProviderInfo defaultProvider = getDefaultWebViewPackage(); + Slog.w( + TAG, + "No provider available for all users, trying to enable " + + defaultProvider.packageName); + mSystemInterface.enablePackageForAllUsers( + mContext, defaultProvider.packageName, true); + } + } catch (Throwable t) { // Log and discard errors at this stage as we must not crash the system server. Slog.e(TAG, "error preparing webview provider from system server", t); } - - if (getCurrentWebViewPackage() == null) { - // We didn't find a valid WebView implementation. Try explicitly re-enabling the - // fallback package for all users in case it was disabled, even if we already did the - // one-time migration before. If this actually changes the state, we will see the - // PackageManager broadcast shortly and try again. - WebViewProviderInfo[] webviewProviders = mSystemInterface.getWebViewPackages(); - WebViewProviderInfo fallbackProvider = getFallbackProvider(webviewProviders); - if (fallbackProvider != null) { - Slog.w(TAG, "No valid provider, trying to enable " + fallbackProvider.packageName); - mSystemInterface.enablePackageForAllUsers(mContext, fallbackProvider.packageName, - true); - } else { - Slog.e(TAG, "No valid provider and no fallback available."); - } - } } private void startZygoteWhenReady() { @@ -421,42 +437,43 @@ class WebViewUpdateServiceImpl2 implements WebViewUpdateServiceInterface { /** * Returns either the package info of the WebView provider determined in the following way: - * If the user has chosen a provider then use that if it is valid, - * otherwise use the first package in the webview priority list that is valid. - * + * If the user has chosen a provider then use that if it is valid, enabled and installed + * for all users, otherwise use the default provider. */ private PackageInfo findPreferredWebViewPackage() throws WebViewPackageMissingException { - ProviderAndPackageInfo[] providers = getValidWebViewPackagesAndInfos(); - - String userChosenProvider = mSystemInterface.getUserChosenWebViewProvider(mContext); - // If the user has chosen provider, use that (if it's installed and enabled for all // users). - for (ProviderAndPackageInfo providerAndPackage : providers) { - if (providerAndPackage.provider.packageName.equals(userChosenProvider)) { - // userPackages can contain null objects. - List<UserPackage> userPackages = - mSystemInterface.getPackageInfoForProviderAllUsers(mContext, - providerAndPackage.provider); - if (isInstalledAndEnabledForAllUsers(userPackages)) { - return providerAndPackage.packageInfo; + String userChosenPackageName = mSystemInterface.getUserChosenWebViewProvider(mContext); + WebViewProviderInfo userChosenProvider = + getWebViewProviderForPackage(userChosenPackageName); + if (userChosenProvider != null) { + try { + PackageInfo packageInfo = + mSystemInterface.getPackageInfoForProvider(userChosenProvider); + if (validityResult(userChosenProvider, packageInfo) == VALIDITY_OK) { + List<UserPackage> userPackages = + mSystemInterface.getPackageInfoForProviderAllUsers( + mContext, userChosenProvider); + if (isInstalledAndEnabledForAllUsers(userPackages)) { + return packageInfo; + } } + } catch (NameNotFoundException e) { + Slog.w(TAG, "User chosen WebView package (" + userChosenPackageName + + ") not found"); } } - // User did not choose, or the choice failed; use the most stable provider that is - // installed and enabled for all users, and available by default (not through - // user choice). - for (ProviderAndPackageInfo providerAndPackage : providers) { - if (providerAndPackage.provider.availableByDefault) { - // userPackages can contain null objects. - List<UserPackage> userPackages = - mSystemInterface.getPackageInfoForProviderAllUsers(mContext, - providerAndPackage.provider); - if (isInstalledAndEnabledForAllUsers(userPackages)) { - return providerAndPackage.packageInfo; - } + // User did not choose, or the choice failed; return the default provider even if it is not + // installed or enabled for all users. + WebViewProviderInfo defaultProvider = getDefaultWebViewPackage(); + try { + PackageInfo packageInfo = mSystemInterface.getPackageInfoForProvider(defaultProvider); + if (validityResult(defaultProvider, packageInfo) == VALIDITY_OK) { + return packageInfo; } + } catch (NameNotFoundException e) { + Slog.w(TAG, "Default WebView package (" + defaultProvider.packageName + ") not found"); } // This should never happen during normal operation (only with modified system images). @@ -464,6 +481,16 @@ class WebViewUpdateServiceImpl2 implements WebViewUpdateServiceInterface { throw new WebViewPackageMissingException("Could not find a loadable WebView package"); } + private WebViewProviderInfo getWebViewProviderForPackage(String packageName) { + WebViewProviderInfo[] allProviders = getWebViewPackages(); + for (int n = 0; n < allProviders.length; n++) { + if (allProviders[n].packageName.equals(packageName)) { + return allProviders[n]; + } + } + return null; + } + /** * Return true iff {@param packageInfos} point to only installed and enabled packages. * The given packages {@param packageInfos} should all be pointing to the same package, but each diff --git a/services/core/java/com/android/server/wm/ActivityClientController.java b/services/core/java/com/android/server/wm/ActivityClientController.java index 676203bc746a..2e0546eee8e7 100644 --- a/services/core/java/com/android/server/wm/ActivityClientController.java +++ b/services/core/java/com/android/server/wm/ActivityClientController.java @@ -778,17 +778,22 @@ class ActivityClientController extends IActivityClientController.Stub { try { synchronized (mGlobalLock) { final ActivityRecord r = ActivityRecord.isInRootTaskLocked(token); + if (r == null) { + return false; + } // Create a transition if the activity is playing in case the below activity didn't // commit invisible. That's because if any activity below this one has changed its // visibility while playing transition, there won't able to commit visibility until // the running transition finish. - final Transition transition = r != null - && r.mTransitionController.inPlayingTransition(r) + final Transition transition = r.mTransitionController.isShellTransitionsEnabled() && !r.mTransitionController.isCollecting() ? r.mTransitionController.createTransition(TRANSIT_TO_BACK) : null; - final boolean changed = r != null && r.setOccludesParent(true); + final boolean changed = r.setOccludesParent(true); if (transition != null) { if (changed) { + // Always set as scene transition because it expects to be a jump-cut. + transition.setOverrideAnimation(TransitionInfo.AnimationOptions + .makeSceneTransitionAnimOptions(), null, null); r.mTransitionController.requestStartTransition(transition, null /*startTask */, null /* remoteTransition */, null /* displayChange */); diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index 69fbe6ba3c29..9b1f9c8441ad 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -3093,7 +3093,6 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A final boolean changed = occludesParent != mOccludesParent; mOccludesParent = occludesParent; setMainWindowOpaque(occludesParent); - mWmService.mWindowPlacerLocked.requestTraversal(); if (changed && task != null && !occludesParent) { getRootTask().convertActivityToTranslucent(this); diff --git a/services/core/java/com/android/server/wm/ActivityStartInterceptor.java b/services/core/java/com/android/server/wm/ActivityStartInterceptor.java index e7621ffe8e3c..182e1c133790 100644 --- a/services/core/java/com/android/server/wm/ActivityStartInterceptor.java +++ b/services/core/java/com/android/server/wm/ActivityStartInterceptor.java @@ -18,6 +18,7 @@ package com.android.server.wm; import static android.app.ActivityManager.INTENT_SENDER_ACTIVITY; import static android.app.ActivityOptions.ANIM_OPEN_CROSS_PROFILE_APPS; +import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED; import static android.app.PendingIntent.FLAG_CANCEL_CURRENT; import static android.app.PendingIntent.FLAG_IMMUTABLE; import static android.app.PendingIntent.FLAG_ONE_SHOT; @@ -144,22 +145,20 @@ class ActivityStartInterceptor { } private IntentSender createIntentSenderForOriginalIntent(int callingUid, int flags) { - Bundle bOptions = deferCrossProfileAppsAnimationIfNecessary(); + ActivityOptions activityOptions = deferCrossProfileAppsAnimationIfNecessary(); + activityOptions.setPendingIntentCreatorBackgroundActivityStartMode( + MODE_BACKGROUND_ACTIVITY_START_ALLOWED); final TaskFragment taskFragment = getLaunchTaskFragment(); // If the original intent is going to be embedded, try to forward the embedding TaskFragment // and its task id to embed back the original intent. if (taskFragment != null) { - ActivityOptions activityOptions = bOptions != null - ? ActivityOptions.fromBundle(bOptions) - : ActivityOptions.makeBasic(); activityOptions.setLaunchTaskFragmentToken(taskFragment.getFragmentToken()); - bOptions = activityOptions.toBundle(); } final IIntentSender target = mService.getIntentSenderLocked( INTENT_SENDER_ACTIVITY, mCallingPackage, mCallingFeatureId, callingUid, mUserId, null /*token*/, null /*resultCode*/, 0 /*requestCode*/, new Intent[] { mIntent }, new String[] { mResolvedType }, - flags, bOptions); + flags, activityOptions.toBundle()); return new IntentSender(target); } @@ -272,12 +271,12 @@ class ActivityStartInterceptor { * * @return the activity option used to start the original intent. */ - private Bundle deferCrossProfileAppsAnimationIfNecessary() { + private ActivityOptions deferCrossProfileAppsAnimationIfNecessary() { if (hasCrossProfileAnimation()) { mActivityOptions = null; - return ActivityOptions.makeOpenCrossProfileAppsAnimation().toBundle(); + return ActivityOptions.makeOpenCrossProfileAppsAnimation(); } - return null; + return ActivityOptions.makeBasic(); } private boolean interceptQuietProfileIfNeeded() { diff --git a/services/core/java/com/android/server/wm/ActivityStarter.java b/services/core/java/com/android/server/wm/ActivityStarter.java index 13f71521c240..f6d77ea33598 100644 --- a/services/core/java/com/android/server/wm/ActivityStarter.java +++ b/services/core/java/com/android/server/wm/ActivityStarter.java @@ -60,6 +60,7 @@ import static android.window.TaskFragmentOperation.OP_TYPE_START_ACTIVITY_IN_TAS import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_CONFIGURATION; import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_TASKS; +import static com.android.server.pm.PackageArchiver.isArchivingEnabled; import static com.android.server.wm.ActivityRecord.State.RESUMED; import static com.android.server.wm.ActivityTaskManagerDebugConfig.DEBUG_PERMISSIONS_REVIEW; import static com.android.server.wm.ActivityTaskManagerDebugConfig.DEBUG_RESULTS; @@ -104,7 +105,6 @@ import android.content.IntentSender; import android.content.pm.ActivityInfo; import android.content.pm.ApplicationInfo; import android.content.pm.AuxiliaryResolveInfo; -import android.content.pm.Flags; import android.content.pm.PackageManager; import android.content.pm.PackageManagerInternal; import android.content.pm.ResolveInfo; @@ -1034,7 +1034,7 @@ class ActivityStarter { } if (err == ActivityManager.START_SUCCESS && aInfo == null) { - if (Flags.archiving()) { + if (isArchivingEnabled()) { PackageArchiver packageArchiver = mService .getPackageManagerInternalLocked() .getPackageArchiver(); diff --git a/services/core/java/com/android/server/wm/BackNavigationController.java b/services/core/java/com/android/server/wm/BackNavigationController.java index 8aaf76a165ab..2bd49bfa6219 100644 --- a/services/core/java/com/android/server/wm/BackNavigationController.java +++ b/services/core/java/com/android/server/wm/BackNavigationController.java @@ -1591,7 +1591,6 @@ class BackNavigationController { private static void setLaunchBehind(@NonNull ActivityRecord activity) { if (!activity.isVisibleRequested()) { - activity.setVisibility(true); // The transition could commit the visibility and in the finishing state, that could // skip commitVisibility call in setVisibility cause the activity won't visible here. // Call it again to make sure the activity could be visible while handling the pending @@ -1669,10 +1668,14 @@ class BackNavigationController { ProtoLog.d(WM_DEBUG_BACK_PREVIEW, "onBackNavigationDone backType=%s, " + "triggerBack=%b", backType, triggerBack); - mNavigationMonitor.stopMonitorForRemote(); - mBackAnimationInProgress = false; - mShowWallpaper = false; - mPendingAnimationBuilder = null; + synchronized (mWindowManagerService.mGlobalLock) { + mNavigationMonitor.stopMonitorForRemote(); + mBackAnimationInProgress = false; + mShowWallpaper = false; + // All animation should be done, clear any un-send animation. + mPendingAnimation = null; + mPendingAnimationBuilder = null; + } } static TaskSnapshot getSnapshot(@NonNull WindowContainer w, diff --git a/services/core/java/com/android/server/wm/LegacyTransitionTracer.java b/services/core/java/com/android/server/wm/LegacyTransitionTracer.java new file mode 100644 index 000000000000..fb2d5bec3908 --- /dev/null +++ b/services/core/java/com/android/server/wm/LegacyTransitionTracer.java @@ -0,0 +1,331 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm; + +import static android.os.Build.IS_USER; + +import static com.android.server.wm.shell.TransitionTraceProto.MAGIC_NUMBER; +import static com.android.server.wm.shell.TransitionTraceProto.MAGIC_NUMBER_H; +import static com.android.server.wm.shell.TransitionTraceProto.MAGIC_NUMBER_L; +import static com.android.server.wm.shell.TransitionTraceProto.REAL_TO_ELAPSED_TIME_OFFSET_NANOS; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.os.SystemClock; +import android.os.Trace; +import android.util.Log; +import android.util.proto.ProtoOutputStream; + +import com.android.internal.util.TraceBuffer; +import com.android.server.wm.Transition.ChangeInfo; + +import java.io.File; +import java.io.IOException; +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.concurrent.TimeUnit; + +/** + * Helper class to collect and dump transition traces. + */ +class LegacyTransitionTracer implements TransitionTracer { + + private static final String LOG_TAG = "TransitionTracer"; + + private static final int ALWAYS_ON_TRACING_CAPACITY = 15 * 1024; // 15 KB + private static final int ACTIVE_TRACING_BUFFER_CAPACITY = 5000 * 1024; // 5 MB + + // This will be the size the proto output streams are initialized to. + // Ideally this should fit most or all the proto objects we will create and be no bigger than + // that to ensure to don't use excessive amounts of memory. + private static final int CHUNK_SIZE = 64; + + static final String WINSCOPE_EXT = ".winscope"; + private static final String TRACE_FILE = + "/data/misc/wmtrace/wm_transition_trace" + WINSCOPE_EXT; + private static final long MAGIC_NUMBER_VALUE = ((long) MAGIC_NUMBER_H << 32) | MAGIC_NUMBER_L; + + private final TraceBuffer mTraceBuffer = new TraceBuffer(ALWAYS_ON_TRACING_CAPACITY); + + private final Object mEnabledLock = new Object(); + private volatile boolean mActiveTracingEnabled = false; + + /** + * Records key information about a transition that has been sent to Shell to be played. + * More information will be appended to the same proto object once the transition is finished or + * aborted. + * Transition information won't be added to the trace buffer until + * {@link #logFinishedTransition} or {@link #logAbortedTransition} is called for this + * transition. + * + * @param transition The transition that has been sent to Shell. + * @param targets Information about the target windows of the transition. + */ + @Override + public void logSentTransition(Transition transition, ArrayList<ChangeInfo> targets) { + try { + final ProtoOutputStream outputStream = new ProtoOutputStream(CHUNK_SIZE); + final long protoToken = outputStream + .start(com.android.server.wm.shell.TransitionTraceProto.TRANSITIONS); + outputStream.write(com.android.server.wm.shell.Transition.ID, transition.getSyncId()); + outputStream.write(com.android.server.wm.shell.Transition.CREATE_TIME_NS, + transition.mLogger.mCreateTimeNs); + outputStream.write(com.android.server.wm.shell.Transition.SEND_TIME_NS, + transition.mLogger.mSendTimeNs); + outputStream.write(com.android.server.wm.shell.Transition.START_TRANSACTION_ID, + transition.getStartTransaction().getId()); + outputStream.write(com.android.server.wm.shell.Transition.FINISH_TRANSACTION_ID, + transition.getFinishTransaction().getId()); + dumpTransitionTargetsToProto(outputStream, transition, targets); + outputStream.end(protoToken); + + mTraceBuffer.add(outputStream); + } catch (Exception e) { + // Don't let any errors in the tracing cause the transition to fail + Log.e(LOG_TAG, "Unexpected exception thrown while logging transitions", e); + } + } + + /** + * Completes the information dumped in {@link #logSentTransition} for a transition + * that has finished or aborted, and add the proto object to the trace buffer. + * + * @param transition The transition that has finished. + */ + @Override + public void logFinishedTransition(Transition transition) { + try { + final ProtoOutputStream outputStream = new ProtoOutputStream(CHUNK_SIZE); + final long protoToken = outputStream + .start(com.android.server.wm.shell.TransitionTraceProto.TRANSITIONS); + outputStream.write(com.android.server.wm.shell.Transition.ID, transition.getSyncId()); + outputStream.write(com.android.server.wm.shell.Transition.FINISH_TIME_NS, + transition.mLogger.mFinishTimeNs); + outputStream.end(protoToken); + + mTraceBuffer.add(outputStream); + } catch (Exception e) { + // Don't let any errors in the tracing cause the transition to fail + Log.e(LOG_TAG, "Unexpected exception thrown while logging transitions", e); + } + } + + /** + * Same as {@link #logFinishedTransition} but don't add the transition to the trace buffer + * unless actively tracing. + * + * @param transition The transition that has been aborted + */ + @Override + public void logAbortedTransition(Transition transition) { + try { + final ProtoOutputStream outputStream = new ProtoOutputStream(CHUNK_SIZE); + final long protoToken = outputStream + .start(com.android.server.wm.shell.TransitionTraceProto.TRANSITIONS); + outputStream.write(com.android.server.wm.shell.Transition.ID, transition.getSyncId()); + outputStream.write(com.android.server.wm.shell.Transition.ABORT_TIME_NS, + transition.mLogger.mAbortTimeNs); + outputStream.end(protoToken); + + mTraceBuffer.add(outputStream); + } catch (Exception e) { + // Don't let any errors in the tracing cause the transition to fail + Log.e(LOG_TAG, "Unexpected exception thrown while logging transitions", e); + } + } + + @Override + public void logRemovingStartingWindow(@NonNull StartingData startingData) { + if (startingData.mTransitionId == 0) { + return; + } + try { + final ProtoOutputStream outputStream = new ProtoOutputStream(CHUNK_SIZE); + final long protoToken = outputStream + .start(com.android.server.wm.shell.TransitionTraceProto.TRANSITIONS); + outputStream.write(com.android.server.wm.shell.Transition.ID, + startingData.mTransitionId); + outputStream.write( + com.android.server.wm.shell.Transition.STARTING_WINDOW_REMOVE_TIME_NS, + SystemClock.elapsedRealtimeNanos()); + outputStream.end(protoToken); + + mTraceBuffer.add(outputStream); + } catch (Exception e) { + Log.e(LOG_TAG, "Unexpected exception thrown while logging transitions", e); + } + } + + private void dumpTransitionTargetsToProto(ProtoOutputStream outputStream, + Transition transition, ArrayList<ChangeInfo> targets) { + Trace.beginSection("TransitionTracer#dumpTransitionTargetsToProto"); + if (mActiveTracingEnabled) { + outputStream.write(com.android.server.wm.shell.Transition.ID, + transition.getSyncId()); + } + + outputStream.write(com.android.server.wm.shell.Transition.TYPE, transition.mType); + outputStream.write(com.android.server.wm.shell.Transition.FLAGS, transition.getFlags()); + + for (int i = 0; i < targets.size(); ++i) { + final long changeToken = outputStream + .start(com.android.server.wm.shell.Transition.TARGETS); + + final Transition.ChangeInfo target = targets.get(i); + + final int layerId; + if (target.mContainer.mSurfaceControl.isValid()) { + layerId = target.mContainer.mSurfaceControl.getLayerId(); + } else { + layerId = -1; + } + + outputStream.write(com.android.server.wm.shell.Target.MODE, target.mReadyMode); + outputStream.write(com.android.server.wm.shell.Target.FLAGS, target.mReadyFlags); + outputStream.write(com.android.server.wm.shell.Target.LAYER_ID, layerId); + + if (mActiveTracingEnabled) { + // What we use in the WM trace + final int windowId = System.identityHashCode(target.mContainer); + outputStream.write(com.android.server.wm.shell.Target.WINDOW_ID, windowId); + } + + outputStream.end(changeToken); + } + + Trace.endSection(); + } + + /** + * Starts collecting transitions for the trace. + * If called while a trace is already running, this will reset the trace. + */ + @Override + public void startTrace(@Nullable PrintWriter pw) { + if (IS_USER) { + LogAndPrintln.e(pw, "Tracing is not supported on user builds."); + return; + } + Trace.beginSection("TransitionTracer#startTrace"); + LogAndPrintln.i(pw, "Starting shell transition trace."); + synchronized (mEnabledLock) { + mActiveTracingEnabled = true; + mTraceBuffer.resetBuffer(); + mTraceBuffer.setCapacity(ACTIVE_TRACING_BUFFER_CAPACITY); + } + Trace.endSection(); + } + + /** + * Stops collecting the transition trace and dump to trace to file. + * + * Dumps the trace to @link{TRACE_FILE}. + */ + @Override + public void stopTrace(@Nullable PrintWriter pw) { + stopTrace(pw, new File(TRACE_FILE)); + } + + /** + * Stops collecting the transition trace and dump to trace to file. + * @param outputFile The file to dump the transition trace to. + */ + public void stopTrace(@Nullable PrintWriter pw, File outputFile) { + if (IS_USER) { + LogAndPrintln.e(pw, "Tracing is not supported on user builds."); + return; + } + Trace.beginSection("TransitionTracer#stopTrace"); + LogAndPrintln.i(pw, "Stopping shell transition trace."); + synchronized (mEnabledLock) { + mActiveTracingEnabled = false; + writeTraceToFileLocked(pw, outputFile); + mTraceBuffer.resetBuffer(); + mTraceBuffer.setCapacity(ALWAYS_ON_TRACING_CAPACITY); + } + Trace.endSection(); + } + + /** + * Being called while taking a bugreport so that tracing files can be included in the bugreport. + * + * @param pw Print writer + */ + @Override + public void saveForBugreport(@Nullable PrintWriter pw) { + if (IS_USER) { + LogAndPrintln.e(pw, "Tracing is not supported on user builds."); + return; + } + Trace.beginSection("TransitionTracer#saveForBugreport"); + synchronized (mEnabledLock) { + final File outputFile = new File(TRACE_FILE); + writeTraceToFileLocked(pw, outputFile); + } + Trace.endSection(); + } + + @Override + public boolean isTracing() { + return mActiveTracingEnabled; + } + + private void writeTraceToFileLocked(@Nullable PrintWriter pw, File file) { + Trace.beginSection("TransitionTracer#writeTraceToFileLocked"); + try { + ProtoOutputStream proto = new ProtoOutputStream(CHUNK_SIZE); + proto.write(MAGIC_NUMBER, MAGIC_NUMBER_VALUE); + long timeOffsetNs = + TimeUnit.MILLISECONDS.toNanos(System.currentTimeMillis()) + - SystemClock.elapsedRealtimeNanos(); + proto.write(REAL_TO_ELAPSED_TIME_OFFSET_NANOS, timeOffsetNs); + int pid = android.os.Process.myPid(); + LogAndPrintln.i(pw, "Writing file to " + file.getAbsolutePath() + + " from process " + pid); + mTraceBuffer.writeTraceToFile(file, proto); + } catch (IOException e) { + LogAndPrintln.e(pw, "Unable to write buffer to file", e); + } + Trace.endSection(); + } + + private static class LogAndPrintln { + private static void i(@Nullable PrintWriter pw, String msg) { + Log.i(LOG_TAG, msg); + if (pw != null) { + pw.println(msg); + pw.flush(); + } + } + + private static void e(@Nullable PrintWriter pw, String msg) { + Log.e(LOG_TAG, msg); + if (pw != null) { + pw.println("ERROR: " + msg); + pw.flush(); + } + } + + private static void e(@Nullable PrintWriter pw, String msg, @NonNull Exception e) { + Log.e(LOG_TAG, msg, e); + if (pw != null) { + pw.println("ERROR: " + msg + " ::\n " + e); + pw.flush(); + } + } + } +} diff --git a/services/core/java/com/android/server/wm/PerfettoTransitionTracer.java b/services/core/java/com/android/server/wm/PerfettoTransitionTracer.java new file mode 100644 index 000000000000..eae9951d0679 --- /dev/null +++ b/services/core/java/com/android/server/wm/PerfettoTransitionTracer.java @@ -0,0 +1,188 @@ +/* + * 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.wm; + +import android.annotation.NonNull; +import android.internal.perfetto.protos.PerfettoTrace; +import android.os.SystemClock; +import android.tracing.perfetto.DataSourceParams; +import android.tracing.perfetto.InitArguments; +import android.tracing.perfetto.Producer; +import android.tracing.transition.TransitionDataSource; +import android.util.proto.ProtoOutputStream; + +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.concurrent.atomic.AtomicInteger; + +class PerfettoTransitionTracer implements TransitionTracer { + private final AtomicInteger mActiveTraces = new AtomicInteger(0); + private final TransitionDataSource mDataSource = + new TransitionDataSource(this.mActiveTraces::incrementAndGet, () -> {}, + this.mActiveTraces::decrementAndGet); + + PerfettoTransitionTracer() { + Producer.init(InitArguments.DEFAULTS); + mDataSource.register(DataSourceParams.DEFAULTS); + } + + /** + * Records key information about a transition that has been sent to Shell to be played. + * More information will be appended to the same proto object once the transition is finished or + * aborted. + * Transition information won't be added to the trace buffer until + * {@link #logFinishedTransition} or {@link #logAbortedTransition} is called for this + * transition. + * + * @param transition The transition that has been sent to Shell. + * @param targets Information about the target windows of the transition. + */ + @Override + public void logSentTransition(Transition transition, ArrayList<Transition.ChangeInfo> targets) { + if (!isTracing()) { + return; + } + + mDataSource.trace((ctx) -> { + final ProtoOutputStream os = ctx.newTracePacket(); + + final long token = os.start(PerfettoTrace.TracePacket.SHELL_TRANSITION); + + os.write(PerfettoTrace.ShellTransition.ID, transition.getSyncId()); + os.write(PerfettoTrace.ShellTransition.CREATE_TIME_NS, + transition.mLogger.mCreateTimeNs); + os.write(PerfettoTrace.ShellTransition.SEND_TIME_NS, transition.mLogger.mSendTimeNs); + os.write(PerfettoTrace.ShellTransition.START_TRANSACTION_ID, + transition.getStartTransaction().getId()); + os.write(PerfettoTrace.ShellTransition.FINISH_TRANSACTION_ID, + transition.getFinishTransaction().getId()); + os.write(PerfettoTrace.ShellTransition.TYPE, transition.mType); + os.write(PerfettoTrace.ShellTransition.FLAGS, transition.getFlags()); + + addTransitionTargetsToProto(os, targets); + + os.end(token); + }); + } + + /** + * Completes the information dumped in {@link #logSentTransition} for a transition + * that has finished or aborted, and add the proto object to the trace buffer. + * + * @param transition The transition that has finished. + */ + @Override + public void logFinishedTransition(Transition transition) { + if (!isTracing()) { + return; + } + + mDataSource.trace((ctx) -> { + final ProtoOutputStream os = ctx.newTracePacket(); + + final long token = os.start(PerfettoTrace.TracePacket.SHELL_TRANSITION); + os.write(PerfettoTrace.ShellTransition.ID, transition.getSyncId()); + os.write(PerfettoTrace.ShellTransition.FINISH_TIME_NS, + transition.mLogger.mFinishTimeNs); + os.end(token); + }); + } + + /** + * Same as {@link #logFinishedTransition} but don't add the transition to the trace buffer + * unless actively tracing. + * + * @param transition The transition that has been aborted + */ + @Override + public void logAbortedTransition(Transition transition) { + if (!isTracing()) { + return; + } + + mDataSource.trace((ctx) -> { + final ProtoOutputStream os = ctx.newTracePacket(); + + final long token = os.start(PerfettoTrace.TracePacket.SHELL_TRANSITION); + os.write(PerfettoTrace.ShellTransition.ID, transition.getSyncId()); + os.write(PerfettoTrace.ShellTransition.WM_ABORT_TIME_NS, + transition.mLogger.mAbortTimeNs); + os.end(token); + }); + } + + @Override + public void logRemovingStartingWindow(@NonNull StartingData startingData) { + if (!isTracing()) { + return; + } + + mDataSource.trace((ctx) -> { + final ProtoOutputStream os = ctx.newTracePacket(); + + final long token = os.start(PerfettoTrace.TracePacket.SHELL_TRANSITION); + os.write(PerfettoTrace.ShellTransition.ID, startingData.mTransitionId); + os.write(PerfettoTrace.ShellTransition.STARTING_WINDOW_REMOVE_TIME_NS, + SystemClock.elapsedRealtimeNanos()); + os.end(token); + }); + } + + @Override + public void startTrace(PrintWriter pw) { + // No-op + } + + @Override + public void stopTrace(PrintWriter pw) { + // No-op + } + + @Override + public void saveForBugreport(PrintWriter pw) { + // Nothing to do here. Handled by Perfetto. + } + + @Override + public boolean isTracing() { + return mActiveTraces.get() > 0; + } + + private void addTransitionTargetsToProto( + ProtoOutputStream os, + ArrayList<Transition.ChangeInfo> targets + ) { + for (int i = 0; i < targets.size(); ++i) { + final Transition.ChangeInfo target = targets.get(i); + + final int layerId; + if (target.mContainer.mSurfaceControl.isValid()) { + layerId = target.mContainer.mSurfaceControl.getLayerId(); + } else { + layerId = -1; + } + final int windowId = System.identityHashCode(target.mContainer); + + final long token = os.start(PerfettoTrace.ShellTransition.TARGETS); + os.write(PerfettoTrace.ShellTransition.Target.MODE, target.mReadyMode); + os.write(PerfettoTrace.ShellTransition.Target.FLAGS, target.mReadyFlags); + os.write(PerfettoTrace.ShellTransition.Target.LAYER_ID, layerId); + os.write(PerfettoTrace.ShellTransition.Target.WINDOW_ID, windowId); + os.end(token); + } + } +} diff --git a/services/core/java/com/android/server/wm/ScreenRecordingCallbackController.java b/services/core/java/com/android/server/wm/ScreenRecordingCallbackController.java new file mode 100644 index 000000000000..5f488b769885 --- /dev/null +++ b/services/core/java/com/android/server/wm/ScreenRecordingCallbackController.java @@ -0,0 +1,284 @@ +/* + * 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.wm; + +import static android.content.Context.MEDIA_PROJECTION_SERVICE; + +import static com.android.internal.protolog.ProtoLogGroup.WM_ERROR; + +import android.media.projection.IMediaProjectionManager; +import android.media.projection.IMediaProjectionWatcherCallback; +import android.media.projection.MediaProjectionInfo; +import android.os.Binder; +import android.os.IBinder; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.util.ArrayMap; +import android.util.ArraySet; +import android.view.ContentRecordingSession; +import android.window.IScreenRecordingCallback; + +import com.android.internal.annotations.GuardedBy; +import com.android.internal.protolog.common.ProtoLog; + +import java.io.PrintWriter; +import java.util.Map; +import java.util.Set; + +public class ScreenRecordingCallbackController { + + private final class Callback implements IBinder.DeathRecipient { + + IScreenRecordingCallback mCallback; + int mUid; + + Callback(IScreenRecordingCallback callback, int uid) { + this.mCallback = callback; + this.mUid = uid; + } + + public void binderDied() { + unregister(mCallback); + } + } + + @GuardedBy("WindowManagerService.mGlobalLock") + private final Map<IBinder, Callback> mCallbacks = new ArrayMap<>(); + + @GuardedBy("WindowManagerService.mGlobalLock") + private final Map<Integer /*UID*/, Boolean> mLastInvokedStateByUid = new ArrayMap<>(); + + private final WindowManagerService mWms; + + @GuardedBy("WindowManagerService.mGlobalLock") + private WindowContainer<WindowContainer> mRecordedWC; + + private boolean mWatcherCallbackRegistered = false; + + private final class MediaProjectionWatcherCallback extends + IMediaProjectionWatcherCallback.Stub { + @Override + public void onStart(MediaProjectionInfo mediaProjectionInfo) { + onScreenRecordingStart(mediaProjectionInfo); + } + + @Override + public void onStop(MediaProjectionInfo mediaProjectionInfo) { + onScreenRecordingStop(); + } + + @Override + public void onRecordingSessionSet(MediaProjectionInfo mediaProjectionInfo, + ContentRecordingSession contentRecordingSession) { + } + } + + ScreenRecordingCallbackController(WindowManagerService wms) { + mWms = wms; + } + + @GuardedBy("WindowManagerService.mGlobalLock") + private void setRecordedWindowContainer(MediaProjectionInfo mediaProjectionInfo) { + if (mediaProjectionInfo.getLaunchCookie() == null) { + mRecordedWC = (WindowContainer) mWms.mRoot.getDefaultDisplay(); + } else { + mRecordedWC = mWms.mRoot.getActivity(activity -> activity.mLaunchCookie + == mediaProjectionInfo.getLaunchCookie()).getTask(); + } + } + + @GuardedBy("WindowManagerService.mGlobalLock") + private void ensureMediaProjectionWatcherCallbackRegistered() { + if (mWatcherCallbackRegistered) { + return; + } + + IBinder binder = ServiceManager.getService(MEDIA_PROJECTION_SERVICE); + IMediaProjectionManager mediaProjectionManager = + IMediaProjectionManager.Stub.asInterface(binder); + + long identityToken = Binder.clearCallingIdentity(); + MediaProjectionInfo mediaProjectionInfo = null; + try { + mediaProjectionInfo = mediaProjectionManager.addCallback( + new MediaProjectionWatcherCallback()); + mWatcherCallbackRegistered = true; + } catch (RemoteException e) { + ProtoLog.e(WM_ERROR, "Failed to register MediaProjectionWatcherCallback"); + } finally { + Binder.restoreCallingIdentity(identityToken); + } + + if (mediaProjectionInfo != null) { + setRecordedWindowContainer(mediaProjectionInfo); + } + } + + boolean register(IScreenRecordingCallback callback) { + synchronized (mWms.mGlobalLock) { + ensureMediaProjectionWatcherCallbackRegistered(); + + IBinder binder = callback.asBinder(); + int uid = Binder.getCallingUid(); + + if (mCallbacks.containsKey(binder)) { + return mLastInvokedStateByUid.get(uid); + } + + Callback callbackInfo = new Callback(callback, uid); + try { + binder.linkToDeath(callbackInfo, 0); + } catch (RemoteException e) { + return false; + } + + boolean uidInRecording = uidHasRecordedActivity(callbackInfo.mUid); + mLastInvokedStateByUid.put(callbackInfo.mUid, uidInRecording); + mCallbacks.put(binder, callbackInfo); + return uidInRecording; + } + } + + void unregister(IScreenRecordingCallback callback) { + synchronized (mWms.mGlobalLock) { + IBinder binder = callback.asBinder(); + Callback callbackInfo = mCallbacks.remove(binder); + binder.unlinkToDeath(callbackInfo, 0); + + boolean uidHasCallback = false; + for (Callback cb : mCallbacks.values()) { + if (cb.mUid == callbackInfo.mUid) { + uidHasCallback = true; + break; + } + } + if (!uidHasCallback) { + mLastInvokedStateByUid.remove(callbackInfo.mUid); + } + } + } + + private void onScreenRecordingStart(MediaProjectionInfo mediaProjectionInfo) { + synchronized (mWms.mGlobalLock) { + setRecordedWindowContainer(mediaProjectionInfo); + dispatchCallbacks(getRecordedUids(), true /* visibleInScreenRecording*/); + } + } + + private void onScreenRecordingStop() { + synchronized (mWms.mGlobalLock) { + dispatchCallbacks(getRecordedUids(), false /*visibleInScreenRecording*/); + mRecordedWC = null; + } + } + + @GuardedBy("WindowManagerService.mGlobalLock") + void onProcessActivityVisibilityChanged(int uid, boolean processVisible) { + // If recording isn't active or there's no registered callback for the uid, there's nothing + // to do on this visibility change. + if (mRecordedWC == null || !mLastInvokedStateByUid.containsKey(uid)) { + return; + } + + // If the callbacks are already in the correct state, avoid making duplicate callbacks for + // the same state. This can happen when: + // * a process becomes visible but its UID already has a recorded activity from another + // process. + // * a process becomes invisible but its UID already doesn't have any recorded activities. + if (processVisible == mLastInvokedStateByUid.get(uid)) { + return; + } + + // If the process visibility change doesn't change the visibility of the UID, avoid making + // duplicate callbacks for the same state. This can happen when: + // * a process becomes visible but the newly visible activity isn't in the recorded window + // container. + // * a process becomes invisible but there are still activities being recorded for the UID. + boolean uidInRecording = uidHasRecordedActivity(uid); + if ((processVisible && !uidInRecording) || (!processVisible && uidInRecording)) { + return; + } + + dispatchCallbacks(Set.of(uid), processVisible); + } + + @GuardedBy("WindowManagerService.mGlobalLock") + private boolean uidHasRecordedActivity(int uid) { + if (mRecordedWC == null) { + return false; + } + boolean[] hasRecordedActivity = {false}; + mRecordedWC.forAllActivities(activityRecord -> { + if (activityRecord.getUid() == uid && activityRecord.isVisibleRequested()) { + hasRecordedActivity[0] = true; + return true; + } + return false; + }, true /*traverseTopToBottom*/); + return hasRecordedActivity[0]; + } + + @GuardedBy("WindowManagerService.mGlobalLock") + private Set<Integer> getRecordedUids() { + Set<Integer> result = new ArraySet<>(); + if (mRecordedWC == null) { + return result; + } + mRecordedWC.forAllActivities(activityRecord -> { + if (activityRecord.isVisibleRequested() && mLastInvokedStateByUid.containsKey( + activityRecord.getUid())) { + result.add(activityRecord.getUid()); + } + }, true /*traverseTopToBottom*/); + return result; + } + + @GuardedBy("WindowManagerService.mGlobalLock") + private void dispatchCallbacks(Set<Integer> uids, boolean visibleInScreenRecording) { + if (uids.isEmpty()) { + return; + } + + for (Integer uid : uids) { + mLastInvokedStateByUid.put(uid, visibleInScreenRecording); + } + + for (Callback callback : mCallbacks.values()) { + if (!uids.contains(callback.mUid)) { + continue; + } + try { + callback.mCallback.onScreenRecordingStateChanged(visibleInScreenRecording); + } catch (RemoteException e) { + // Client has died. Cleanup is handled via DeathRecipient. + } + } + } + + void dump(PrintWriter pw) { + pw.format("ScreenRecordingCallbackController:\n"); + pw.format(" Registered callbacks:\n"); + for (Map.Entry<IBinder, Callback> entry : mCallbacks.entrySet()) { + pw.format(" callback=%s uid=%s\n", entry.getKey(), entry.getValue().mUid); + } + pw.format(" Last invoked states:\n"); + for (Map.Entry<Integer, Boolean> entry : mLastInvokedStateByUid.entrySet()) { + pw.format(" uid=%s isVisibleInScreenRecording=%s\n", entry.getKey(), + entry.getValue()); + } + } +} diff --git a/services/core/java/com/android/server/wm/SensitiveContentPackages.java b/services/core/java/com/android/server/wm/SensitiveContentPackages.java index 3862b82512c3..a7d6903bbe30 100644 --- a/services/core/java/com/android/server/wm/SensitiveContentPackages.java +++ b/services/core/java/com/android/server/wm/SensitiveContentPackages.java @@ -21,7 +21,6 @@ import android.util.ArraySet; import java.io.PrintWriter; import java.util.Objects; -import java.util.Set; /** * Cache of distinct package/uid pairs that require being blocked from screen capture. This class is @@ -41,10 +40,33 @@ public class SensitiveContentPackages { return false; } - /** Replaces the set of package/uid pairs to set that should be blocked from screen capture */ - public void setShouldBlockScreenCaptureForApp(@NonNull Set<PackageInfo> packageInfos) { - mProtectedPackages.clear(); + /** + * Adds the set of package/uid pairs to set that should be blocked from screen capture + * + * @param packageInfos packages to be blocked + * @return {@code true} if packages set is modified, {@code false} otherwise. + */ + public boolean addBlockScreenCaptureForApps(@NonNull ArraySet<PackageInfo> packageInfos) { + if (mProtectedPackages.equals(packageInfos)) { + // new set is equal to current set of packages, no need to update + return false; + } mProtectedPackages.addAll(packageInfos); + return true; + } + + /** + * Clears the set of package/uid pairs that should be blocked from screen capture + * + * @return {@code true} if packages set is modified, {@code false} otherwise. + */ + public boolean clearBlockedApps() { + if (mProtectedPackages.isEmpty()) { + // set was already empty + return false; + } + mProtectedPackages.clear(); + return true; } void dump(PrintWriter pw) { diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java index a7a6bf2ed2a1..8f9ed8353456 100644 --- a/services/core/java/com/android/server/wm/Task.java +++ b/services/core/java/com/android/server/wm/Task.java @@ -208,6 +208,7 @@ import java.io.PrintWriter; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; +import java.util.List; import java.util.Objects; import java.util.function.Consumer; import java.util.function.Predicate; @@ -1702,6 +1703,8 @@ class Task extends TaskFragment { final ActivityRecord r = findActivityInHistory(newR.mActivityComponent, newR.mUserId); if (r == null) return null; + moveTaskFragmentsToBottomIfNeeded(r, finishCount); + final PooledPredicate f = PooledLambda.obtainPredicate( (ActivityRecord ar, ActivityRecord boundaryActivity) -> finishActivityAbove(ar, boundaryActivity, finishCount), @@ -1722,6 +1725,50 @@ class Task extends TaskFragment { return r; } + /** + * Moves {@link TaskFragment}s to the bottom if the flag + * {@link TaskFragment#isMoveToBottomIfClearWhenLaunch} is {@code true}. + */ + @VisibleForTesting + void moveTaskFragmentsToBottomIfNeeded(@NonNull ActivityRecord r, @NonNull int[] finishCount) { + final int activityIndex = mChildren.indexOf(r); + if (activityIndex < 0) { + return; + } + + List<TaskFragment> taskFragmentsToMove = null; + + // Find the TaskFragments that need to be moved + for (int i = mChildren.size() - 1; i > activityIndex; i--) { + final TaskFragment taskFragment = mChildren.get(i).asTaskFragment(); + if (taskFragment != null && taskFragment.isMoveToBottomIfClearWhenLaunch()) { + if (taskFragmentsToMove == null) { + taskFragmentsToMove = new ArrayList<>(); + } + taskFragmentsToMove.add(taskFragment); + } + } + if (taskFragmentsToMove == null) { + return; + } + + // Move the TaskFragments to the bottom of the Task. Their relative orders are preserved. + final int size = taskFragmentsToMove.size(); + for (int i = 0; i < size; i++) { + final TaskFragment taskFragment = taskFragmentsToMove.get(i); + + // The visibility of the TaskFragment may change. Collect it in the transition so that + // transition animation can be properly played. + mTransitionController.collect(taskFragment); + + positionChildAt(POSITION_BOTTOM, taskFragment, false /* includeParents */); + } + + // Treat it as if the TaskFragments are finished so that a transition animation can be + // played to send the TaskFragments back and bring the activity to front. + finishCount[0] += size; + } + private static boolean finishActivityAbove(ActivityRecord r, ActivityRecord boundaryActivity, @NonNull int[] finishCount) { // Stop operation once we reach the boundary activity. @@ -3511,9 +3558,11 @@ class Task extends TaskFragment { && top.mLetterboxUiController.isSystemOverrideToFullscreenEnabled(); appCompatTaskInfo.isFromLetterboxDoubleTap = top != null && top.mLetterboxUiController.isFromDoubleTap(); - if (appCompatTaskInfo.isLetterboxDoubleTapEnabled) { + if (top != null) { appCompatTaskInfo.topActivityLetterboxWidth = top.getBounds().width(); appCompatTaskInfo.topActivityLetterboxHeight = top.getBounds().height(); + } + if (appCompatTaskInfo.isLetterboxDoubleTapEnabled) { if (appCompatTaskInfo.isTopActivityPillarboxed()) { // Pillarboxed appCompatTaskInfo.topActivityLetterboxHorizontalPosition = diff --git a/services/core/java/com/android/server/wm/TaskFragment.java b/services/core/java/com/android/server/wm/TaskFragment.java index f56759f9481c..7d418eae6548 100644 --- a/services/core/java/com/android/server/wm/TaskFragment.java +++ b/services/core/java/com/android/server/wm/TaskFragment.java @@ -363,6 +363,12 @@ class TaskFragment extends WindowContainer<WindowContainer> { */ private boolean mIsolatedNav; + /** + * Whether the TaskFragment should move to bottom of task when any activity below it is + * launched in clear top mode. + */ + private boolean mMoveToBottomIfClearWhenLaunch; + /** 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; @@ -3045,6 +3051,14 @@ class TaskFragment extends WindowContainer<WindowContainer> { mEmbeddedDimArea = embeddedDimArea; } + void setMoveToBottomIfClearWhenLaunch(boolean moveToBottomIfClearWhenLaunch) { + mMoveToBottomIfClearWhenLaunch = moveToBottomIfClearWhenLaunch; + } + + boolean isMoveToBottomIfClearWhenLaunch() { + return mMoveToBottomIfClearWhenLaunch; + } + @VisibleForTesting boolean isDimmingOnParentTask() { return mEmbeddedDimArea == EMBEDDED_DIM_AREA_PARENT_TASK; diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java index f620a9743eb4..2accf9a2a43a 100644 --- a/services/core/java/com/android/server/wm/Transition.java +++ b/services/core/java/com/android/server/wm/Transition.java @@ -2841,6 +2841,19 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { } } + /** Returns {@code true} if the display should use high performance hint for this transition. */ + boolean shouldUsePerfHint(@NonNull DisplayContent dc) { + if (mOverrideOptions != null + && mOverrideOptions.getType() == ActivityOptions.ANIM_SCENE_TRANSITION + && mType == TRANSIT_TO_BACK && mParticipants.size() == 1) { + // This should be from convertFromTranslucent that makes the occluded activity invisible + // without animation. So do not use perf hint (especially early-wakeup) that may disturb + // SurfaceFlinger scheduling around the last frame. + return false; + } + return mTargetDisplays.contains(dc); + } + /** * Returns {@code true} if the transition and the corresponding transaction should be applied * on display thread. Currently, this only checks for display rotation change because the order diff --git a/services/core/java/com/android/server/wm/TransitionController.java b/services/core/java/com/android/server/wm/TransitionController.java index 708d63e27ec2..59e3350d5c13 100644 --- a/services/core/java/com/android/server/wm/TransitionController.java +++ b/services/core/java/com/android/server/wm/TransitionController.java @@ -1237,8 +1237,15 @@ class TransitionController { // enableHighPerfTransition(true) is also called in Transition#recordDisplay. for (int i = mAtm.mRootWindowContainer.getChildCount() - 1; i >= 0; i--) { final DisplayContent dc = mAtm.mRootWindowContainer.getChildAt(i); - if (isTransitionOnDisplay(dc)) { + if (mCollectingTransition != null && mCollectingTransition.shouldUsePerfHint(dc)) { dc.enableHighPerfTransition(true); + continue; + } + for (int j = mPlayingTransitions.size() - 1; j >= 0; j--) { + if (mPlayingTransitions.get(j).shouldUsePerfHint(dc)) { + dc.enableHighPerfTransition(true); + break; + } } } // Usually transitions put quite a load onto the system already (with all the things diff --git a/services/core/java/com/android/server/wm/TransitionTracer.java b/services/core/java/com/android/server/wm/TransitionTracer.java index c59d2d392e93..0f3fe229b864 100644 --- a/services/core/java/com/android/server/wm/TransitionTracer.java +++ b/services/core/java/com/android/server/wm/TransitionTracer.java @@ -1,323 +1,19 @@ -/* - * Copyright (C) 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - package com.android.server.wm; -import static android.os.Build.IS_USER; - -import static com.android.server.wm.shell.TransitionTraceProto.MAGIC_NUMBER; -import static com.android.server.wm.shell.TransitionTraceProto.MAGIC_NUMBER_H; -import static com.android.server.wm.shell.TransitionTraceProto.MAGIC_NUMBER_L; -import static com.android.server.wm.shell.TransitionTraceProto.REAL_TO_ELAPSED_TIME_OFFSET_NANOS; - import android.annotation.NonNull; import android.annotation.Nullable; -import android.os.SystemClock; -import android.os.Trace; -import android.util.Log; -import android.util.proto.ProtoOutputStream; -import com.android.internal.util.TraceBuffer; -import com.android.server.wm.Transition.ChangeInfo; - -import java.io.File; -import java.io.IOException; import java.io.PrintWriter; import java.util.ArrayList; -import java.util.concurrent.TimeUnit; - -/** - * Helper class to collect and dump transition traces. - */ -public class TransitionTracer { - - private static final String LOG_TAG = "TransitionTracer"; - - private static final int ALWAYS_ON_TRACING_CAPACITY = 15 * 1024; // 15 KB - private static final int ACTIVE_TRACING_BUFFER_CAPACITY = 5000 * 1024; // 5 MB - - // This will be the size the proto output streams are initialized to. - // Ideally this should fit most or all the proto objects we will create and be no bigger than - // that to ensure to don't use excessive amounts of memory. - private static final int CHUNK_SIZE = 64; - - static final String WINSCOPE_EXT = ".winscope"; - private static final String TRACE_FILE = - "/data/misc/wmtrace/wm_transition_trace" + WINSCOPE_EXT; - private static final long MAGIC_NUMBER_VALUE = ((long) MAGIC_NUMBER_H << 32) | MAGIC_NUMBER_L; - - private final TraceBuffer mTraceBuffer = new TraceBuffer(ALWAYS_ON_TRACING_CAPACITY); - - private final Object mEnabledLock = new Object(); - private volatile boolean mActiveTracingEnabled = false; - - /** - * Records key information about a transition that has been sent to Shell to be played. - * More information will be appended to the same proto object once the transition is finished or - * aborted. - * Transition information won't be added to the trace buffer until - * {@link #logFinishedTransition} or {@link #logAbortedTransition} is called for this - * transition. - * - * @param transition The transition that has been sent to Shell. - * @param targets Information about the target windows of the transition. - */ - public void logSentTransition(Transition transition, ArrayList<ChangeInfo> targets) { - try { - final ProtoOutputStream outputStream = new ProtoOutputStream(CHUNK_SIZE); - final long protoToken = outputStream - .start(com.android.server.wm.shell.TransitionTraceProto.TRANSITIONS); - outputStream.write(com.android.server.wm.shell.Transition.ID, transition.getSyncId()); - outputStream.write(com.android.server.wm.shell.Transition.CREATE_TIME_NS, - transition.mLogger.mCreateTimeNs); - outputStream.write(com.android.server.wm.shell.Transition.SEND_TIME_NS, - transition.mLogger.mSendTimeNs); - outputStream.write(com.android.server.wm.shell.Transition.START_TRANSACTION_ID, - transition.getStartTransaction().getId()); - outputStream.write(com.android.server.wm.shell.Transition.FINISH_TRANSACTION_ID, - transition.getFinishTransaction().getId()); - dumpTransitionTargetsToProto(outputStream, transition, targets); - outputStream.end(protoToken); - - mTraceBuffer.add(outputStream); - } catch (Exception e) { - // Don't let any errors in the tracing cause the transition to fail - Log.e(LOG_TAG, "Unexpected exception thrown while logging transitions", e); - } - } - - /** - * Completes the information dumped in {@link #logSentTransition} for a transition - * that has finished or aborted, and add the proto object to the trace buffer. - * - * @param transition The transition that has finished. - */ - public void logFinishedTransition(Transition transition) { - try { - final ProtoOutputStream outputStream = new ProtoOutputStream(CHUNK_SIZE); - final long protoToken = outputStream - .start(com.android.server.wm.shell.TransitionTraceProto.TRANSITIONS); - outputStream.write(com.android.server.wm.shell.Transition.ID, transition.getSyncId()); - outputStream.write(com.android.server.wm.shell.Transition.FINISH_TIME_NS, - transition.mLogger.mFinishTimeNs); - outputStream.end(protoToken); - - mTraceBuffer.add(outputStream); - } catch (Exception e) { - // Don't let any errors in the tracing cause the transition to fail - Log.e(LOG_TAG, "Unexpected exception thrown while logging transitions", e); - } - } - - /** - * Same as {@link #logFinishedTransition} but don't add the transition to the trace buffer - * unless actively tracing. - * - * @param transition The transition that has been aborted - */ - public void logAbortedTransition(Transition transition) { - try { - final ProtoOutputStream outputStream = new ProtoOutputStream(CHUNK_SIZE); - final long protoToken = outputStream - .start(com.android.server.wm.shell.TransitionTraceProto.TRANSITIONS); - outputStream.write(com.android.server.wm.shell.Transition.ID, transition.getSyncId()); - outputStream.write(com.android.server.wm.shell.Transition.ABORT_TIME_NS, - transition.mLogger.mAbortTimeNs); - outputStream.end(protoToken); - - mTraceBuffer.add(outputStream); - } catch (Exception e) { - // Don't let any errors in the tracing cause the transition to fail - Log.e(LOG_TAG, "Unexpected exception thrown while logging transitions", e); - } - } - - void logRemovingStartingWindow(@NonNull StartingData startingData) { - if (startingData.mTransitionId == 0) { - return; - } - try { - final ProtoOutputStream outputStream = new ProtoOutputStream(CHUNK_SIZE); - final long protoToken = outputStream - .start(com.android.server.wm.shell.TransitionTraceProto.TRANSITIONS); - outputStream.write(com.android.server.wm.shell.Transition.ID, - startingData.mTransitionId); - outputStream.write( - com.android.server.wm.shell.Transition.STARTING_WINDOW_REMOVE_TIME_NS, - SystemClock.elapsedRealtimeNanos()); - outputStream.end(protoToken); - - mTraceBuffer.add(outputStream); - } catch (Exception e) { - Log.e(LOG_TAG, "Unexpected exception thrown while logging transitions", e); - } - } - - private void dumpTransitionTargetsToProto(ProtoOutputStream outputStream, - Transition transition, ArrayList<ChangeInfo> targets) { - Trace.beginSection("TransitionTracer#dumpTransitionTargetsToProto"); - if (mActiveTracingEnabled) { - outputStream.write(com.android.server.wm.shell.Transition.ID, - transition.getSyncId()); - } - - outputStream.write(com.android.server.wm.shell.Transition.TYPE, transition.mType); - outputStream.write(com.android.server.wm.shell.Transition.FLAGS, transition.getFlags()); - - for (int i = 0; i < targets.size(); ++i) { - final long changeToken = outputStream - .start(com.android.server.wm.shell.Transition.TARGETS); - - final Transition.ChangeInfo target = targets.get(i); - - final int layerId; - if (target.mContainer.mSurfaceControl.isValid()) { - layerId = target.mContainer.mSurfaceControl.getLayerId(); - } else { - layerId = -1; - } - - outputStream.write(com.android.server.wm.shell.Target.MODE, target.mReadyMode); - outputStream.write(com.android.server.wm.shell.Target.FLAGS, target.mReadyFlags); - outputStream.write(com.android.server.wm.shell.Target.LAYER_ID, layerId); - - if (mActiveTracingEnabled) { - // What we use in the WM trace - final int windowId = System.identityHashCode(target.mContainer); - outputStream.write(com.android.server.wm.shell.Target.WINDOW_ID, windowId); - } - - outputStream.end(changeToken); - } - - Trace.endSection(); - } - - /** - * Starts collecting transitions for the trace. - * If called while a trace is already running, this will reset the trace. - */ - public void startTrace(@Nullable PrintWriter pw) { - if (IS_USER) { - LogAndPrintln.e(pw, "Tracing is not supported on user builds."); - return; - } - Trace.beginSection("TransitionTracer#startTrace"); - LogAndPrintln.i(pw, "Starting shell transition trace."); - synchronized (mEnabledLock) { - mActiveTracingEnabled = true; - mTraceBuffer.resetBuffer(); - mTraceBuffer.setCapacity(ACTIVE_TRACING_BUFFER_CAPACITY); - } - Trace.endSection(); - } - - /** - * Stops collecting the transition trace and dump to trace to file. - * - * Dumps the trace to @link{TRACE_FILE}. - */ - public void stopTrace(@Nullable PrintWriter pw) { - stopTrace(pw, new File(TRACE_FILE)); - } - - /** - * Stops collecting the transition trace and dump to trace to file. - * @param outputFile The file to dump the transition trace to. - */ - public void stopTrace(@Nullable PrintWriter pw, File outputFile) { - if (IS_USER) { - LogAndPrintln.e(pw, "Tracing is not supported on user builds."); - return; - } - Trace.beginSection("TransitionTracer#stopTrace"); - LogAndPrintln.i(pw, "Stopping shell transition trace."); - synchronized (mEnabledLock) { - mActiveTracingEnabled = false; - writeTraceToFileLocked(pw, outputFile); - mTraceBuffer.resetBuffer(); - mTraceBuffer.setCapacity(ALWAYS_ON_TRACING_CAPACITY); - } - Trace.endSection(); - } - - /** - * Being called while taking a bugreport so that tracing files can be included in the bugreport. - * - * @param pw Print writer - */ - public void saveForBugreport(@Nullable PrintWriter pw) { - if (IS_USER) { - LogAndPrintln.e(pw, "Tracing is not supported on user builds."); - return; - } - Trace.beginSection("TransitionTracer#saveForBugreport"); - synchronized (mEnabledLock) { - final File outputFile = new File(TRACE_FILE); - writeTraceToFileLocked(pw, outputFile); - } - Trace.endSection(); - } - - boolean isActiveTracingEnabled() { - return mActiveTracingEnabled; - } - - private void writeTraceToFileLocked(@Nullable PrintWriter pw, File file) { - Trace.beginSection("TransitionTracer#writeTraceToFileLocked"); - try { - ProtoOutputStream proto = new ProtoOutputStream(CHUNK_SIZE); - proto.write(MAGIC_NUMBER, MAGIC_NUMBER_VALUE); - long timeOffsetNs = - TimeUnit.MILLISECONDS.toNanos(System.currentTimeMillis()) - - SystemClock.elapsedRealtimeNanos(); - proto.write(REAL_TO_ELAPSED_TIME_OFFSET_NANOS, timeOffsetNs); - int pid = android.os.Process.myPid(); - LogAndPrintln.i(pw, "Writing file to " + file.getAbsolutePath() - + " from process " + pid); - mTraceBuffer.writeTraceToFile(file, proto); - } catch (IOException e) { - LogAndPrintln.e(pw, "Unable to write buffer to file", e); - } - Trace.endSection(); - } - - private static class LogAndPrintln { - private static void i(@Nullable PrintWriter pw, String msg) { - Log.i(LOG_TAG, msg); - if (pw != null) { - pw.println(msg); - pw.flush(); - } - } - private static void e(@Nullable PrintWriter pw, String msg) { - Log.e(LOG_TAG, msg); - if (pw != null) { - pw.println("ERROR: " + msg); - pw.flush(); - } - } +interface TransitionTracer { + void logSentTransition(Transition transition, ArrayList<Transition.ChangeInfo> targets); + void logFinishedTransition(Transition transition); + void logAbortedTransition(Transition transition); + void logRemovingStartingWindow(@NonNull StartingData startingData); - private static void e(@Nullable PrintWriter pw, String msg, @NonNull Exception e) { - Log.e(LOG_TAG, msg, e); - if (pw != null) { - pw.println("ERROR: " + msg + " ::\n " + e); - pw.flush(); - } - } - } + void startTrace(@Nullable PrintWriter pw); + void stopTrace(@Nullable PrintWriter pw); + boolean isTracing(); + void saveForBugreport(@Nullable PrintWriter pw); } diff --git a/services/core/java/com/android/server/wm/WindowManagerInternal.java b/services/core/java/com/android/server/wm/WindowManagerInternal.java index 22b690e85c35..f2a58e54bfbe 100644 --- a/services/core/java/com/android/server/wm/WindowManagerInternal.java +++ b/services/core/java/com/android/server/wm/WindowManagerInternal.java @@ -32,6 +32,7 @@ import android.hardware.display.VirtualDisplayConfig; import android.os.Bundle; import android.os.IBinder; import android.os.Message; +import android.util.ArraySet; import android.util.Pair; import android.view.ContentRecordingSession; import android.view.Display; @@ -1020,5 +1021,13 @@ public abstract class WindowManagerInternal { * * @param packageInfos set of {@link PackageInfo} whose windows should be blocked from capture */ - public abstract void setShouldBlockScreenCaptureForApp(@NonNull Set<PackageInfo> packageInfos); + public abstract void addBlockScreenCaptureForApps(@NonNull ArraySet<PackageInfo> packageInfos); + + /** + * Clears apps added to collection of apps in which screen capture should be disabled. + * + * <p> This clears and resets any existing set or added applications from + * * {@link #addBlockScreenCaptureForApps(ArraySet)} + */ + public abstract void clearBlockedApps(); } diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java index 95448352736f..6c833565119f 100644 --- a/services/core/java/com/android/server/wm/WindowManagerService.java +++ b/services/core/java/com/android/server/wm/WindowManagerService.java @@ -303,6 +303,7 @@ import android.view.displayhash.VerifiedDisplayHash; import android.view.inputmethod.ImeTracker; import android.window.AddToSurfaceSyncGroupResult; import android.window.ClientWindowFrames; +import android.window.IScreenRecordingCallback; import android.window.ISurfaceSyncGroupCompletedListener; import android.window.ITaskFpsCallback; import android.window.ITrustedPresentationListener; @@ -369,7 +370,6 @@ import java.util.Map; import java.util.NoSuchElementException; import java.util.Objects; import java.util.Optional; -import java.util.Set; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.function.Function; @@ -1104,6 +1104,8 @@ public class WindowManagerService extends IWindowManager.Stub void onAppFreezeTimeout(); } + private final ScreenRecordingCallbackController mScreenRecordingCallbackController; + public static WindowManagerService main(final Context context, final InputManagerService im, final boolean showBootMsgs, WindowManagerPolicy policy, ActivityTaskManagerService atm) { @@ -1213,7 +1215,12 @@ public class WindowManagerService extends IWindowManager.Stub mWindowTracing = WindowTracing.createDefaultAndStartLooper(this, Choreographer.getInstance()); - mTransitionTracer = new TransitionTracer(); + + if (android.tracing.Flags.perfettoTransitionTracing()) { + mTransitionTracer = new PerfettoTransitionTracer(); + } else { + mTransitionTracer = new LegacyTransitionTracer(); + } LocalServices.addService(WindowManagerPolicy.class, mPolicy); @@ -1340,6 +1347,7 @@ public class WindowManagerService extends IWindowManager.Stub mBlurController = new BlurController(mContext, mPowerManager); mTaskFpsCallbackController = new TaskFpsCallbackController(mContext); mAccessibilityController = new AccessibilityController(this); + mScreenRecordingCallbackController = new ScreenRecordingCallbackController(this); mSystemPerformanceHinter = new SystemPerformanceHinter(mContext, displayId -> { synchronized (mGlobalLock) { DisplayContent dc = mRoot.getDisplayContent(displayId); @@ -6087,7 +6095,7 @@ public class WindowManagerService extends IWindowManager.Stub @Override public boolean isTransitionTraceEnabled() { - return mTransitionTracer.isActiveTracingEnabled(); + return mTransitionTracer.isTracing(); } @Override @@ -7183,6 +7191,7 @@ public class WindowManagerService extends IWindowManager.Stub mSystemPerformanceHinter.dump(pw, ""); mTrustedPresentationListenerController.dump(pw); mSensitiveContentPackages.dump(pw); + mScreenRecordingCallbackController.dump(pw); } } @@ -8566,10 +8575,23 @@ public class WindowManagerService extends IWindowManager.Stub } @Override - public void setShouldBlockScreenCaptureForApp(Set<PackageInfo> packageInfos) { + public void addBlockScreenCaptureForApps(ArraySet<PackageInfo> packageInfos) { synchronized (mGlobalLock) { - mSensitiveContentPackages.setShouldBlockScreenCaptureForApp(packageInfos); - WindowManagerService.this.refreshScreenCaptureDisabled(); + boolean modified = + mSensitiveContentPackages.addBlockScreenCaptureForApps(packageInfos); + if (modified) { + WindowManagerService.this.refreshScreenCaptureDisabled(); + } + } + } + + @Override + public void clearBlockedApps() { + synchronized (mGlobalLock) { + boolean modified = mSensitiveContentPackages.clearBlockedApps(); + if (modified) { + WindowManagerService.this.refreshScreenCaptureDisabled(); + } } } } @@ -9884,4 +9906,18 @@ public class WindowManagerService extends IWindowManager.Stub int id) { mTrustedPresentationListenerController.unregisterListener(listener, id); } + + @Override + public boolean registerScreenRecordingCallback(IScreenRecordingCallback callback) { + return mScreenRecordingCallbackController.register(callback); + } + + @Override + public void unregisterScreenRecordingCallback(IScreenRecordingCallback callback) { + mScreenRecordingCallbackController.unregister(callback); + } + + void onProcessActivityVisibilityChanged(int uid, boolean visible) { + mScreenRecordingCallbackController.onProcessActivityVisibilityChanged(uid, visible); + } } diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java index 0da0bb45eb9b..205ed977f316 100644 --- a/services/core/java/com/android/server/wm/WindowOrganizerController.java +++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java @@ -36,6 +36,7 @@ import static android.window.TaskFragmentOperation.OP_TYPE_SET_ANIMATION_PARAMS; import static android.window.TaskFragmentOperation.OP_TYPE_SET_COMPANION_TASK_FRAGMENT; import static android.window.TaskFragmentOperation.OP_TYPE_SET_DIM_ON_TASK; import static android.window.TaskFragmentOperation.OP_TYPE_SET_ISOLATED_NAVIGATION; +import static android.window.TaskFragmentOperation.OP_TYPE_SET_MOVE_TO_BOTTOM_IF_CLEAR_WHEN_LAUNCH; import static android.window.TaskFragmentOperation.OP_TYPE_SET_RELATIVE_BOUNDS; import static android.window.TaskFragmentOperation.OP_TYPE_START_ACTIVITY_IN_TASK_FRAGMENT; import static android.window.TaskFragmentOperation.OP_TYPE_UNKNOWN; @@ -1514,6 +1515,11 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub : EMBEDDED_DIM_AREA_TASK_FRAGMENT); break; } + case OP_TYPE_SET_MOVE_TO_BOTTOM_IF_CLEAR_WHEN_LAUNCH: { + taskFragment.setMoveToBottomIfClearWhenLaunch( + operation.isMoveToBottomIfClearWhenLaunch()); + break; + } } return effects; } @@ -1566,6 +1572,17 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub return false; } + if ((opType == OP_TYPE_SET_MOVE_TO_BOTTOM_IF_CLEAR_WHEN_LAUNCH) + && !mTaskFragmentOrganizerController.isSystemOrganizer(organizer.asBinder())) { + final Throwable exception = new SecurityException( + "Only a system organizer can perform " + + "OP_TYPE_SET_MOVE_TO_BOTTOM_IF_CLEAR_WHEN_LAUNCH." + ); + sendTaskFragmentOperationFailure(organizer, errorCallbackToken, taskFragment, + opType, exception); + return false; + } + final IBinder secondaryFragmentToken = operation.getSecondaryFragmentToken(); return secondaryFragmentToken == null || validateTaskFragment(mLaunchTaskFragments.get(secondaryFragmentToken), opType, diff --git a/services/core/java/com/android/server/wm/WindowProcessController.java b/services/core/java/com/android/server/wm/WindowProcessController.java index b8fa5e5b2786..6d2e8cc29506 100644 --- a/services/core/java/com/android/server/wm/WindowProcessController.java +++ b/services/core/java/com/android/server/wm/WindowProcessController.java @@ -1271,8 +1271,10 @@ public class WindowProcessController extends ConfigurationContainer<Configuratio & (ACTIVITY_STATE_FLAG_IS_VISIBLE | ACTIVITY_STATE_FLAG_IS_WINDOW_VISIBLE)) != 0; if (!wasAnyVisible && anyVisible) { mAtm.mVisibleActivityProcessTracker.onAnyActivityVisible(this); + mAtm.mWindowManager.onProcessActivityVisibilityChanged(mUid, true /*visible*/); } else if (wasAnyVisible && !anyVisible) { mAtm.mVisibleActivityProcessTracker.onAllActivitiesInvisible(this); + mAtm.mWindowManager.onProcessActivityVisibilityChanged(mUid, false /*visible*/); } else if (wasAnyVisible && !wasResumed && hasResumedActivity()) { mAtm.mVisibleActivityProcessTracker.onActivityResumedWhileVisible(this); } diff --git a/services/core/jni/Android.bp b/services/core/jni/Android.bp index 775570cb08ba..dfa9dcecfbb5 100644 --- a/services/core/jni/Android.bp +++ b/services/core/jni/Android.bp @@ -37,7 +37,6 @@ cc_library_static { "com_android_server_adb_AdbDebuggingManager.cpp", "com_android_server_am_BatteryStatsService.cpp", "com_android_server_biometrics_SurfaceToNativeHandleConverter.cpp", - "com_android_server_BootReceiver.cpp", "com_android_server_ConsumerIrService.cpp", "com_android_server_companion_virtual_InputController.cpp", "com_android_server_devicepolicy_CryptoTestHelper.cpp", @@ -95,16 +94,6 @@ cc_library_static { header_libs: [ "bionic_libc_platform_headers", ], - - static_libs: [ - "libunwindstack", - ], - - whole_static_libs: [ - "libdebuggerd_tombstone_proto_to_text", - ], - - runtime_libs: ["libdexfile"], } cc_defaults { diff --git a/services/core/jni/OWNERS b/services/core/jni/OWNERS index 15eb7c64a40c..cc08488742b2 100644 --- a/services/core/jni/OWNERS +++ b/services/core/jni/OWNERS @@ -33,7 +33,3 @@ per-file com_android_server_companion_virtual_InputController.cpp = file:/servic # Bug component : 158088 = per-file *AnrTimer* per-file *AnrTimer* = file:/PERFORMANCE_OWNERS - -# Bug component : 158088 = per-file com_android_server_utils_AnrTimer*.java -per-file com_android_server_utils_AnrTimer*.java = file:/PERFORMANCE_OWNERS -per-file com_android_server_BootReceiver.cpp = file:/STABILITY_OWNERS diff --git a/services/core/jni/com_android_server_BootReceiver.cpp b/services/core/jni/com_android_server_BootReceiver.cpp deleted file mode 100644 index 3892d284dafb..000000000000 --- a/services/core/jni/com_android_server_BootReceiver.cpp +++ /dev/null @@ -1,57 +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. - */ - -#include <libdebuggerd/tombstone.h> -#include <nativehelper/JNIHelp.h> - -#include <sstream> - -#include "jni.h" -#include "tombstone.pb.h" - -namespace android { - -static void writeToString(std::stringstream& ss, const std::string& line, bool should_log) { - ss << line << std::endl; -} - -static jstring com_android_server_BootReceiver_getTombstoneText(JNIEnv* env, jobject, - jbyteArray tombstoneBytes) { - Tombstone tombstone; - tombstone.ParseFromArray(env->GetByteArrayElements(tombstoneBytes, 0), - env->GetArrayLength(tombstoneBytes)); - - std::stringstream tombstoneString; - - tombstone_proto_to_text(tombstone, - std::bind(&writeToString, std::ref(tombstoneString), - std::placeholders::_1, std::placeholders::_2)); - - return env->NewStringUTF(tombstoneString.str().c_str()); -} - -static const JNINativeMethod sMethods[] = { - /* name, signature, funcPtr */ - {"getTombstoneText", "([B)Ljava/lang/String;", - (jstring*)com_android_server_BootReceiver_getTombstoneText}, -}; - -int register_com_android_server_BootReceiver(JNIEnv* env) { - return jniRegisterNativeMethods(env, "com/android/server/BootReceiver", sMethods, - NELEM(sMethods)); -} - -} // namespace android diff --git a/services/core/jni/com_android_server_companion_virtual_InputController.cpp b/services/core/jni/com_android_server_companion_virtual_InputController.cpp index 4cd018b0269e..50d48b7d30e7 100644 --- a/services/core/jni/com_android_server_companion_virtual_InputController.cpp +++ b/services/core/jni/com_android_server_companion_virtual_InputController.cpp @@ -44,6 +44,7 @@ enum class DeviceType { MOUSE, TOUCHSCREEN, DPAD, + STYLUS, }; static unique_fd invalidFd() { @@ -98,6 +99,24 @@ static unique_fd openUinput(const char* readableName, jint vendorId, jint produc ioctl(fd, UI_SET_ABSBIT, ABS_MT_TOUCH_MAJOR); ioctl(fd, UI_SET_ABSBIT, ABS_MT_PRESSURE); ioctl(fd, UI_SET_PROPBIT, INPUT_PROP_DIRECT); + break; + case DeviceType::STYLUS: + ioctl(fd, UI_SET_EVBIT, EV_ABS); + ioctl(fd, UI_SET_KEYBIT, BTN_TOUCH); + ioctl(fd, UI_SET_KEYBIT, BTN_STYLUS); + ioctl(fd, UI_SET_KEYBIT, BTN_STYLUS2); + ioctl(fd, UI_SET_KEYBIT, BTN_TOOL_PEN); + ioctl(fd, UI_SET_KEYBIT, BTN_TOOL_RUBBER); + ioctl(fd, UI_SET_ABSBIT, ABS_X); + ioctl(fd, UI_SET_ABSBIT, ABS_Y); + ioctl(fd, UI_SET_ABSBIT, ABS_TILT_X); + ioctl(fd, UI_SET_ABSBIT, ABS_TILT_Y); + ioctl(fd, UI_SET_ABSBIT, ABS_PRESSURE); + ioctl(fd, UI_SET_PROPBIT, INPUT_PROP_DIRECT); + break; + default: + ALOGE("Invalid input device type %d", static_cast<int32_t>(deviceType)); + return invalidFd(); } int version; @@ -158,6 +177,47 @@ static unique_fd openUinput(const char* readableName, jint vendorId, jint produc ALOGE("Error creating touchscreen uinput tracking ids: %s", strerror(errno)); return invalidFd(); } + } else if (deviceType == DeviceType::STYLUS) { + uinput_abs_setup xAbsSetup; + xAbsSetup.code = ABS_X; + xAbsSetup.absinfo.maximum = screenWidth - 1; + xAbsSetup.absinfo.minimum = 0; + if (ioctl(fd, UI_ABS_SETUP, &xAbsSetup) != 0) { + ALOGE("Error creating stylus uinput x axis: %s", strerror(errno)); + return invalidFd(); + } + uinput_abs_setup yAbsSetup; + yAbsSetup.code = ABS_Y; + yAbsSetup.absinfo.maximum = screenHeight - 1; + yAbsSetup.absinfo.minimum = 0; + if (ioctl(fd, UI_ABS_SETUP, &yAbsSetup) != 0) { + ALOGE("Error creating stylus uinput y axis: %s", strerror(errno)); + return invalidFd(); + } + uinput_abs_setup tiltXAbsSetup; + tiltXAbsSetup.code = ABS_TILT_X; + tiltXAbsSetup.absinfo.maximum = 90; + tiltXAbsSetup.absinfo.minimum = -90; + if (ioctl(fd, UI_ABS_SETUP, &tiltXAbsSetup) != 0) { + ALOGE("Error creating stylus uinput tilt x axis: %s", strerror(errno)); + return invalidFd(); + } + uinput_abs_setup tiltYAbsSetup; + tiltYAbsSetup.code = ABS_TILT_Y; + tiltYAbsSetup.absinfo.maximum = 90; + tiltYAbsSetup.absinfo.minimum = -90; + if (ioctl(fd, UI_ABS_SETUP, &tiltYAbsSetup) != 0) { + ALOGE("Error creating stylus uinput tilt y axis: %s", strerror(errno)); + return invalidFd(); + } + uinput_abs_setup pressureAbsSetup; + pressureAbsSetup.code = ABS_PRESSURE; + pressureAbsSetup.absinfo.maximum = 255; + pressureAbsSetup.absinfo.minimum = 0; + if (ioctl(fd, UI_ABS_SETUP, &pressureAbsSetup) != 0) { + ALOGE("Error creating touchscreen uinput pressure axis: %s", strerror(errno)); + return invalidFd(); + } } if (ioctl(fd, UI_DEV_SETUP, &setup) != 0) { ALOGE("Error creating uinput device: %s", strerror(errno)); @@ -182,6 +242,17 @@ static unique_fd openUinput(const char* readableName, jint vendorId, jint produc fallback.absmax[ABS_MT_TOUCH_MAJOR] = screenWidth - 1; fallback.absmin[ABS_MT_PRESSURE] = 0; fallback.absmax[ABS_MT_PRESSURE] = 255; + } else if (deviceType == DeviceType::STYLUS) { + fallback.absmin[ABS_X] = 0; + fallback.absmax[ABS_X] = screenWidth - 1; + fallback.absmin[ABS_Y] = 0; + fallback.absmax[ABS_Y] = screenHeight - 1; + fallback.absmin[ABS_TILT_X] = -90; + fallback.absmax[ABS_TILT_X] = 90; + fallback.absmin[ABS_TILT_Y] = -90; + fallback.absmax[ABS_TILT_Y] = 90; + fallback.absmin[ABS_PRESSURE] = 0; + fallback.absmax[ABS_PRESSURE] = 255; } if (TEMP_FAILURE_RETRY(write(fd, &fallback, sizeof(fallback))) != sizeof(fallback)) { ALOGE("Error creating uinput device: %s", strerror(errno)); @@ -234,6 +305,13 @@ static jlong nativeOpenUinputTouchscreen(JNIEnv* env, jobject thiz, jstring name return fd.ok() ? reinterpret_cast<jlong>(new VirtualTouchscreen(std::move(fd))) : INVALID_PTR; } +static jlong nativeOpenUinputStylus(JNIEnv* env, jobject thiz, jstring name, jint vendorId, + jint productId, jstring phys, jint height, jint width) { + auto fd = + openUinputJni(env, name, vendorId, productId, phys, DeviceType::STYLUS, height, width); + return fd.ok() ? reinterpret_cast<jlong>(new VirtualStylus(std::move(fd))) : INVALID_PTR; +} + static void nativeCloseUinput(JNIEnv* env, jobject thiz, jlong ptr) { VirtualInputDevice* virtualInputDevice = reinterpret_cast<VirtualInputDevice*>(ptr); delete virtualInputDevice; @@ -287,6 +365,22 @@ static bool nativeWriteScrollEvent(JNIEnv* env, jobject thiz, jlong ptr, jfloat std::chrono::nanoseconds(eventTimeNanos)); } +// Native methods for VirtualStylus +static bool nativeWriteStylusMotionEvent(JNIEnv* env, jobject thiz, jlong ptr, jint toolType, + jint action, jint locationX, jint locationY, jint pressure, + jint tiltX, jint tiltY, jlong eventTimeNanos) { + VirtualStylus* virtualStylus = reinterpret_cast<VirtualStylus*>(ptr); + return virtualStylus->writeMotionEvent(toolType, action, locationX, locationY, pressure, tiltX, + tiltY, std::chrono::nanoseconds(eventTimeNanos)); +} + +static bool nativeWriteStylusButtonEvent(JNIEnv* env, jobject thiz, jlong ptr, jint buttonCode, + jint action, jlong eventTimeNanos) { + VirtualStylus* virtualStylus = reinterpret_cast<VirtualStylus*>(ptr); + return virtualStylus->writeButtonEvent(buttonCode, action, + std::chrono::nanoseconds(eventTimeNanos)); +} + static JNINativeMethod methods[] = { {"nativeOpenUinputDpad", "(Ljava/lang/String;IILjava/lang/String;)J", (void*)nativeOpenUinputDpad}, @@ -296,6 +390,8 @@ static JNINativeMethod methods[] = { (void*)nativeOpenUinputMouse}, {"nativeOpenUinputTouchscreen", "(Ljava/lang/String;IILjava/lang/String;II)J", (void*)nativeOpenUinputTouchscreen}, + {"nativeOpenUinputStylus", "(Ljava/lang/String;IILjava/lang/String;II)J", + (void*)nativeOpenUinputStylus}, {"nativeCloseUinput", "(J)V", (void*)nativeCloseUinput}, {"nativeWriteDpadKeyEvent", "(JIIJ)Z", (void*)nativeWriteDpadKeyEvent}, {"nativeWriteKeyEvent", "(JIIJ)Z", (void*)nativeWriteKeyEvent}, @@ -303,6 +399,8 @@ static JNINativeMethod methods[] = { {"nativeWriteTouchEvent", "(JIIIFFFFJ)Z", (void*)nativeWriteTouchEvent}, {"nativeWriteRelativeEvent", "(JFFJ)Z", (void*)nativeWriteRelativeEvent}, {"nativeWriteScrollEvent", "(JFFJ)Z", (void*)nativeWriteScrollEvent}, + {"nativeWriteStylusMotionEvent", "(JIIIIIIIJ)Z", (void*)nativeWriteStylusMotionEvent}, + {"nativeWriteStylusButtonEvent", "(JIIJ)Z", (void*)nativeWriteStylusButtonEvent}, }; int register_android_server_companion_virtual_InputController(JNIEnv* env) { diff --git a/services/core/jni/com_android_server_input_InputManagerService.cpp b/services/core/jni/com_android_server_input_InputManagerService.cpp index 414339d3f349..2b9bb7a4de87 100644 --- a/services/core/jni/com_android_server_input_InputManagerService.cpp +++ b/services/core/jni/com_android_server_input_InputManagerService.cpp @@ -685,6 +685,8 @@ void NativeInputManager::getReaderConfiguration(InputReaderConfiguration* outCon { // acquire lock std::scoped_lock _l(mLock); + outConfig->mousePointerSpeed = mLocked.pointerSpeed; + outConfig->mousePointerAccelerationEnabled = mLocked.mousePointerAccelerationEnabled; outConfig->pointerVelocityControlParameters.scale = exp2f(mLocked.pointerSpeed * POINTER_SPEED_EXPONENT); outConfig->pointerVelocityControlParameters.acceleration = diff --git a/services/core/jni/onload.cpp b/services/core/jni/onload.cpp index a4b1f841d3bc..5d1eb496903b 100644 --- a/services/core/jni/onload.cpp +++ b/services/core/jni/onload.cpp @@ -66,7 +66,6 @@ int register_android_server_stats_pull_StatsPullAtomService(JNIEnv* env); int register_android_server_sensor_SensorService(JavaVM* vm, JNIEnv* env); int register_android_server_companion_virtual_InputController(JNIEnv* env); int register_android_server_app_GameManagerService(JNIEnv* env); -int register_com_android_server_BootReceiver(JNIEnv* env); int register_com_android_server_wm_TaskFpsCallbackController(JNIEnv* env); int register_com_android_server_display_DisplayControl(JNIEnv* env); int register_com_android_server_SystemClockTime(JNIEnv* env); @@ -129,7 +128,6 @@ extern "C" jint JNI_OnLoad(JavaVM* vm, void* /* reserved */) register_android_server_sensor_SensorService(vm, env); register_android_server_companion_virtual_InputController(env); register_android_server_app_GameManagerService(env); - register_com_android_server_BootReceiver(env); register_com_android_server_wm_TaskFpsCallbackController(env); register_com_android_server_display_DisplayControl(env); register_com_android_server_SystemClockTime(env); diff --git a/services/core/xsd/display-device-config/display-device-config.xsd b/services/core/xsd/display-device-config/display-device-config.xsd index 3cbceec5b9cd..a46916553abc 100644 --- a/services/core/xsd/display-device-config/display-device-config.xsd +++ b/services/core/xsd/display-device-config/display-device-config.xsd @@ -625,6 +625,9 @@ If no mode is specified, the mapping will be used for the default mode. If no setting is specified, the mapping will be used for the normal brightness setting. + + If no mapping is defined for one of the settings, the mapping for the normal setting will be + used as a fallback. --> <xs:complexType name="luxToBrightnessMapping"> <xs:element name="map" type="nonNegativeFloatToFloatMap"> diff --git a/services/credentials/java/com/android/server/credentials/CredentialManagerService.java b/services/credentials/java/com/android/server/credentials/CredentialManagerService.java index fb0729f806b1..667e086c8b40 100644 --- a/services/credentials/java/com/android/server/credentials/CredentialManagerService.java +++ b/services/credentials/java/com/android/server/credentials/CredentialManagerService.java @@ -63,7 +63,6 @@ import android.text.TextUtils; import android.util.Pair; import android.util.Slog; import android.util.SparseArray; -import android.view.autofill.IAutoFillManagerClient; import com.android.internal.annotations.GuardedBy; import com.android.server.credentials.metrics.ApiName; @@ -484,7 +483,7 @@ public final class CredentialManagerService public ICancellationSignal getCandidateCredentials( GetCredentialRequest request, IGetCandidateCredentialsCallback callback, - IAutoFillManagerClient clientCallback, + IBinder clientBinder, final String callingPackage) { Slog.i(TAG, "starting getCandidateCredentials with callingPackage: " + callingPackage); @@ -506,7 +505,7 @@ public final class CredentialManagerService constructCallingAppInfo(callingPackage, userId, request.getOrigin()), getEnabledProvidersForUser(userId), CancellationSignal.fromTransport(cancelTransport), - clientCallback + clientBinder ); addSessionLocked(userId, session); diff --git a/services/credentials/java/com/android/server/credentials/GetCandidateRequestSession.java b/services/credentials/java/com/android/server/credentials/GetCandidateRequestSession.java index 0187ce8140f5..25281ba89960 100644 --- a/services/credentials/java/com/android/server/credentials/GetCandidateRequestSession.java +++ b/services/credentials/java/com/android/server/credentials/GetCandidateRequestSession.java @@ -30,11 +30,11 @@ import android.credentials.ui.GetCredentialProviderData; import android.credentials.ui.ProviderData; import android.credentials.ui.RequestInfo; import android.os.CancellationSignal; +import android.os.IBinder; import android.os.RemoteException; import android.service.credentials.CallingAppInfo; import android.service.credentials.PermissionUtils; import android.util.Slog; -import android.view.autofill.IAutoFillManagerClient; import java.util.ArrayList; import java.util.List; @@ -52,7 +52,7 @@ public class GetCandidateRequestSession extends RequestSession<GetCredentialRequ private static final String SESSION_ID_KEY = "autofill_session_id"; private static final String REQUEST_ID_KEY = "autofill_request_id"; - private final IAutoFillManagerClient mAutoFillCallback; + private final IBinder mClientBinder; private final int mAutofillSessionId; private final int mAutofillRequestId; @@ -62,15 +62,15 @@ public class GetCandidateRequestSession extends RequestSession<GetCredentialRequ IGetCandidateCredentialsCallback callback, GetCredentialRequest request, CallingAppInfo callingAppInfo, Set<ComponentName> enabledProviders, CancellationSignal cancellationSignal, - IAutoFillManagerClient autoFillCallback) { + IBinder clientBinder) { super(context, sessionCallback, lock, userId, callingUid, request, callback, RequestInfo.TYPE_GET, callingAppInfo, enabledProviders, cancellationSignal, 0L, /*shouldBindClientToDeath=*/ false); - mAutoFillCallback = autoFillCallback; + mClientBinder = clientBinder; mAutofillSessionId = request.getData().getInt(SESSION_ID_KEY, -1); mAutofillRequestId = request.getData().getInt(REQUEST_ID_KEY, -1); - if (mAutoFillCallback != null) { - setUpClientCallbackListener(mAutoFillCallback.asBinder()); + if (mClientBinder != null) { + setUpClientCallbackListener(mClientBinder); } } diff --git a/services/foldables/devicestateprovider/proguard.flags b/services/foldables/devicestateprovider/proguard.flags index 069cbc642050..b810cad5217d 100644 --- a/services/foldables/devicestateprovider/proguard.flags +++ b/services/foldables/devicestateprovider/proguard.flags @@ -1 +1 @@ --keep,allowoptimization,allowaccessmodification class com.android.server.policy.TentModeDeviceStatePolicy { *; } +-keep,allowoptimization,allowaccessmodification class com.android.server.policy.BookStyleDeviceStatePolicy { *; } diff --git a/services/foldables/devicestateprovider/src/com/android/server/policy/BookStyleClosedStatePredicate.java b/services/foldables/devicestateprovider/src/com/android/server/policy/BookStyleClosedStatePredicate.java new file mode 100644 index 000000000000..d5a3cffd71dd --- /dev/null +++ b/services/foldables/devicestateprovider/src/com/android/server/policy/BookStyleClosedStatePredicate.java @@ -0,0 +1,432 @@ +/* + * 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.policy; + +import static android.hardware.SensorManager.SENSOR_DELAY_NORMAL; +import static android.view.Display.DEFAULT_DISPLAY; + +import static com.android.server.policy.BookStylePreferredScreenCalculator.PreferredScreen.OUTER; +import static com.android.server.policy.BookStylePreferredScreenCalculator.HingeAngle.ANGLE_0; +import static com.android.server.policy.BookStylePreferredScreenCalculator.HingeAngle.ANGLE_0_TO_45; +import static com.android.server.policy.BookStylePreferredScreenCalculator.HingeAngle.ANGLE_45_TO_90; +import static com.android.server.policy.BookStylePreferredScreenCalculator.HingeAngle.ANGLE_90_TO_180; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.Context; +import android.hardware.Sensor; +import android.hardware.SensorEvent; +import android.hardware.SensorEventListener; +import android.hardware.SensorManager; +import android.hardware.display.DisplayManager; +import android.os.Handler; +import android.util.ArraySet; +import android.view.Display; +import android.view.Surface; + +import com.android.server.policy.BookStylePreferredScreenCalculator.PreferredScreen; +import com.android.server.policy.BookStylePreferredScreenCalculator.HingeAngle; +import com.android.server.policy.BookStylePreferredScreenCalculator.StateTransition; +import com.android.server.policy.BookStyleClosedStatePredicate.ConditionSensorListener.SensorSubscription; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.function.Predicate; +import java.util.function.Supplier; + +/** + * 'Closed' state predicate that takes into account the posture of the device + * It accepts list of state transitions that control how the device moves between + * device states. + * See {@link BookStyleStateTransitions} for detailed description of the default behavior. + */ +public class BookStyleClosedStatePredicate implements Predicate<FoldableDeviceStateProvider>, + DisplayManager.DisplayListener { + + private final BookStylePreferredScreenCalculator mClosedStateCalculator; + private final Handler mHandler = new Handler(); + private final PostureEstimator mPostureEstimator; + private final DisplayManager mDisplayManager; + + /** + * Creates {@link BookStyleClosedStatePredicate}. It is expected that the device has a pair + * of accelerometer sensors (one for each movable part of the device), see parameter + * descriptions for the behaviour when these sensors are not available. + * @param context context that could be used to get system services + * @param updatesListener callback that will be executed whenever the predicate should be + * checked again + * @param leftAccelerometerSensor accelerometer sensor that is located in the half of the + * device that has the outer screen, in case if this sensor is + * not provided, tent/wedge mode will be detected only using + * orientation sensor and screen rotation, so this mode won't + * be accessible by putting the device on a flat surface + * @param rightAccelerometerSensor accelerometer sensor that is located on the opposite side + * across the hinge from the previous accelerometer sensor, + * in case if this sensor is not provided, reverse wedge mode + * won't be detected, so the device will use closed state using + * constant angle when folding + * @param stateTransitions definition of all possible state transitions, see + * {@link BookStyleStateTransitions} for sample and more details + */ + + public BookStyleClosedStatePredicate(@NonNull Context context, + @NonNull ClosedStateUpdatesListener updatesListener, + @Nullable Sensor leftAccelerometerSensor, @Nullable Sensor rightAccelerometerSensor, + @NonNull List<StateTransition> stateTransitions) { + mDisplayManager = context.getSystemService(DisplayManager.class); + mDisplayManager.registerDisplayListener(this, mHandler); + + mClosedStateCalculator = new BookStylePreferredScreenCalculator(stateTransitions); + + final SensorManager sensorManager = context.getSystemService(SensorManager.class); + final Sensor orientationSensor = sensorManager.getDefaultSensor( + Sensor.TYPE_DEVICE_ORIENTATION); + + mPostureEstimator = new PostureEstimator(mHandler, sensorManager, + leftAccelerometerSensor, rightAccelerometerSensor, orientationSensor, + updatesListener::onClosedStateUpdated); + } + + /** + * Based on the current sensor readings and current state, returns true if the device should use + * 'CLOSED' device state and false if it should not use 'CLOSED' state (e.g. could use half-open + * or open states). + */ + @Override + public boolean test(FoldableDeviceStateProvider foldableDeviceStateProvider) { + final HingeAngle hingeAngle = hingeAngleFromFloat( + foldableDeviceStateProvider.getHingeAngle()); + + mPostureEstimator.onDeviceClosedStatusChanged(hingeAngle == ANGLE_0); + + final PreferredScreen preferredScreen = mClosedStateCalculator. + calculatePreferredScreen(hingeAngle, mPostureEstimator.isLikelyTentOrWedgeMode(), + mPostureEstimator.isLikelyReverseWedgeMode(hingeAngle)); + + return preferredScreen == OUTER; + } + + private HingeAngle hingeAngleFromFloat(float hingeAngle) { + if (hingeAngle == 0f) { + return ANGLE_0; + } else if (hingeAngle < 45f) { + return ANGLE_0_TO_45; + } else if (hingeAngle < 90f) { + return ANGLE_45_TO_90; + } else { + return ANGLE_90_TO_180; + } + } + + @Override + public void onDisplayChanged(int displayId) { + if (displayId == DEFAULT_DISPLAY) { + final Display display = mDisplayManager.getDisplay(displayId); + int displayState = display.getState(); + boolean isDisplayOn = displayState == Display.STATE_ON; + mPostureEstimator.onDisplayPowerStatusChanged(isDisplayOn); + mPostureEstimator.onDisplayRotationChanged(display.getRotation()); + } + } + + @Override + public void onDisplayAdded(int displayId) { + + } + + @Override + public void onDisplayRemoved(int displayId) { + + } + + public interface ClosedStateUpdatesListener { + void onClosedStateUpdated(); + } + + /** + * Estimates if the device is going to enter wedge/tent mode based on the sensor data + */ + private static class PostureEstimator implements SensorEventListener { + + + private static final int FLAT_INCLINATION_THRESHOLD_DEGREES = 8; + + /** + * Alpha parameter of the accelerometer low pass filter: the lower the value, the less high + * frequency noise it filter but reduces the latency. + */ + private static final float GRAVITY_VECTOR_LOW_PASS_ALPHA_VALUE = 0.8f; + + + @Nullable + private final Sensor mLeftAccelerometerSensor; + @Nullable + private final Sensor mRightAccelerometerSensor; + private final Sensor mOrientationSensor; + private final Runnable mOnSensorUpdatedListener; + + private final ConditionSensorListener mConditionedSensorListener; + + @Nullable + private float[] mRightGravityVector; + + @Nullable + private float[] mLeftGravityVector; + + @Nullable + private Integer mLastScreenRotation; + + @Nullable + private SensorEvent mLastDeviceOrientationSensorEvent = null; + + private boolean mScreenTurnedOn = false; + private boolean mDeviceClosed = false; + + public PostureEstimator(Handler handler, SensorManager sensorManager, + @Nullable Sensor leftAccelerometerSensor, @Nullable Sensor rightAccelerometerSensor, + Sensor orientationSensor, Runnable onSensorUpdated) { + mLeftAccelerometerSensor = leftAccelerometerSensor; + mRightAccelerometerSensor = rightAccelerometerSensor; + mOrientationSensor = orientationSensor; + + mOnSensorUpdatedListener = onSensorUpdated; + + final List<SensorSubscription> sensorSubscriptions = new ArrayList<>(); + if (mLeftAccelerometerSensor != null) { + sensorSubscriptions.add(new SensorSubscription( + mLeftAccelerometerSensor, + /* allowedToListen= */ () -> mScreenTurnedOn && !mDeviceClosed, + /* cleanup= */ () -> mLeftGravityVector = null)); + } + + if (mRightAccelerometerSensor != null) { + sensorSubscriptions.add(new SensorSubscription( + mRightAccelerometerSensor, + /* allowedToListen= */ () -> mScreenTurnedOn, + /* cleanup= */ () -> mRightGravityVector = null)); + } + + sensorSubscriptions.add(new SensorSubscription(mOrientationSensor, + /* allowedToListen= */ () -> mScreenTurnedOn, + /* cleanup= */ () -> mLastDeviceOrientationSensorEvent = null)); + + mConditionedSensorListener = new ConditionSensorListener(sensorManager, this, handler, + sensorSubscriptions); + } + + @Override + public void onSensorChanged(SensorEvent event) { + if (event.sensor == mRightAccelerometerSensor) { + if (mRightGravityVector == null) { + mRightGravityVector = new float[3]; + } + setNewValueWithHighPassFilter(mRightGravityVector, event.values); + + final boolean isRightMostlyFlat = Objects.equals( + isGravityVectorMostlyFlat(mRightGravityVector), Boolean.TRUE); + + if (isRightMostlyFlat) { + // Reset orientation sensor when the device becomes flat + mLastDeviceOrientationSensorEvent = null; + } + } else if (event.sensor == mLeftAccelerometerSensor) { + if (mLeftGravityVector == null) { + mLeftGravityVector = new float[3]; + } + setNewValueWithHighPassFilter(mLeftGravityVector, event.values); + } else if (event.sensor == mOrientationSensor) { + mLastDeviceOrientationSensorEvent = event; + } + + mOnSensorUpdatedListener.run(); + } + + @Override + public void onAccuracyChanged(Sensor sensor, int accuracy) { + + } + + private void setNewValueWithHighPassFilter(float[] output, float[] newValues) { + final float alpha = GRAVITY_VECTOR_LOW_PASS_ALPHA_VALUE; + output[0] = alpha * output[0] + (1 - alpha) * newValues[0]; + output[1] = alpha * output[1] + (1 - alpha) * newValues[1]; + output[2] = alpha * output[2] + (1 - alpha) * newValues[2]; + } + + /** + * Returns true if the phone likely in reverse wedge mode (when a foldable phone is lying + * on the outer screen mostly flat to the ground) + */ + public boolean isLikelyReverseWedgeMode(HingeAngle hingeAngle) { + return hingeAngle != ANGLE_0 && Objects.equals( + isGravityVectorMostlyFlat(mLeftGravityVector), Boolean.TRUE); + } + + /** + * Returns true if the phone is likely in tent or wedge mode when unfolding. Tent mode + * is detected by checking if the phone is in seascape position, screen is rotated to + * landscape or seascape, or if the right side of the device is mostly flat. + */ + public boolean isLikelyTentOrWedgeMode() { + boolean isScreenLandscapeOrSeascape = Objects.equals(mLastScreenRotation, + Surface.ROTATION_270) || Objects.equals(mLastScreenRotation, + Surface.ROTATION_90); + if (isScreenLandscapeOrSeascape) { + return true; + } + + boolean isRightMostlyFlat = Objects.equals( + isGravityVectorMostlyFlat(mRightGravityVector), Boolean.TRUE); + if (isRightMostlyFlat) { + return true; + } + + boolean isSensorSeaScape = Objects.equals(getOrientationSensorRotation(), + Surface.ROTATION_270); + if (isSensorSeaScape) { + return true; + } + + return false; + } + + /** + * Returns true if the passed gravity vector implies that the phone is mostly flat (the + * vector is close to be perpendicular to the ground and has a positive Z component). + * Returns null if there is no data from the sensor. + */ + private Boolean isGravityVectorMostlyFlat(@Nullable float[] vector) { + if (vector == null) return null; + if (vector[0] == 0.0f && vector[1] == 0.0f && vector[2] == 0.0f) { + // Likely we haven't received the actual data yet, treat it as no data + return null; + } + + double vectorMagnitude = Math.sqrt( + vector[0] * vector[0] + vector[1] * vector[1] + vector[2] * vector[2]); + float normalizedGravityZ = (float) (vector[2] / vectorMagnitude); + + final int inclination = (int) Math.round(Math.toDegrees(Math.acos(normalizedGravityZ))); + return inclination < FLAT_INCLINATION_THRESHOLD_DEGREES; + } + + private Integer getOrientationSensorRotation() { + if (mLastDeviceOrientationSensorEvent == null) return null; + return (int) mLastDeviceOrientationSensorEvent.values[0]; + } + + /** + * Called whenever display status changes, we use this signal to start/stop listening + * to sensors when the display is off to save battery. Using display state instead of + * general power state to reduce the time when sensors are on, we don't need to listen + * to the extra sensors when the screen is off. + */ + public void onDisplayPowerStatusChanged(boolean screenTurnedOn) { + mScreenTurnedOn = screenTurnedOn; + mConditionedSensorListener.updateListeningState(); + } + + /** + * Called whenever we display rotation might have been updated + * @param rotation new rotation + */ + public void onDisplayRotationChanged(int rotation) { + mLastScreenRotation = rotation; + } + + /** + * Called whenever foldable device becomes fully closed or opened + */ + public void onDeviceClosedStatusChanged(boolean deviceClosed) { + mDeviceClosed = deviceClosed; + mConditionedSensorListener.updateListeningState(); + } + } + + /** + * Helper class that subscribes or unsubscribes from a sensor based on a condition specified + * in {@link SensorSubscription} + */ + static class ConditionSensorListener { + private final List<SensorSubscription> mSensorSubscriptions; + private final ArraySet<Sensor> mIsListening = new ArraySet<>(); + + private final SensorManager mSensorManager; + private final SensorEventListener mSensorEventListener; + + private final Handler mHandler; + + public ConditionSensorListener(SensorManager sensorManager, + SensorEventListener sensorEventListener, Handler handler, + List<SensorSubscription> sensorSubscriptions) { + mSensorManager = sensorManager; + mSensorEventListener = sensorEventListener; + mSensorSubscriptions = sensorSubscriptions; + mHandler = handler; + } + + /** + * Updates current listening state of the sensor based on the provided conditions + */ + public void updateListeningState() { + for (int i = 0; i < mSensorSubscriptions.size(); i++) { + final SensorSubscription subscription = mSensorSubscriptions.get(i); + final Sensor sensor = subscription.mSensor; + + final boolean shouldBeListening = subscription.mAllowedToListenSupplier.get(); + final boolean isListening = mIsListening.contains(sensor); + final boolean shouldUpdateListening = isListening != shouldBeListening; + + if (shouldUpdateListening) { + if (shouldBeListening) { + mIsListening.add(sensor); + mSensorManager.registerListener(mSensorEventListener, sensor, + SENSOR_DELAY_NORMAL, mHandler); + } else { + mIsListening.remove(sensor); + mSensorManager.unregisterListener(mSensorEventListener, sensor); + subscription.mOnUnsubscribe.run(); + } + } + } + } + + /** + * Represents a configuration of a single sensor subscription + */ + public static class SensorSubscription { + private final Sensor mSensor; + private final Supplier<Boolean> mAllowedToListenSupplier; + private final Runnable mOnUnsubscribe; + + /** + * @param sensor sensor to listen to + * @param allowedToListen return true when it is allowed to listen to the sensor + * @param cleanup a runnable that will be closed just before unsubscribing from the + * sensor + */ + + public SensorSubscription(Sensor sensor, Supplier<Boolean> allowedToListen, + Runnable cleanup) { + mSensor = sensor; + mAllowedToListenSupplier = allowedToListen; + mOnUnsubscribe = cleanup; + } + } + } +} diff --git a/services/foldables/devicestateprovider/src/com/android/server/policy/TentModeDeviceStatePolicy.java b/services/foldables/devicestateprovider/src/com/android/server/policy/BookStyleDeviceStatePolicy.java index 5968b6346d35..ad938aff396a 100644 --- a/services/foldables/devicestateprovider/src/com/android/server/policy/TentModeDeviceStatePolicy.java +++ b/services/foldables/devicestateprovider/src/com/android/server/policy/BookStyleDeviceStatePolicy.java @@ -21,10 +21,12 @@ import static com.android.server.devicestate.DeviceState.FLAG_CANCEL_WHEN_REQUES import static com.android.server.devicestate.DeviceState.FLAG_EMULATED_ONLY; import static com.android.server.devicestate.DeviceState.FLAG_UNSUPPORTED_WHEN_POWER_SAVE_MODE; import static com.android.server.devicestate.DeviceState.FLAG_UNSUPPORTED_WHEN_THERMAL_STATUS_CRITICAL; +import static com.android.server.policy.BookStyleStateTransitions.DEFAULT_STATE_TRANSITIONS; import static com.android.server.policy.FoldableDeviceStateProvider.DeviceStateConfiguration.createConfig; import static com.android.server.policy.FoldableDeviceStateProvider.DeviceStateConfiguration.createTentModeClosedState; import android.annotation.NonNull; +import android.annotation.Nullable; import android.content.Context; import android.hardware.Sensor; import android.hardware.SensorManager; @@ -39,12 +41,15 @@ import com.android.server.policy.feature.flags.FeatureFlagsImpl; import java.util.function.Predicate; /** - * Device state policy for a foldable device that supports tent mode: a mode when the device - * keeps the outer display on until reaching a certain hinge angle threshold. + * Device state policy for a foldable device with two screens in a book style, where the hinge is + * located on the left side of the device when in folded posture. + * The policy supports tent/wedge mode: a mode when the device keeps the outer display on + * until reaching certain conditions like hinge angle threshold. * * Contains configuration for {@link FoldableDeviceStateProvider}. */ -public class TentModeDeviceStatePolicy extends DeviceStatePolicy { +public class BookStyleDeviceStatePolicy extends DeviceStatePolicy implements + BookStyleClosedStatePredicate.ClosedStateUpdatesListener { private static final int DEVICE_STATE_CLOSED = 0; private static final int DEVICE_STATE_HALF_OPENED = 1; @@ -57,9 +62,10 @@ public class TentModeDeviceStatePolicy extends DeviceStatePolicy { private static final int MIN_CLOSED_ANGLE_DEGREES = 0; private static final int MAX_CLOSED_ANGLE_DEGREES = 5; - private final DeviceStateProvider mProvider; + private final FoldableDeviceStateProvider mProvider; private final boolean mIsDualDisplayBlockingEnabled; + private final boolean mEnablePostureBasedClosedState; private static final Predicate<FoldableDeviceStateProvider> ALLOWED = p -> true; private static final Predicate<FoldableDeviceStateProvider> NOT_ALLOWED = p -> false; @@ -73,30 +79,30 @@ public class TentModeDeviceStatePolicy extends DeviceStatePolicy { * between folded and unfolded modes, otherwise when folding the * display switch will happen at 0 degrees */ - public TentModeDeviceStatePolicy(@NonNull Context context, - @NonNull Sensor hingeAngleSensor, @NonNull Sensor hallSensor, int closeAngleDegrees) { - this(new FeatureFlagsImpl(), context, hingeAngleSensor, hallSensor, closeAngleDegrees); - } - - public TentModeDeviceStatePolicy(@NonNull FeatureFlags featureFlags, @NonNull Context context, - @NonNull Sensor hingeAngleSensor, @NonNull Sensor hallSensor, - int closeAngleDegrees) { + public BookStyleDeviceStatePolicy(@NonNull FeatureFlags featureFlags, @NonNull Context context, + @NonNull Sensor hingeAngleSensor, @NonNull Sensor hallSensor, + @Nullable Sensor leftAccelerometerSensor, @Nullable Sensor rightAccelerometerSensor, + Integer closeAngleDegrees) { super(context); final SensorManager sensorManager = mContext.getSystemService(SensorManager.class); final DisplayManager displayManager = mContext.getSystemService(DisplayManager.class); - final DeviceStateConfiguration[] configuration = createConfiguration(closeAngleDegrees); - + mEnablePostureBasedClosedState = featureFlags.enableFoldablesPostureBasedClosedState(); mIsDualDisplayBlockingEnabled = featureFlags.enableDualDisplayBlocking(); + final DeviceStateConfiguration[] configuration = createConfiguration( + leftAccelerometerSensor, rightAccelerometerSensor, closeAngleDegrees); + mProvider = new FoldableDeviceStateProvider(mContext, sensorManager, hingeAngleSensor, hallSensor, displayManager, configuration); } - private DeviceStateConfiguration[] createConfiguration(int closeAngleDegrees) { + private DeviceStateConfiguration[] createConfiguration(@Nullable Sensor leftAccelerometerSensor, + @Nullable Sensor rightAccelerometerSensor, Integer closeAngleDegrees) { return new DeviceStateConfiguration[]{ - createClosedConfiguration(closeAngleDegrees), + createClosedConfiguration(leftAccelerometerSensor, rightAccelerometerSensor, + closeAngleDegrees), createConfig(DEVICE_STATE_HALF_OPENED, /* name= */ "HALF_OPENED", /* activeStatePredicate= */ (provider) -> { @@ -123,8 +129,10 @@ public class TentModeDeviceStatePolicy extends DeviceStatePolicy { }; } - private DeviceStateConfiguration createClosedConfiguration(int closeAngleDegrees) { - if (closeAngleDegrees > 0) { + private DeviceStateConfiguration createClosedConfiguration( + @Nullable Sensor leftAccelerometerSensor, @Nullable Sensor rightAccelerometerSensor, + @Nullable Integer closeAngleDegrees) { + if (closeAngleDegrees != null) { // Switch displays at closeAngleDegrees in both ways (folding and unfolding) return createConfig( DEVICE_STATE_CLOSED, @@ -137,6 +145,19 @@ public class TentModeDeviceStatePolicy extends DeviceStatePolicy { ); } + if (mEnablePostureBasedClosedState) { + // Use smart closed state predicate that will use different switch angles + // based on the device posture (e.g. wedge mode, tent mode, reverse wedge mode) + return createConfig( + DEVICE_STATE_CLOSED, + /* name= */ "CLOSED", + /* flags= */ FLAG_CANCEL_OVERRIDE_REQUESTS, + /* activeStatePredicate= */ new BookStyleClosedStatePredicate(mContext, + this, leftAccelerometerSensor, rightAccelerometerSensor, + DEFAULT_STATE_TRANSITIONS) + ); + } + // Switch to the outer display only at 0 degrees but use TENT_MODE_SWITCH_ANGLE_DEGREES // angle when switching to the inner display return createTentModeClosedState(DEVICE_STATE_CLOSED, @@ -148,6 +169,11 @@ public class TentModeDeviceStatePolicy extends DeviceStatePolicy { } @Override + public void onClosedStateUpdated() { + mProvider.notifyDeviceStateChangedIfNeeded(); + } + + @Override public DeviceStateProvider getDeviceStateProvider() { return mProvider; } diff --git a/services/foldables/devicestateprovider/src/com/android/server/policy/BookStylePreferredScreenCalculator.java b/services/foldables/devicestateprovider/src/com/android/server/policy/BookStylePreferredScreenCalculator.java new file mode 100644 index 000000000000..8977422a90a8 --- /dev/null +++ b/services/foldables/devicestateprovider/src/com/android/server/policy/BookStylePreferredScreenCalculator.java @@ -0,0 +1,309 @@ +/* + * 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.policy; + +import android.annotation.Nullable; + +import java.util.List; +import java.util.Objects; + +/** + * Calculates if we should use outer or inner display on foldable devices based on a several + * inputs like device orientation, hinge angle signals. + * + * This is a stateful class and acts like a state machine with fixed number of states + * and transitions. It allows to list all possible state transitions instead of performing + * imperative logic to make sure that we cover all scenarios and improve debuggability. + * + * See {@link BookStyleStateTransitions} for detailed description of the default behavior. + */ +public class BookStylePreferredScreenCalculator { + + /** + * When calculating the new state we will re-calculate it until it settles down. We re-calculate + * it because the new state might trigger another state transition and this might happen + * several times. We don't want to have infinite loops in state calculation, so this value + * limits the number of such state transitions. + * For example, in the default configuration {@link BookStyleStateTransitions}, after each + * transition with 'set sticky flag' output it will perform a transition to a state without + * 'set sticky flag' output. + * We also have a unit test covering all possible states which checks that we don't have such + * states that could end up in an infinite transition. See sample test for the default + * transitions in {@link BookStyleClosedStateCalculatorTest}. + */ + private static final int MAX_STATE_CHANGES = 16; + + private State mState = new State( + /* stickyKeepOuterUntil90Degrees= */ false, + /* stickyKeepInnerUntil45Degrees= */ false, + PreferredScreen.INVALID); + + private final List<StateTransition> mStateTransitions; + + /** + * Creates BookStyleClosedStateCalculator + * @param stateTransitions list of all state transitions + */ + public BookStylePreferredScreenCalculator(List<StateTransition> stateTransitions) { + mStateTransitions = stateTransitions; + } + + /** + * Calculates updated {@link PreferredScreen} based on the current inputs and the current state. + * The calculation is done based on defined {@link StateTransition}s, it might perform + * multiple transitions until we settle down on a single state. Multiple transitions could be + * performed in case if {@link StateTransition} causes another update of the state. + * There is a limit of maximum {@link MAX_STATE_CHANGES} state transitions, after which + * this method will throw an {@link IllegalStateException}. + * + * @param angle current hinge angle + * @param likelyTentOrWedge true if the device is likely in tent or wedge mode + * @param likelyReverseWedge true if the device is likely in reverse wedge mode + * @return updated {@link PreferredScreen} + */ + public PreferredScreen calculatePreferredScreen(HingeAngle angle, boolean likelyTentOrWedge, + boolean likelyReverseWedge) { + int attempts = 0; + State newState = calculateNewState(mState, angle, likelyTentOrWedge, likelyReverseWedge); + while (attempts < MAX_STATE_CHANGES && !Objects.equals(mState, newState)) { + mState = newState; + newState = calculateNewState(mState, angle, likelyTentOrWedge, likelyReverseWedge); + attempts++; + } + + if (attempts >= MAX_STATE_CHANGES) { + throw new IllegalStateException( + "Can't settle state " + mState + ", inputs: hingeAngle = " + angle + + ", likelyTentOrWedge = " + likelyTentOrWedge + + ", likelyReverseWedge = " + likelyReverseWedge); + } + + final State oldState = mState; + mState = newState; + + if (mState.mPreferredScreen == PreferredScreen.INVALID) { + throw new IllegalStateException( + "Reached invalid state " + mState + ", inputs: hingeAngle = " + angle + + ", likelyTentOrWedge = " + likelyTentOrWedge + + ", likelyReverseWedge = " + likelyReverseWedge + ", old state: " + + oldState); + } + + return mState.mPreferredScreen; + } + + /** + * Returns the current state of the calculator + */ + public State getState() { + return mState; + } + + private State calculateNewState(State current, HingeAngle hingeAngle, boolean likelyTentOrWedge, + boolean likelyReverseWedge) { + for (int i = 0; i < mStateTransitions.size(); i++) { + final State newState = mStateTransitions.get(i).tryTransition(hingeAngle, + likelyTentOrWedge, likelyReverseWedge, current); + if (newState != null) { + return newState; + } + } + + throw new IllegalArgumentException( + "Entry not found for state: " + current + ", hingeAngle = " + hingeAngle + + ", likelyTentOrWedge = " + likelyTentOrWedge + ", likelyReverseWedge = " + + likelyReverseWedge); + } + + /** + * The angle between two halves of the foldable device in degrees. The angle is '0' when + * the device is fully closed and '180' when the device is fully open and flat. + */ + public enum HingeAngle { + ANGLE_0, + ANGLE_0_TO_45, + ANGLE_45_TO_90, + ANGLE_90_TO_180 + } + + /** + * Resulting closed state of the device, where OPEN state indicates that the device should use + * the inner display and CLOSED means that it should use the outer (cover) screen. + */ + public enum PreferredScreen { + INNER, + OUTER, + INVALID + } + + /** + * Describes a state transition for the posture based active screen calculator + */ + public static class StateTransition { + private final Input mInput; + private final State mOutput; + + public StateTransition(HingeAngle hingeAngle, boolean likelyTentOrWedge, + boolean likelyReverseWedge, + boolean stickyKeepOuterUntil90Degrees, boolean stickyKeepInnerUntil45Degrees, + PreferredScreen preferredScreen, Boolean setStickyKeepOuterUntil90Degrees, + Boolean setStickyKeepInnerUntil45Degrees) { + mInput = new Input(hingeAngle, likelyTentOrWedge, likelyReverseWedge, + stickyKeepOuterUntil90Degrees, stickyKeepInnerUntil45Degrees); + mOutput = new State(setStickyKeepOuterUntil90Degrees, + setStickyKeepInnerUntil45Degrees, preferredScreen); + } + + /** + * Returns true if the state transition is applicable for the given inputs + */ + private boolean isApplicable(HingeAngle hingeAngle, boolean likelyTentOrWedge, + boolean likelyReverseWedge, State currentState) { + return mInput.hingeAngle == hingeAngle + && mInput.likelyTentOrWedge == likelyTentOrWedge + && mInput.likelyReverseWedge == likelyReverseWedge + && Objects.equals(mInput.stickyKeepOuterUntil90Degrees, + currentState.stickyKeepOuterUntil90Degrees) + && Objects.equals(mInput.stickyKeepInnerUntil45Degrees, + currentState.stickyKeepInnerUntil45Degrees); + } + + /** + * Try to perform transition for the inputs, returns new state if this + * transition is applicable for the given state and inputs + */ + @Nullable + State tryTransition(HingeAngle hingeAngle, boolean likelyTentOrWedge, + boolean likelyReverseWedge, State currentState) { + if (!isApplicable(hingeAngle, likelyTentOrWedge, likelyReverseWedge, currentState)) { + return null; + } + + boolean stickyKeepOuterUntil90Degrees = currentState.stickyKeepOuterUntil90Degrees; + boolean stickyKeepInnerUntil45Degrees = currentState.stickyKeepInnerUntil45Degrees; + + if (mOutput.stickyKeepOuterUntil90Degrees != null) { + stickyKeepOuterUntil90Degrees = + mOutput.stickyKeepOuterUntil90Degrees; + } + + if (mOutput.stickyKeepInnerUntil45Degrees != null) { + stickyKeepInnerUntil45Degrees = + mOutput.stickyKeepInnerUntil45Degrees; + } + + return new State(stickyKeepOuterUntil90Degrees, stickyKeepInnerUntil45Degrees, + mOutput.mPreferredScreen); + } + } + + /** + * The input part of the {@link StateTransition}, these are the values that are used + * to decide which {@link State} output to choose. + */ + private static class Input { + final HingeAngle hingeAngle; + final boolean likelyTentOrWedge; + final boolean likelyReverseWedge; + final boolean stickyKeepOuterUntil90Degrees; + final boolean stickyKeepInnerUntil45Degrees; + + public Input(HingeAngle hingeAngle, boolean likelyTentOrWedge, + boolean likelyReverseWedge, + boolean stickyKeepOuterUntil90Degrees, boolean stickyKeepInnerUntil45Degrees) { + this.hingeAngle = hingeAngle; + this.likelyTentOrWedge = likelyTentOrWedge; + this.likelyReverseWedge = likelyReverseWedge; + this.stickyKeepOuterUntil90Degrees = stickyKeepOuterUntil90Degrees; + this.stickyKeepInnerUntil45Degrees = stickyKeepInnerUntil45Degrees; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Input)) return false; + Input that = (Input) o; + return likelyTentOrWedge == that.likelyTentOrWedge + && likelyReverseWedge == that.likelyReverseWedge + && stickyKeepOuterUntil90Degrees == that.stickyKeepOuterUntil90Degrees + && stickyKeepInnerUntil45Degrees == that.stickyKeepInnerUntil45Degrees + && hingeAngle == that.hingeAngle; + } + + @Override + public int hashCode() { + return Objects.hash(hingeAngle, likelyTentOrWedge, likelyReverseWedge, + stickyKeepOuterUntil90Degrees, stickyKeepInnerUntil45Degrees); + } + + @Override + public String toString() { + return "InputState{" + + "hingeAngle=" + hingeAngle + + ", likelyTentOrWedge=" + likelyTentOrWedge + + ", likelyReverseWedge=" + likelyReverseWedge + + ", stickyKeepOuterUntil90Degrees=" + stickyKeepOuterUntil90Degrees + + ", stickyKeepInnerUntil45Degrees=" + stickyKeepInnerUntil45Degrees + + '}'; + } + } + + /** + * Class that holds a state of the calculator, it could be used to store the current + * state or to define the target (output) state based on some input in {@link StateTransition}. + */ + public static class State { + public Boolean stickyKeepOuterUntil90Degrees; + public Boolean stickyKeepInnerUntil45Degrees; + + PreferredScreen mPreferredScreen; + + public State(Boolean stickyKeepOuterUntil90Degrees, + Boolean stickyKeepInnerUntil45Degrees, + PreferredScreen preferredScreen) { + this.stickyKeepOuterUntil90Degrees = stickyKeepOuterUntil90Degrees; + this.stickyKeepInnerUntil45Degrees = stickyKeepInnerUntil45Degrees; + this.mPreferredScreen = preferredScreen; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof State)) return false; + State that = (State) o; + return Objects.equals(stickyKeepOuterUntil90Degrees, + that.stickyKeepOuterUntil90Degrees) && Objects.equals( + stickyKeepInnerUntil45Degrees, that.stickyKeepInnerUntil45Degrees) + && mPreferredScreen == that.mPreferredScreen; + } + + @Override + public int hashCode() { + return Objects.hash(stickyKeepOuterUntil90Degrees, stickyKeepInnerUntil45Degrees, + mPreferredScreen); + } + + @Override + public String toString() { + return "State{" + + "stickyKeepOuterUntil90Degrees=" + stickyKeepOuterUntil90Degrees + + ", stickyKeepInnerUntil90Degrees=" + stickyKeepInnerUntil45Degrees + + ", closedState=" + mPreferredScreen + + '}'; + } + } +} diff --git a/services/foldables/devicestateprovider/src/com/android/server/policy/BookStyleStateTransitions.java b/services/foldables/devicestateprovider/src/com/android/server/policy/BookStyleStateTransitions.java new file mode 100644 index 000000000000..16daacb36693 --- /dev/null +++ b/services/foldables/devicestateprovider/src/com/android/server/policy/BookStyleStateTransitions.java @@ -0,0 +1,722 @@ +/* + * 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.policy; + +import com.android.server.policy.BookStylePreferredScreenCalculator.PreferredScreen; +import com.android.server.policy.BookStylePreferredScreenCalculator.HingeAngle; +import com.android.server.policy.BookStylePreferredScreenCalculator.StateTransition; + +import java.util.ArrayList; +import java.util.List; + +/** + * Describes all possible state transitions for {@link BookStylePreferredScreenCalculator}. + * It contains a default configuration for a foldable device that has two screens: smaller outer + * screen which has portrait natural orientation and a larger inner screen and allows to use the + * device in tent mode or wedge mode. + * + * As the output state could affect calculating of the new state, it could potentially cause + * infinite loop and make the state never settle down. This could be avoided using automated test + * that checks all possible inputs and asserts that the final state is valid. + * See sample test for the default transitions in {@link BookStyleClosedStateCalculatorTest}. + * + * - Tent mode is defined as a posture when the device is partially opened and placed on the ground + * on the edges that are parallel to the hinge. + * - Wedge mode is when the device is partially opened and placed flat on the ground with the part + * of the device that doesn't have the display + * - Reverse wedge mode is when the device is partially opened and placed flat on the ground with + * the outer screen down, so the outer screen is not accessible + * + * Behavior description: + * - When unfolding with screens off we assume that no sensor data available except hinge angle + * (based on hall sensor), so we switch to the inner screen immediately + * + * - When unfolding when screen is 'on' we can check if we are likely in tent or wedge mode + * - If not likely tent/wedge mode or sensors data not available, then we unfold immediately + * After unfolding, the state of the inner screen 'on' is sticky between 0 and 45 degrees, so + * it won't jump back to the outer screen even if you move the phone into tent/wedge mode. The + * stickiness is reset after fully closing the device or unfolding past 45 degrees. + * - If likely tent or wedge mode, switch only at 90 degrees + * Tent/wedge mode is 'sticky' between 0 and 90 degrees, so it won't reset until you either + * fully close the device or unfold past 90 degrees. + * + * - When folding we can check if we are likely in reverse wedge mode + * - If not likely in reverse wedge mode or sensor data is not available we switch to the outer + * screen at 45 degrees and enable sticky tent/wedge mode as before, this allows to enter + * tent/wedge mode even if you are not on an even surface or holding phone in landscape + * - If likely in reverse wedge mode, switch to the outer screen only at 0 degrees to allow + * some use cases like using camera in this posture, the check happens after passing 45 degrees + * and inner screen becomes sticky turned 'on' until fully closing or unfolding past 45 degrees + */ +public class BookStyleStateTransitions { + + public static final List<StateTransition> DEFAULT_STATE_TRANSITIONS = new ArrayList<>(); + + static { + // region Angle 0 + DEFAULT_STATE_TRANSITIONS.add(new StateTransition( + HingeAngle.ANGLE_0, + /* likelyTentOrWedge */ false, + /* likelyReverseWedge */ false, + /* stickyKeepOuterUntil90Degrees */ false, + /* stickyKeepInnerUntil45Degrees */ false, + PreferredScreen.OUTER, + /* setStickyKeepOuterUntil90Degrees */ null, + /* setStickyKeepInnerUntil45Degrees */ true + )); + DEFAULT_STATE_TRANSITIONS.add(new StateTransition( + HingeAngle.ANGLE_0, + /* likelyTentOrWedge */ false, + /* likelyReverseWedge */ false, + /* stickyKeepOuterUntil90Degrees */ false, + /* stickyKeepInnerUntil45Degrees */ true, + PreferredScreen.OUTER, + /* setStickyKeepOuterUntil90Degrees */ null, + /* setStickyKeepInnerUntil45Degrees */ null + )); + DEFAULT_STATE_TRANSITIONS.add(new StateTransition( + HingeAngle.ANGLE_0, + /* likelyTentOrWedge */ false, + /* likelyReverseWedge */ false, + /* stickyKeepOuterUntil90Degrees */ true, + /* stickyKeepInnerUntil45Degrees */ false, + PreferredScreen.OUTER, + /* setStickyKeepOuterUntil90Degrees */ false, + /* setStickyKeepInnerUntil45Degrees */ true + )); + DEFAULT_STATE_TRANSITIONS.add(new StateTransition( + HingeAngle.ANGLE_0, + /* likelyTentOrWedge */ false, + /* likelyReverseWedge */ false, + /* stickyKeepOuterUntil90Degrees */ true, + /* stickyKeepInnerUntil45Degrees */ true, + PreferredScreen.INVALID, + /* setStickyKeepOuterUntil90Degrees */ null, + /* setStickyKeepInnerUntil45Degrees */ null + )); + DEFAULT_STATE_TRANSITIONS.add(new StateTransition( + HingeAngle.ANGLE_0, + /* likelyTentOrWedge */ false, + /* likelyReverseWedge */ true, + /* stickyKeepOuterUntil90Degrees */ false, + /* stickyKeepInnerUntil45Degrees */ false, + PreferredScreen.OUTER, + /* setStickyKeepOuterUntil90Degrees */ null, + /* setStickyKeepInnerUntil45Degrees */ true + )); + DEFAULT_STATE_TRANSITIONS.add(new StateTransition( + HingeAngle.ANGLE_0, + /* likelyTentOrWedge */ false, + /* likelyReverseWedge */ true, + /* stickyKeepOuterUntil90Degrees */ false, + /* stickyKeepInnerUntil45Degrees */ true, + PreferredScreen.OUTER, + /* setStickyKeepOuterUntil90Degrees */ null, + /* setStickyKeepInnerUntil45Degrees */ null + )); + DEFAULT_STATE_TRANSITIONS.add(new StateTransition( + HingeAngle.ANGLE_0, + /* likelyTentOrWedge */ false, + /* likelyReverseWedge */ true, + /* stickyKeepOuterUntil90Degrees */ true, + /* stickyKeepInnerUntil45Degrees */ false, + PreferredScreen.OUTER, + /* setStickyKeepOuterUntil90Degrees */ false, + /* setStickyKeepInnerUntil45Degrees */ true + )); + DEFAULT_STATE_TRANSITIONS.add(new StateTransition( + HingeAngle.ANGLE_0, + /* likelyTentOrWedge */ false, + /* likelyReverseWedge */ true, + /* stickyKeepOuterUntil90Degrees */ true, + /* stickyKeepInnerUntil45Degrees */ true, + PreferredScreen.INVALID, + /* setStickyKeepOuterUntil90Degrees */ null, + /* setStickyKeepInnerUntil45Degrees */ null + )); + DEFAULT_STATE_TRANSITIONS.add(new StateTransition( + HingeAngle.ANGLE_0, + /* likelyTentOrWedge */ true, + /* likelyReverseWedge */ false, + /* stickyKeepOuterUntil90Degrees */ false, + /* stickyKeepInnerUntil45Degrees */ false, + PreferredScreen.OUTER, + /* setStickyKeepOuterUntil90Degrees */ null, + /* setStickyKeepInnerUntil45Degrees */ null + )); + DEFAULT_STATE_TRANSITIONS.add(new StateTransition( + HingeAngle.ANGLE_0, + /* likelyTentOrWedge */ true, + /* likelyReverseWedge */ false, + /* stickyKeepOuterUntil90Degrees */ false, + /* stickyKeepInnerUntil45Degrees */ true, + PreferredScreen.OUTER, + /* setStickyKeepOuterUntil90Degrees */ null, + /* setStickyKeepInnerUntil45Degrees */ false + )); + DEFAULT_STATE_TRANSITIONS.add(new StateTransition( + HingeAngle.ANGLE_0, + /* likelyTentOrWedge */ true, + /* likelyReverseWedge */ false, + /* stickyKeepOuterUntil90Degrees */ true, + /* stickyKeepInnerUntil45Degrees */ false, + PreferredScreen.OUTER, + /* setStickyKeepOuterUntil90Degrees */ false, + /* setStickyKeepInnerUntil45Degrees */ null + )); + DEFAULT_STATE_TRANSITIONS.add(new StateTransition( + HingeAngle.ANGLE_0, + /* likelyTentOrWedge */ true, + /* likelyReverseWedge */ false, + /* stickyKeepOuterUntil90Degrees */ true, + /* stickyKeepInnerUntil45Degrees */ true, + PreferredScreen.INVALID, + /* setStickyKeepOuterUntil90Degrees */ null, + /* setStickyKeepInnerUntil45Degrees */ null + )); + DEFAULT_STATE_TRANSITIONS.add(new StateTransition( + HingeAngle.ANGLE_0, + /* likelyTentOrWedge */ true, + /* likelyReverseWedge */ true, + /* stickyKeepOuterUntil90Degrees */ false, + /* stickyKeepInnerUntil45Degrees */ false, + PreferredScreen.OUTER, + /* setStickyKeepOuterUntil90Degrees */ null, + /* setStickyKeepInnerUntil45Degrees */ null + )); + DEFAULT_STATE_TRANSITIONS.add(new StateTransition( + HingeAngle.ANGLE_0, + /* likelyTentOrWedge */ true, + /* likelyReverseWedge */ true, + /* stickyKeepOuterUntil90Degrees */ false, + /* stickyKeepInnerUntil45Degrees */ true, + PreferredScreen.OUTER, + /* setStickyKeepOuterUntil90Degrees */ null, + /* setStickyKeepInnerUntil45Degrees */ false + )); + DEFAULT_STATE_TRANSITIONS.add(new StateTransition( + HingeAngle.ANGLE_0, + /* likelyTentOrWedge */ true, + /* likelyReverseWedge */ true, + /* stickyKeepOuterUntil90Degrees */ true, + /* stickyKeepInnerUntil45Degrees */ false, + PreferredScreen.OUTER, + /* setStickyKeepOuterUntil90Degrees */ null, + /* setStickyKeepInnerUntil45Degrees */ null + )); + DEFAULT_STATE_TRANSITIONS.add(new StateTransition( + HingeAngle.ANGLE_0, + /* likelyTentOrWedge */ true, + /* likelyReverseWedge */ true, + /* stickyKeepOuterUntil90Degrees */ true, + /* stickyKeepInnerUntil45Degrees */ true, + PreferredScreen.INVALID, + /* setStickyKeepOuterUntil90Degrees */ null, + /* setStickyKeepInnerUntil45Degrees */ null + )); + // endregion + + // region Angle 0-45 + DEFAULT_STATE_TRANSITIONS.add(new StateTransition( + HingeAngle.ANGLE_0_TO_45, + /* likelyTentOrWedge */ false, + /* likelyReverseWedge */ false, + /* stickyKeepOuterUntil90Degrees */ false, + /* stickyKeepInnerUntil45Degrees */ false, + PreferredScreen.OUTER, + /* setStickyKeepOuterUntil90Degrees */ true, + /* setStickyKeepInnerUntil45Degrees */ null + )); + DEFAULT_STATE_TRANSITIONS.add(new StateTransition( + HingeAngle.ANGLE_0_TO_45, + /* likelyTentOrWedge */ false, + /* likelyReverseWedge */ false, + /* stickyKeepOuterUntil90Degrees */ false, + /* stickyKeepInnerUntil45Degrees */ true, + PreferredScreen.INNER, + /* setStickyKeepOuterUntil90Degrees */ null, + /* setStickyKeepInnerUntil45Degrees */ null + )); + DEFAULT_STATE_TRANSITIONS.add(new StateTransition( + HingeAngle.ANGLE_0_TO_45, + /* likelyTentOrWedge */ false, + /* likelyReverseWedge */ false, + /* stickyKeepOuterUntil90Degrees */ true, + /* stickyKeepInnerUntil45Degrees */ false, + PreferredScreen.OUTER, + /* setStickyKeepOuterUntil90Degrees */ null, + /* setStickyKeepInnerUntil45Degrees */ null + )); + DEFAULT_STATE_TRANSITIONS.add(new StateTransition( + HingeAngle.ANGLE_0_TO_45, + /* likelyTentOrWedge */ false, + /* likelyReverseWedge */ false, + /* stickyKeepOuterUntil90Degrees */ true, + /* stickyKeepInnerUntil45Degrees */ true, + PreferredScreen.INVALID, + /* setStickyKeepOuterUntil90Degrees */ null, + /* setStickyKeepInnerUntil45Degrees */ null + )); + DEFAULT_STATE_TRANSITIONS.add(new StateTransition( + HingeAngle.ANGLE_0_TO_45, + /* likelyTentOrWedge */ false, + /* likelyReverseWedge */ true, + /* stickyKeepOuterUntil90Degrees */ false, + /* stickyKeepInnerUntil45Degrees */ false, + PreferredScreen.INNER, + /* setStickyKeepOuterUntil90Degrees */ null, + /* setStickyKeepInnerUntil45Degrees */ true + )); + DEFAULT_STATE_TRANSITIONS.add(new StateTransition( + HingeAngle.ANGLE_0_TO_45, + /* likelyTentOrWedge */ false, + /* likelyReverseWedge */ true, + /* stickyKeepOuterUntil90Degrees */ false, + /* stickyKeepInnerUntil45Degrees */ true, + PreferredScreen.INNER, + /* setStickyKeepOuterUntil90Degrees */ null, + /* setStickyKeepInnerUntil45Degrees */ null + )); + DEFAULT_STATE_TRANSITIONS.add(new StateTransition( + HingeAngle.ANGLE_0_TO_45, + /* likelyTentOrWedge */ false, + /* likelyReverseWedge */ true, + /* stickyKeepOuterUntil90Degrees */ true, + /* stickyKeepInnerUntil45Degrees */ false, + PreferredScreen.OUTER, + /* setStickyKeepOuterUntil90Degrees */ null, + /* setStickyKeepInnerUntil45Degrees */ null + )); + DEFAULT_STATE_TRANSITIONS.add(new StateTransition( + HingeAngle.ANGLE_0_TO_45, + /* likelyTentOrWedge */ false, + /* likelyReverseWedge */ true, + /* stickyKeepOuterUntil90Degrees */ true, + /* stickyKeepInnerUntil45Degrees */ true, + PreferredScreen.INVALID, + /* setStickyKeepOuterUntil90Degrees */ null, + /* setStickyKeepInnerUntil45Degrees */ null + )); + DEFAULT_STATE_TRANSITIONS.add(new StateTransition( + HingeAngle.ANGLE_0_TO_45, + /* likelyTentOrWedge */ true, + /* likelyReverseWedge */ false, + /* stickyKeepOuterUntil90Degrees */ false, + /* stickyKeepInnerUntil45Degrees */ false, + PreferredScreen.OUTER, + /* setStickyKeepOuterUntil90Degrees */ true, + /* setStickyKeepInnerUntil45Degrees */ null + )); + DEFAULT_STATE_TRANSITIONS.add(new StateTransition( + HingeAngle.ANGLE_0_TO_45, + /* likelyTentOrWedge */ true, + /* likelyReverseWedge */ false, + /* stickyKeepOuterUntil90Degrees */ false, + /* stickyKeepInnerUntil45Degrees */ true, + PreferredScreen.INNER, + /* setStickyKeepOuterUntil90Degrees */ null, + /* setStickyKeepInnerUntil45Degrees */ null + )); + DEFAULT_STATE_TRANSITIONS.add(new StateTransition( + HingeAngle.ANGLE_0_TO_45, + /* likelyTentOrWedge */ true, + /* likelyReverseWedge */ false, + /* stickyKeepOuterUntil90Degrees */ true, + /* stickyKeepInnerUntil45Degrees */ false, + PreferredScreen.OUTER, + /* setStickyKeepOuterUntil90Degrees */ null, + /* setStickyKeepInnerUntil45Degrees */ null + )); + DEFAULT_STATE_TRANSITIONS.add(new StateTransition( + HingeAngle.ANGLE_0_TO_45, + /* likelyTentOrWedge */ true, + /* likelyReverseWedge */ false, + /* stickyKeepOuterUntil90Degrees */ true, + /* stickyKeepInnerUntil45Degrees */ true, + PreferredScreen.INVALID, + /* setStickyKeepOuterUntil90Degrees */ null, + /* setStickyKeepInnerUntil45Degrees */ null + )); + DEFAULT_STATE_TRANSITIONS.add(new StateTransition( + HingeAngle.ANGLE_0_TO_45, + /* likelyTentOrWedge */ true, + /* likelyReverseWedge */ true, + /* stickyKeepOuterUntil90Degrees */ false, + /* stickyKeepInnerUntil45Degrees */ false, + PreferredScreen.INNER, + /* setStickyKeepOuterUntil90Degrees */ null, + /* setStickyKeepInnerUntil45Degrees */ true + )); + DEFAULT_STATE_TRANSITIONS.add(new StateTransition( + HingeAngle.ANGLE_0_TO_45, + /* likelyTentOrWedge */ true, + /* likelyReverseWedge */ true, + /* stickyKeepOuterUntil90Degrees */ false, + /* stickyKeepInnerUntil45Degrees */ true, + PreferredScreen.INNER, + /* setStickyKeepOuterUntil90Degrees */ null, + /* setStickyKeepInnerUntil45Degrees */ null + )); + DEFAULT_STATE_TRANSITIONS.add(new StateTransition( + HingeAngle.ANGLE_0_TO_45, + /* likelyTentOrWedge */ true, + /* likelyReverseWedge */ true, + /* stickyKeepOuterUntil90Degrees */ true, + /* stickyKeepInnerUntil45Degrees */ false, + PreferredScreen.OUTER, + /* setStickyKeepOuterUntil90Degrees */ null, + /* setStickyKeepInnerUntil45Degrees */ null + )); + DEFAULT_STATE_TRANSITIONS.add(new StateTransition( + HingeAngle.ANGLE_0_TO_45, + /* likelyTentOrWedge */ true, + /* likelyReverseWedge */ true, + /* stickyKeepOuterUntil90Degrees */ true, + /* stickyKeepInnerUntil45Degrees */ true, + PreferredScreen.INVALID, + /* setStickyKeepOuterUntil90Degrees */ null, + /* setStickyKeepInnerUntil45Degrees */ null + )); + // endregion + + // region Angle 45-90 + DEFAULT_STATE_TRANSITIONS.add(new StateTransition( + HingeAngle.ANGLE_45_TO_90, + /* likelyTentOrWedge */ false, + /* likelyReverseWedge */ false, + /* stickyKeepOuterUntil90Degrees */ false, + /* stickyKeepInnerUntil45Degrees */ false, + PreferredScreen.INNER, + /* setStickyKeepOuterUntil90Degrees */ null, + /* setStickyKeepInnerUntil45Degrees */ null + )); + DEFAULT_STATE_TRANSITIONS.add(new StateTransition( + HingeAngle.ANGLE_45_TO_90, + /* likelyTentOrWedge */ false, + /* likelyReverseWedge */ false, + /* stickyKeepOuterUntil90Degrees */ false, + /* stickyKeepInnerUntil45Degrees */ true, + PreferredScreen.INNER, + /* setStickyKeepOuterUntil90Degrees */ null, + /* setStickyKeepInnerUntil45Degrees */ false + )); + DEFAULT_STATE_TRANSITIONS.add(new StateTransition( + HingeAngle.ANGLE_45_TO_90, + /* likelyTentOrWedge */ false, + /* likelyReverseWedge */ false, + /* stickyKeepOuterUntil90Degrees */ true, + /* stickyKeepInnerUntil45Degrees */ false, + PreferredScreen.OUTER, + /* setStickyKeepOuterUntil90Degrees */ null, + /* setStickyKeepInnerUntil45Degrees */ null + )); + DEFAULT_STATE_TRANSITIONS.add(new StateTransition( + HingeAngle.ANGLE_45_TO_90, + /* likelyTentOrWedge */ false, + /* likelyReverseWedge */ false, + /* stickyKeepOuterUntil90Degrees */ true, + /* stickyKeepInnerUntil45Degrees */ true, + PreferredScreen.INVALID, + /* setStickyKeepOuterUntil90Degrees */ null, + /* setStickyKeepInnerUntil45Degrees */ null + )); + DEFAULT_STATE_TRANSITIONS.add(new StateTransition( + HingeAngle.ANGLE_45_TO_90, + /* likelyTentOrWedge */ false, + /* likelyReverseWedge */ true, + /* stickyKeepOuterUntil90Degrees */ false, + /* stickyKeepInnerUntil45Degrees */ false, + PreferredScreen.INNER, + /* setStickyKeepOuterUntil90Degrees */ null, + /* setStickyKeepInnerUntil45Degrees */ true + )); + DEFAULT_STATE_TRANSITIONS.add(new StateTransition( + HingeAngle.ANGLE_45_TO_90, + /* likelyTentOrWedge */ false, + /* likelyReverseWedge */ true, + /* stickyKeepOuterUntil90Degrees */ false, + /* stickyKeepInnerUntil45Degrees */ true, + PreferredScreen.INNER, + /* setStickyKeepOuterUntil90Degrees */ null, + /* setStickyKeepInnerUntil45Degrees */ null + )); + DEFAULT_STATE_TRANSITIONS.add(new StateTransition( + HingeAngle.ANGLE_45_TO_90, + /* likelyTentOrWedge */ false, + /* likelyReverseWedge */ true, + /* stickyKeepOuterUntil90Degrees */ true, + /* stickyKeepInnerUntil45Degrees */ false, + PreferredScreen.OUTER, + /* setStickyKeepOuterUntil90Degrees */ null, + /* setStickyKeepInnerUntil45Degrees */ null + )); + DEFAULT_STATE_TRANSITIONS.add(new StateTransition( + HingeAngle.ANGLE_45_TO_90, + /* likelyTentOrWedge */ false, + /* likelyReverseWedge */ true, + /* stickyKeepOuterUntil90Degrees */ true, + /* stickyKeepInnerUntil45Degrees */ true, + PreferredScreen.INVALID, + /* setStickyKeepOuterUntil90Degrees */ null, + /* setStickyKeepInnerUntil45Degrees */ null + )); + DEFAULT_STATE_TRANSITIONS.add(new StateTransition( + HingeAngle.ANGLE_45_TO_90, + /* likelyTentOrWedge */ true, + /* likelyReverseWedge */ false, + /* stickyKeepOuterUntil90Degrees */ false, + /* stickyKeepInnerUntil45Degrees */ false, + PreferredScreen.INNER, + /* setStickyKeepOuterUntil90Degrees */ null, + /* setStickyKeepInnerUntil45Degrees */ null + )); + DEFAULT_STATE_TRANSITIONS.add(new StateTransition( + HingeAngle.ANGLE_45_TO_90, + /* likelyTentOrWedge */ true, + /* likelyReverseWedge */ false, + /* stickyKeepOuterUntil90Degrees */ false, + /* stickyKeepInnerUntil45Degrees */ true, + PreferredScreen.INNER, + /* setStickyKeepOuterUntil90Degrees */ null, + /* setStickyKeepInnerUntil45Degrees */ false + )); + DEFAULT_STATE_TRANSITIONS.add(new StateTransition( + HingeAngle.ANGLE_45_TO_90, + /* likelyTentOrWedge */ true, + /* likelyReverseWedge */ false, + /* stickyKeepOuterUntil90Degrees */ true, + /* stickyKeepInnerUntil45Degrees */ false, + PreferredScreen.OUTER, + /* setStickyKeepOuterUntil90Degrees */ null, + /* setStickyKeepInnerUntil45Degrees */ null + )); + DEFAULT_STATE_TRANSITIONS.add(new StateTransition( + HingeAngle.ANGLE_45_TO_90, + /* likelyTentOrWedge */ true, + /* likelyReverseWedge */ false, + /* stickyKeepOuterUntil90Degrees */ true, + /* stickyKeepInnerUntil45Degrees */ true, + PreferredScreen.INVALID, + /* setStickyKeepOuterUntil90Degrees */ null, + /* setStickyKeepInnerUntil45Degrees */ null + )); + DEFAULT_STATE_TRANSITIONS.add(new StateTransition( + HingeAngle.ANGLE_45_TO_90, + /* likelyTentOrWedge */ true, + /* likelyReverseWedge */ true, + /* stickyKeepOuterUntil90Degrees */ false, + /* stickyKeepInnerUntil45Degrees */ false, + PreferredScreen.INNER, + /* setStickyKeepOuterUntil90Degrees */ null, + /* setStickyKeepInnerUntil45Degrees */ true + )); + DEFAULT_STATE_TRANSITIONS.add(new StateTransition( + HingeAngle.ANGLE_45_TO_90, + /* likelyTentOrWedge */ true, + /* likelyReverseWedge */ true, + /* stickyKeepOuterUntil90Degrees */ false, + /* stickyKeepInnerUntil45Degrees */ true, + PreferredScreen.INNER, + /* setStickyKeepOuterUntil90Degrees */ null, + /* setStickyKeepInnerUntil45Degrees */ null + )); + DEFAULT_STATE_TRANSITIONS.add(new StateTransition( + HingeAngle.ANGLE_45_TO_90, + /* likelyTentOrWedge */ true, + /* likelyReverseWedge */ true, + /* stickyKeepOuterUntil90Degrees */ true, + /* stickyKeepInnerUntil45Degrees */ false, + PreferredScreen.OUTER, + /* setStickyKeepOuterUntil90Degrees */ null, + /* setStickyKeepInnerUntil45Degrees */ null + )); + DEFAULT_STATE_TRANSITIONS.add(new StateTransition( + HingeAngle.ANGLE_45_TO_90, + /* likelyTentOrWedge */ true, + /* likelyReverseWedge */ true, + /* stickyKeepOuterUntil90Degrees */ true, + /* stickyKeepInnerUntil45Degrees */ true, + PreferredScreen.INVALID, + /* setStickyKeepOuterUntil90Degrees */ null, + /* setStickyKeepInnerUntil45Degrees */ null + )); + // endregion + + // region Angle 90-180 + DEFAULT_STATE_TRANSITIONS.add(new StateTransition( + HingeAngle.ANGLE_90_TO_180, + /* likelyTentOrWedge */ false, + /* likelyReverseWedge */ false, + /* stickyKeepOuterUntil90Degrees */ false, + /* stickyKeepInnerUntil45Degrees */ false, + PreferredScreen.INNER, + /* setStickyKeepOuterUntil90Degrees */ null, + /* setStickyKeepInnerUntil45Degrees */ null + )); + DEFAULT_STATE_TRANSITIONS.add(new StateTransition( + HingeAngle.ANGLE_90_TO_180, + /* likelyTentOrWedge */ false, + /* likelyReverseWedge */ false, + /* stickyKeepOuterUntil90Degrees */ false, + /* stickyKeepInnerUntil45Degrees */ true, + PreferredScreen.INNER, + /* setStickyKeepOuterUntil90Degrees */ null, + /* setStickyKeepInnerUntil45Degrees */ false + )); + DEFAULT_STATE_TRANSITIONS.add(new StateTransition( + HingeAngle.ANGLE_90_TO_180, + /* likelyTentOrWedge */ false, + /* likelyReverseWedge */ false, + /* stickyKeepOuterUntil90Degrees */ true, + /* stickyKeepInnerUntil45Degrees */ false, + PreferredScreen.INNER, + /* setStickyKeepOuterUntil90Degrees */ false, + /* setStickyKeepInnerUntil45Degrees */ null + )); + DEFAULT_STATE_TRANSITIONS.add(new StateTransition( + HingeAngle.ANGLE_90_TO_180, + /* likelyTentOrWedge */ false, + /* likelyReverseWedge */ false, + /* stickyKeepOuterUntil90Degrees */ true, + /* stickyKeepInnerUntil45Degrees */ true, + PreferredScreen.INVALID, + /* setStickyKeepOuterUntil90Degrees */ null, + /* setStickyKeepInnerUntil45Degrees */ null + )); + DEFAULT_STATE_TRANSITIONS.add(new StateTransition( + HingeAngle.ANGLE_90_TO_180, + /* likelyTentOrWedge */ false, + /* likelyReverseWedge */ true, + /* stickyKeepOuterUntil90Degrees */ false, + /* stickyKeepInnerUntil45Degrees */ false, + PreferredScreen.INNER, + /* setStickyKeepOuterUntil90Degrees */ null, + /* setStickyKeepInnerUntil45Degrees */ null + )); + DEFAULT_STATE_TRANSITIONS.add(new StateTransition( + HingeAngle.ANGLE_90_TO_180, + /* likelyTentOrWedge */ false, + /* likelyReverseWedge */ true, + /* stickyKeepOuterUntil90Degrees */ false, + /* stickyKeepInnerUntil45Degrees */ true, + PreferredScreen.INNER, + /* setStickyKeepOuterUntil90Degrees */ null, + /* setStickyKeepInnerUntil45Degrees */ false + )); + DEFAULT_STATE_TRANSITIONS.add(new StateTransition( + HingeAngle.ANGLE_90_TO_180, + /* likelyTentOrWedge */ false, + /* likelyReverseWedge */ true, + /* stickyKeepOuterUntil90Degrees */ true, + /* stickyKeepInnerUntil45Degrees */ false, + PreferredScreen.INNER, + /* setStickyKeepOuterUntil90Degrees */ false, + /* setStickyKeepInnerUntil45Degrees */ null + )); + DEFAULT_STATE_TRANSITIONS.add(new StateTransition( + HingeAngle.ANGLE_90_TO_180, + /* likelyTentOrWedge */ false, + /* likelyReverseWedge */ true, + /* stickyKeepOuterUntil90Degrees */ true, + /* stickyKeepInnerUntil45Degrees */ true, + PreferredScreen.INVALID, + /* setStickyKeepOuterUntil90Degrees */ null, + /* setStickyKeepInnerUntil45Degrees */ null + )); + DEFAULT_STATE_TRANSITIONS.add(new StateTransition( + HingeAngle.ANGLE_90_TO_180, + /* likelyTentOrWedge */ true, + /* likelyReverseWedge */ false, + /* stickyKeepOuterUntil90Degrees */ false, + /* stickyKeepInnerUntil45Degrees */ false, + PreferredScreen.INNER, + /* setStickyKeepOuterUntil90Degrees */ null, + /* setStickyKeepInnerUntil45Degrees */ null + )); + DEFAULT_STATE_TRANSITIONS.add(new StateTransition( + HingeAngle.ANGLE_90_TO_180, + /* likelyTentOrWedge */ true, + /* likelyReverseWedge */ false, + /* stickyKeepOuterUntil90Degrees */ false, + /* stickyKeepInnerUntil45Degrees */ true, + PreferredScreen.INNER, + /* setStickyKeepOuterUntil90Degrees */ null, + /* setStickyKeepInnerUntil45Degrees */ false + )); + DEFAULT_STATE_TRANSITIONS.add(new StateTransition( + HingeAngle.ANGLE_90_TO_180, + /* likelyTentOrWedge */ true, + /* likelyReverseWedge */ false, + /* stickyKeepOuterUntil90Degrees */ true, + /* stickyKeepInnerUntil45Degrees */ false, + PreferredScreen.INNER, + /* setStickyKeepOuterUntil90Degrees */ false, + /* setStickyKeepInnerUntil45Degrees */ null + )); + DEFAULT_STATE_TRANSITIONS.add(new StateTransition( + HingeAngle.ANGLE_90_TO_180, + /* likelyTentOrWedge */ true, + /* likelyReverseWedge */ false, + /* stickyKeepOuterUntil90Degrees */ true, + /* stickyKeepInnerUntil45Degrees */ true, + PreferredScreen.INVALID, + /* setStickyKeepOuterUntil90Degrees */ null, + /* setStickyKeepInnerUntil45Degrees */ null + )); + DEFAULT_STATE_TRANSITIONS.add(new StateTransition( + HingeAngle.ANGLE_90_TO_180, + /* likelyTentOrWedge */ true, + /* likelyReverseWedge */ true, + /* stickyKeepOuterUntil90Degrees */ false, + /* stickyKeepInnerUntil45Degrees */ false, + PreferredScreen.INNER, + /* setStickyKeepOuterUntil90Degrees */ null, + /* setStickyKeepInnerUntil45Degrees */ null + )); + DEFAULT_STATE_TRANSITIONS.add(new StateTransition( + HingeAngle.ANGLE_90_TO_180, + /* likelyTentOrWedge */ true, + /* likelyReverseWedge */ true, + /* stickyKeepOuterUntil90Degrees */ false, + /* stickyKeepInnerUntil45Degrees */ true, + PreferredScreen.INNER, + /* setStickyKeepOuterUntil90Degrees */ null, + /* setStickyKeepInnerUntil45Degrees */ false + )); + DEFAULT_STATE_TRANSITIONS.add(new StateTransition( + HingeAngle.ANGLE_90_TO_180, + /* likelyTentOrWedge */ true, + /* likelyReverseWedge */ true, + /* stickyKeepOuterUntil90Degrees */ true, + /* stickyKeepInnerUntil45Degrees */ false, + PreferredScreen.INNER, + /* setStickyKeepOuterUntil90Degrees */ false, + /* setStickyKeepInnerUntil45Degrees */ null + )); + DEFAULT_STATE_TRANSITIONS.add(new StateTransition( + HingeAngle.ANGLE_90_TO_180, + /* likelyTentOrWedge */ true, + /* likelyReverseWedge */ true, + /* stickyKeepOuterUntil90Degrees */ true, + /* stickyKeepInnerUntil45Degrees */ true, + PreferredScreen.INVALID, + /* setStickyKeepOuterUntil90Degrees */ null, + /* setStickyKeepInnerUntil45Degrees */ null + )); + // endregion + } +} diff --git a/services/foldables/devicestateprovider/tests/src/com/android/server/policy/BookStyleDeviceStatePolicyTest.java b/services/foldables/devicestateprovider/tests/src/com/android/server/policy/BookStyleDeviceStatePolicyTest.java new file mode 100644 index 000000000000..8d01b7a9c523 --- /dev/null +++ b/services/foldables/devicestateprovider/tests/src/com/android/server/policy/BookStyleDeviceStatePolicyTest.java @@ -0,0 +1,703 @@ +/* + * 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.policy; + +import static android.view.Display.DEFAULT_DISPLAY; +import static android.view.Display.STATE_OFF; +import static android.view.Display.STATE_ON; + +import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth.assertWithMessage; + +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.nullable; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.clearInvocations; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.app.Instrumentation; +import android.content.res.Configuration; +import android.hardware.Sensor; +import android.hardware.SensorEvent; +import android.hardware.SensorEventListener; +import android.hardware.SensorManager; +import android.hardware.display.DisplayManager; +import android.hardware.input.InputSensorInfo; +import android.os.Handler; +import android.testing.AndroidTestingRunner; +import android.testing.TestableContext; +import android.view.Display; +import android.view.Surface; + +import androidx.test.platform.app.InstrumentationRegistry; + +import com.android.server.devicestate.DeviceStateProvider; +import com.android.server.devicestate.DeviceStateProvider.Listener; +import com.android.server.policy.feature.flags.FakeFeatureFlagsImpl; +import com.android.server.policy.feature.flags.Flags; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.mockito.internal.util.reflection.FieldSetter; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Unit tests for {@link BookStyleDeviceStatePolicy.Provider}. + * <p/> + * Run with <code>atest BookStyleDeviceStatePolicyTest</code>. + */ +@RunWith(AndroidTestingRunner.class) +public final class BookStyleDeviceStatePolicyTest { + + private static final int DEVICE_STATE_CLOSED = 0; + private static final int DEVICE_STATE_HALF_OPENED = 1; + private static final int DEVICE_STATE_OPENED = 2; + + @Captor + private ArgumentCaptor<Integer> mDeviceStateCaptor; + @Captor + private ArgumentCaptor<DisplayManager.DisplayListener> mDisplayListenerCaptor; + @Mock + private SensorManager mSensorManager; + @Mock + private InputSensorInfo mInputSensorInfo; + @Mock + private Listener mListener; + @Mock + DisplayManager mDisplayManager; + @Mock + private Display mDisplay; + + private final FakeFeatureFlagsImpl mFakeFeatureFlags = new FakeFeatureFlagsImpl(); + + private final Configuration mConfiguration = new Configuration(); + + private final Instrumentation mInstrumentation = InstrumentationRegistry.getInstrumentation(); + + @Rule + public final TestableContext mContext = new TestableContext( + mInstrumentation.getTargetContext()); + + private Sensor mHallSensor; + private Sensor mOrientationSensor; + private Sensor mHingeAngleSensor; + private Sensor mLeftAccelerometer; + private Sensor mRightAccelerometer; + + private Map<Sensor, List<SensorEventListener>> mSensorEventListeners = new HashMap<>(); + private DeviceStateProvider mProvider; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + + mFakeFeatureFlags.setFlag(Flags.FLAG_ENABLE_FOLDABLES_POSTURE_BASED_CLOSED_STATE, true); + mFakeFeatureFlags.setFlag(Flags.FLAG_ENABLE_DUAL_DISPLAY_BLOCKING, true); + + when(mInputSensorInfo.getName()).thenReturn("hall-effect"); + mHallSensor = new Sensor(mInputSensorInfo); + when(mInputSensorInfo.getName()).thenReturn("hinge-angle"); + mHingeAngleSensor = new Sensor(mInputSensorInfo); + when(mInputSensorInfo.getName()).thenReturn("left-accelerometer"); + mLeftAccelerometer = new Sensor(mInputSensorInfo); + when(mInputSensorInfo.getName()).thenReturn("right-accelerometer"); + mRightAccelerometer = new Sensor(mInputSensorInfo); + when(mInputSensorInfo.getName()).thenReturn("orientation"); + mOrientationSensor = new Sensor(mInputSensorInfo); + + mContext.addMockSystemService(SensorManager.class, mSensorManager); + + when(mSensorManager.getDefaultSensor(eq(Sensor.TYPE_HINGE_ANGLE), eq(true))) + .thenReturn(mHingeAngleSensor); + when(mSensorManager.getDefaultSensor(eq(Sensor.TYPE_DEVICE_ORIENTATION))) + .thenReturn(mOrientationSensor); + + when(mDisplayManager.getDisplay(eq(DEFAULT_DISPLAY))).thenReturn(mDisplay); + mContext.addMockSystemService(DisplayManager.class, mDisplayManager); + + mContext.ensureTestableResources(); + when(mContext.getResources().getConfiguration()).thenReturn(mConfiguration); + + final List<Sensor> sensors = new ArrayList<>(); + sensors.add(mHallSensor); + sensors.add(mHingeAngleSensor); + sensors.add(mOrientationSensor); + sensors.add(mLeftAccelerometer); + sensors.add(mRightAccelerometer); + + when(mSensorManager.registerListener(any(), any(), anyInt(), any())).thenAnswer( + invocation -> { + final SensorEventListener listener = invocation.getArgument(0); + final Sensor sensor = invocation.getArgument(1); + addSensorListener(sensor, listener); + return true; + }); + when(mSensorManager.registerListener(any(), any(), anyInt())).thenAnswer( + invocation -> { + final SensorEventListener listener = invocation.getArgument(0); + final Sensor sensor = invocation.getArgument(1); + addSensorListener(sensor, listener); + return true; + }); + + doAnswer(invocation -> { + final SensorEventListener listener = invocation.getArgument(0); + final boolean[] removed = {false}; + mSensorEventListeners.forEach((sensor, sensorEventListeners) -> + removed[0] |= sensorEventListeners.remove(listener)); + + if (!removed[0]) { + throw new IllegalArgumentException( + "Trying to unregister listener " + listener + " that was not registered"); + } + + return null; + }).when(mSensorManager).unregisterListener(any(SensorEventListener.class)); + + doAnswer(invocation -> { + final SensorEventListener listener = invocation.getArgument(0); + final Sensor sensor = invocation.getArgument(1); + + boolean removed = mSensorEventListeners.get(sensor).remove(listener); + if (!removed) { + throw new IllegalArgumentException( + "Trying to unregister listener " + listener + + " that was not registered for sensor " + sensor); + } + + return null; + }).when(mSensorManager).unregisterListener(any(SensorEventListener.class), + any(Sensor.class)); + + try { + FieldSetter.setField(mHallSensor, mHallSensor.getClass() + .getDeclaredField("mStringType"), "com.google.sensor.hall_effect"); + } catch (NoSuchFieldException e) { + throw new RuntimeException(e); + } + + when(mSensorManager.getSensorList(eq(Sensor.TYPE_ALL))) + .thenReturn(sensors); + + mInstrumentation.runOnMainSync(() -> mProvider = createProvider()); + + verify(mDisplayManager, atLeastOnce()).registerDisplayListener( + mDisplayListenerCaptor.capture(), nullable(Handler.class)); + setScreenOn(true); + } + + @Test + public void test_noSensorEventsYet_reportOpenedState() { + mProvider.setListener(mListener); + verify(mListener).onStateChanged(mDeviceStateCaptor.capture()); + assertEquals(DEVICE_STATE_OPENED, mDeviceStateCaptor.getValue().intValue()); + } + + @Test + public void test_deviceClosedSensorEventsBecameAvailable_reportsClosedState() { + mProvider.setListener(mListener); + clearInvocations(mListener); + + sendHingeAngle(0f); + + verify(mListener).onStateChanged(mDeviceStateCaptor.capture()); + assertEquals(DEVICE_STATE_CLOSED, mDeviceStateCaptor.getValue().intValue()); + } + + @Test + public void test_hingeAngleClosed_reportsClosedState() { + sendHingeAngle(0f); + + mProvider.setListener(mListener); + verify(mListener).onStateChanged(mDeviceStateCaptor.capture()); + assertEquals(DEVICE_STATE_CLOSED, mDeviceStateCaptor.getValue().intValue()); + } + + @Test + public void test_hingeAngleFullyOpened_reportsOpenedState() { + sendHingeAngle(180f); + + mProvider.setListener(mListener); + verify(mListener).onStateChanged(mDeviceStateCaptor.capture()); + assertEquals(DEVICE_STATE_OPENED, mDeviceStateCaptor.getValue().intValue()); + } + + @Test + public void test_unfoldingFromClosedToFullyOpened_reportsOpenedEvent() { + sendHingeAngle(0f); + mProvider.setListener(mListener); + clearInvocations(mListener); + + sendHingeAngle(180f); + + verify(mListener).onStateChanged(mDeviceStateCaptor.capture()); + assertEquals(DEVICE_STATE_OPENED, mDeviceStateCaptor.getValue().intValue()); + } + + @Test + public void test_foldingFromFullyOpenToFullyClosed_movesToClosedState() { + sendHingeAngle(180f); + + sendHingeAngle(0f); + + mProvider.setListener(mListener); + verify(mListener).onStateChanged(mDeviceStateCaptor.capture()); + assertEquals(DEVICE_STATE_CLOSED, mDeviceStateCaptor.getValue().intValue()); + } + + @Test + public void test_slowUnfolding_reportsEventsInOrder() { + sendHingeAngle(0f); + mProvider.setListener(mListener); + + sendHingeAngle(5f); + sendHingeAngle(10f); + sendHingeAngle(60f); + sendHingeAngle(100f); + sendHingeAngle(180f); + + verify(mListener, atLeastOnce()).onStateChanged(mDeviceStateCaptor.capture()); + assertThat(mDeviceStateCaptor.getAllValues()).containsExactly( + DEVICE_STATE_CLOSED, + DEVICE_STATE_HALF_OPENED, + DEVICE_STATE_OPENED + ); + } + + @Test + public void test_slowFolding_reportsEventsInOrder() { + sendHingeAngle(180f); + mProvider.setListener(mListener); + + sendHingeAngle(180f); + sendHingeAngle(100f); + sendHingeAngle(60f); + sendHingeAngle(10f); + sendHingeAngle(5f); + + verify(mListener, atLeastOnce()).onStateChanged(mDeviceStateCaptor.capture()); + assertThat(mDeviceStateCaptor.getAllValues()).containsExactly( + DEVICE_STATE_OPENED, + DEVICE_STATE_HALF_OPENED, + DEVICE_STATE_CLOSED + ); + } + + @Test + public void test_hingeAngleOpen_screenOff_reportsHalfFolded() { + sendHingeAngle(0f); + setScreenOn(false); + mProvider.setListener(mListener); + + sendHingeAngle(10f); + + verify(mListener, atLeastOnce()).onStateChanged(mDeviceStateCaptor.capture()); + assertThat(mDeviceStateCaptor.getAllValues()).containsExactly( + DEVICE_STATE_CLOSED, + DEVICE_STATE_HALF_OPENED + ); + } + + @Test + public void test_slowUnfoldingWithScreenOff_reportsEventsInOrder() { + sendHingeAngle(0f); + setScreenOn(false); + mProvider.setListener(mListener); + + sendHingeAngle(5f); + assertLatestReportedState(DEVICE_STATE_HALF_OPENED); + sendHingeAngle(10f); + assertLatestReportedState(DEVICE_STATE_HALF_OPENED); + sendHingeAngle(60f); + assertLatestReportedState(DEVICE_STATE_HALF_OPENED); + sendHingeAngle(100f); + sendHingeAngle(180f); + assertLatestReportedState(DEVICE_STATE_OPENED); + + verify(mListener, atLeastOnce()).onStateChanged(mDeviceStateCaptor.capture()); + assertThat(mDeviceStateCaptor.getAllValues()).containsExactly( + DEVICE_STATE_CLOSED, + DEVICE_STATE_HALF_OPENED, + DEVICE_STATE_OPENED + ); + } + + @Test + public void test_unfoldWithScreenOff_reportsHalfOpened() { + sendHingeAngle(0f); + setScreenOn(false); + mProvider.setListener(mListener); + + sendHingeAngle(5f); + sendHingeAngle(10f); + + verify(mListener, atLeastOnce()).onStateChanged(mDeviceStateCaptor.capture()); + assertThat(mDeviceStateCaptor.getAllValues()).containsExactly( + DEVICE_STATE_CLOSED, + DEVICE_STATE_HALF_OPENED + ); + } + + @Test + public void test_slowUnfoldingAndFolding_reportsEventsInOrder() { + sendHingeAngle(0f); + mProvider.setListener(mListener); + assertLatestReportedState(DEVICE_STATE_CLOSED); + + // Started unfolding + sendHingeAngle(5f); + sendHingeAngle(30f); + assertLatestReportedState(DEVICE_STATE_HALF_OPENED); + sendHingeAngle(60f); + assertLatestReportedState(DEVICE_STATE_HALF_OPENED); + sendHingeAngle(100f); + assertLatestReportedState(DEVICE_STATE_HALF_OPENED); + sendHingeAngle(180f); + assertLatestReportedState(DEVICE_STATE_OPENED); + + // Started folding + sendHingeAngle(100f); + assertLatestReportedState(DEVICE_STATE_HALF_OPENED); + sendHingeAngle(60f); + assertLatestReportedState(DEVICE_STATE_HALF_OPENED); + sendHingeAngle(30f); + assertLatestReportedState(DEVICE_STATE_CLOSED); + sendHingeAngle(5f); + assertLatestReportedState(DEVICE_STATE_CLOSED); + + verify(mListener, atLeastOnce()).onStateChanged(mDeviceStateCaptor.capture()); + assertThat(mDeviceStateCaptor.getAllValues()).containsExactly( + DEVICE_STATE_CLOSED, + DEVICE_STATE_HALF_OPENED, + DEVICE_STATE_OPENED, + DEVICE_STATE_HALF_OPENED, + DEVICE_STATE_CLOSED + ); + } + + @Test + public void test_unfoldTo30Degrees_screenOnRightSideMostlyFlat_keepsClosedState() { + sendHingeAngle(0f); + sendRightSideFlatSensorEvent(true); + mProvider.setListener(mListener); + assertLatestReportedState(DEVICE_STATE_CLOSED); + clearInvocations(mListener); + + sendHingeAngle(30f); + + verify(mListener, never()).onStateChanged(mDeviceStateCaptor.capture()); + } + + @Test + public void test_unfoldTo30Degrees_seascapeDeviceOrientation_keepsClosedState() { + sendHingeAngle(0f); + sendRightSideFlatSensorEvent(false); + sendDeviceOrientation(Surface.ROTATION_270); + mProvider.setListener(mListener); + assertLatestReportedState(DEVICE_STATE_CLOSED); + clearInvocations(mListener); + + sendHingeAngle(30f); + + verify(mListener, never()).onStateChanged(mDeviceStateCaptor.capture()); + } + + @Test + public void test_unfoldTo30Degrees_landscapeScreenRotation_keepsClosedState() { + sendHingeAngle(0f); + sendRightSideFlatSensorEvent(false); + sendScreenRotation(Surface.ROTATION_90); + mProvider.setListener(mListener); + assertLatestReportedState(DEVICE_STATE_CLOSED); + clearInvocations(mListener); + + sendHingeAngle(30f); + + verify(mListener, never()).onStateChanged(mDeviceStateCaptor.capture()); + } + + @Test + public void test_unfoldTo30Degrees_seascapeScreenRotation_keepsClosedState() { + sendHingeAngle(0f); + sendRightSideFlatSensorEvent(false); + sendScreenRotation(Surface.ROTATION_270); + mProvider.setListener(mListener); + assertLatestReportedState(DEVICE_STATE_CLOSED); + clearInvocations(mListener); + + sendHingeAngle(30f); + + verify(mListener, never()).onStateChanged(mDeviceStateCaptor.capture()); + } + + @Test + public void test_unfoldTo30Degrees_screenOnRightSideNotFlat_switchesToHalfOpenState() { + sendHingeAngle(0f); + sendRightSideFlatSensorEvent(false); + mProvider.setListener(mListener); + assertLatestReportedState(DEVICE_STATE_CLOSED); + clearInvocations(mListener); + + sendHingeAngle(30f); + + verify(mListener).onStateChanged(DEVICE_STATE_HALF_OPENED); + } + + @Test + public void test_unfoldTo30Degrees_screenOffRightSideFlat_switchesToHalfOpenState() { + sendHingeAngle(0f); + setScreenOn(false); + // This sensor event should be ignored as screen is off + sendRightSideFlatSensorEvent(true); + mProvider.setListener(mListener); + assertLatestReportedState(DEVICE_STATE_CLOSED); + clearInvocations(mListener); + + sendHingeAngle(30f); + + verify(mListener).onStateChanged(DEVICE_STATE_HALF_OPENED); + } + + @Test + public void test_unfoldTo60Degrees_andFoldTo10_switchesToClosedState() { + sendHingeAngle(0f); + sendRightSideFlatSensorEvent(false); + mProvider.setListener(mListener); + assertLatestReportedState(DEVICE_STATE_CLOSED); + sendHingeAngle(60f); + assertLatestReportedState(DEVICE_STATE_HALF_OPENED); + clearInvocations(mListener); + + sendHingeAngle(10f); + + verify(mListener).onStateChanged(DEVICE_STATE_CLOSED); + } + + @Test + public void test_foldTo10AndUnfoldTo85Degrees_keepsClosedState() { + sendHingeAngle(0f); + sendRightSideFlatSensorEvent(false); + mProvider.setListener(mListener); + assertLatestReportedState(DEVICE_STATE_CLOSED); + sendHingeAngle(180f); + assertLatestReportedState(DEVICE_STATE_OPENED); + sendHingeAngle(10f); + assertLatestReportedState(DEVICE_STATE_CLOSED); + + sendHingeAngle(85f); + + // Keeps 'tent'/'wedge' mode even when right side is not flat + // as user manually folded the device not all the way + assertLatestReportedState(DEVICE_STATE_CLOSED); + } + + @Test + public void test_foldTo0AndUnfoldTo85Degrees_doesNotKeepClosedState() { + sendHingeAngle(0f); + sendRightSideFlatSensorEvent(false); + mProvider.setListener(mListener); + assertLatestReportedState(DEVICE_STATE_CLOSED); + sendHingeAngle(180f); + assertLatestReportedState(DEVICE_STATE_OPENED); + sendHingeAngle(0f); + assertLatestReportedState(DEVICE_STATE_CLOSED); + + sendHingeAngle(85f); + + // Do not enter 'tent'/'wedge' mode when right side is not flat + // as user fully folded the device before that + assertLatestReportedState(DEVICE_STATE_HALF_OPENED); + } + + @Test + public void test_foldTo10_leftSideIsFlat_keepsInnerScreenForReverseWedge() { + sendHingeAngle(180f); + sendLeftSideFlatSensorEvent(true); + mProvider.setListener(mListener); + assertLatestReportedState(DEVICE_STATE_OPENED); + + sendHingeAngle(10f); + + // Keep the inner screen for reverse wedge mode (e.g. for astrophotography use case) + assertLatestReportedState(DEVICE_STATE_HALF_OPENED); + } + + @Test + public void test_foldTo10_leftSideIsNotFlat_switchesToOuterScreen() { + sendHingeAngle(180f); + sendLeftSideFlatSensorEvent(false); + mProvider.setListener(mListener); + assertLatestReportedState(DEVICE_STATE_OPENED); + + sendHingeAngle(10f); + + // Do not keep the inner screen as it is not reverse wedge mode + assertLatestReportedState(DEVICE_STATE_CLOSED); + } + + @Test + public void test_foldTo10_noAccelerometerEvents_switchesToOuterScreen() { + sendHingeAngle(180f); + mProvider.setListener(mListener); + assertLatestReportedState(DEVICE_STATE_OPENED); + + sendHingeAngle(10f); + + // Do not keep the inner screen as it is not reverse wedge mode + assertLatestReportedState(DEVICE_STATE_CLOSED); + } + + @Test + public void test_deviceClosed_screenIsOff_noSensorListeners() { + mProvider.setListener(mListener); + + sendHingeAngle(0f); + setScreenOn(false); + + assertNoListenersForSensor(mLeftAccelerometer); + assertNoListenersForSensor(mRightAccelerometer); + assertNoListenersForSensor(mOrientationSensor); + } + + @Test + public void test_deviceClosed_screenIsOn_doesNotListenForOneAccelerometer() { + mProvider.setListener(mListener); + + sendHingeAngle(0f); + setScreenOn(true); + + assertNoListenersForSensor(mLeftAccelerometer); + assertListensForSensor(mRightAccelerometer); + assertListensForSensor(mOrientationSensor); + } + + @Test + public void test_deviceOpened_screenIsOn_listensToSensors() { + mProvider.setListener(mListener); + + sendHingeAngle(180f); + setScreenOn(true); + + assertListensForSensor(mLeftAccelerometer); + assertListensForSensor(mRightAccelerometer); + assertListensForSensor(mOrientationSensor); + } + + private void assertLatestReportedState(int state) { + final ArgumentCaptor<Integer> integerCaptor = ArgumentCaptor.forClass(Integer.class); + verify(mListener, atLeastOnce()).onStateChanged(integerCaptor.capture()); + assertEquals(state, integerCaptor.getValue().intValue()); + } + + private void sendHingeAngle(float angle) { + sendSensorEvent(mHingeAngleSensor, new float[]{angle}); + } + + private void sendDeviceOrientation(int orientation) { + sendSensorEvent(mOrientationSensor, new float[]{orientation}); + } + + private void sendScreenRotation(int rotation) { + when(mDisplay.getRotation()).thenReturn(rotation); + mDisplayListenerCaptor.getAllValues().forEach((l) -> l.onDisplayChanged(DEFAULT_DISPLAY)); + } + + private void sendRightSideFlatSensorEvent(boolean flat) { + sendAccelerometerFlatEvents(mRightAccelerometer, flat); + } + + private void sendLeftSideFlatSensorEvent(boolean flat) { + sendAccelerometerFlatEvents(mLeftAccelerometer, flat); + } + + private static final int ACCELEROMETER_EVENTS = 10; + + private void sendAccelerometerFlatEvents(Sensor sensor, boolean flat) { + final float[] values = flat ? new float[]{0.00021f, -0.00013f, 9.7899f} : + new float[]{6.124f, 4.411f, -1.7899f}; + // Send the same values multiple times to bypass noise filter + for (int i = 0; i < ACCELEROMETER_EVENTS; i++) { + sendSensorEvent(sensor, values); + } + } + + private void setScreenOn(boolean isOn) { + int state = isOn ? STATE_ON : STATE_OFF; + when(mDisplay.getState()).thenReturn(state); + mDisplayListenerCaptor.getAllValues().forEach((l) -> l.onDisplayChanged(DEFAULT_DISPLAY)); + } + + private void sendSensorEvent(Sensor sensor, float[] values) { + SensorEvent event = mock(SensorEvent.class); + event.sensor = sensor; + try { + FieldSetter.setField(event, event.getClass().getField("values"), + values); + } catch (NoSuchFieldException e) { + throw new RuntimeException(e); + } + + List<SensorEventListener> listeners = mSensorEventListeners.get(sensor); + if (listeners != null) { + listeners.forEach(sensorEventListener -> sensorEventListener.onSensorChanged(event)); + } + } + + private void assertNoListenersForSensor(Sensor sensor) { + final List<SensorEventListener> listeners = mSensorEventListeners.getOrDefault(sensor, + new ArrayList<>()); + assertWithMessage("Expected no listeners for sensor " + sensor + " but found some").that( + listeners).isEmpty(); + } + + private void assertListensForSensor(Sensor sensor) { + final List<SensorEventListener> listeners = mSensorEventListeners.getOrDefault(sensor, + new ArrayList<>()); + assertWithMessage( + "Expected at least one listener for sensor " + sensor).that( + listeners).isNotEmpty(); + } + + private void addSensorListener(Sensor sensor, SensorEventListener listener) { + List<SensorEventListener> listeners = mSensorEventListeners.computeIfAbsent( + sensor, k -> new ArrayList<>()); + listeners.add(listener); + } + + private DeviceStateProvider createProvider() { + return new BookStyleDeviceStatePolicy(mFakeFeatureFlags, mContext, mHingeAngleSensor, + mHallSensor, mLeftAccelerometer, mRightAccelerometer, + /* closeAngleDegrees= */ null).getDeviceStateProvider(); + } +} diff --git a/services/foldables/devicestateprovider/tests/src/com/android/server/policy/BookStylePreferredScreenCalculatorTest.java b/services/foldables/devicestateprovider/tests/src/com/android/server/policy/BookStylePreferredScreenCalculatorTest.java new file mode 100644 index 000000000000..ae05b3f5c121 --- /dev/null +++ b/services/foldables/devicestateprovider/tests/src/com/android/server/policy/BookStylePreferredScreenCalculatorTest.java @@ -0,0 +1,81 @@ +/* + * 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.policy; + + +import static com.android.server.policy.BookStyleStateTransitions.DEFAULT_STATE_TRANSITIONS; + +import static com.google.common.truth.Truth.assertWithMessage; + +import android.testing.AndroidTestingRunner; + +import com.android.server.policy.BookStylePreferredScreenCalculator.PreferredScreen; +import com.android.server.policy.BookStylePreferredScreenCalculator.HingeAngle; + +import com.google.common.collect.Lists; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.Arrays; +import java.util.List; + +/** + * Unit tests for {@link BookStylePreferredScreenCalculator}. + * <p/> + * Run with <code>atest BookStyleClosedStateCalculatorTest</code>. + */ +@RunWith(AndroidTestingRunner.class) +public final class BookStylePreferredScreenCalculatorTest { + + private final BookStylePreferredScreenCalculator mCalculator = + new BookStylePreferredScreenCalculator(DEFAULT_STATE_TRANSITIONS); + + private final List<HingeAngle> mHingeAngleValues = Arrays.asList(HingeAngle.values()); + private final List<Boolean> mLikelyTentModeValues = Arrays.asList(true, false); + private final List<Boolean> mLikelyReverseWedgeModeValues = Arrays.asList(true, false); + + @Test + public void transitionAllStates_noCrashes() { + final List<List<Object>> arguments = Lists.cartesianProduct(Arrays.asList( + mHingeAngleValues, + mLikelyTentModeValues, + mLikelyReverseWedgeModeValues + )); + + arguments.forEach(objects -> { + final HingeAngle hingeAngle = (HingeAngle) objects.get(0); + final boolean likelyTent = (boolean) objects.get(1); + final boolean likelyReverseWedge = (boolean) objects.get(2); + + final String description = + "Input: hinge angle = " + hingeAngle + ", likelyTent = " + likelyTent + + ", likelyReverseWedge = " + likelyReverseWedge; + + // Verify that there are no crashes because of infinite state transitions and + // that it returns a valid active state + try { + PreferredScreen preferredScreen = mCalculator.calculatePreferredScreen(hingeAngle, likelyTent, + likelyReverseWedge); + + assertWithMessage(description).that(preferredScreen).isNotEqualTo(PreferredScreen.INVALID); + } catch (Throwable exception) { + throw new AssertionError(description, exception); + } + }); + } +} diff --git a/services/permission/java/com/android/server/permission/access/appop/AppIdAppOpPolicy.kt b/services/permission/java/com/android/server/permission/access/appop/AppIdAppOpPolicy.kt index 94caf2865b66..c8a65459d3df 100644 --- a/services/permission/java/com/android/server/permission/access/appop/AppIdAppOpPolicy.kt +++ b/services/permission/java/com/android/server/permission/access/appop/AppIdAppOpPolicy.kt @@ -17,6 +17,7 @@ package com.android.server.permission.access.appop import android.app.AppOpsManager +import android.util.Slog import com.android.server.permission.access.GetStateScope import com.android.server.permission.access.MutableAccessState import com.android.server.permission.access.MutateStateScope @@ -84,6 +85,10 @@ class AppIdAppOpPolicy : BaseAppOpPolicy(AppIdAppOpPersistence()) { appOpName: String, mode: Int ): Boolean { + if (userId !in newState.userStates) { + Slog.e(LOG_TAG, "Unable to set app op mode for missing user $userId") + return false + } val defaultMode = AppOpsManager.opToDefaultMode(appOpName) val oldMode = newState.userStates[userId]!! @@ -152,4 +157,8 @@ class AppIdAppOpPolicy : BaseAppOpPolicy(AppIdAppOpPersistence()) { */ abstract fun onStateMutated() } + + companion object { + private val LOG_TAG = AppIdAppOpPolicy::class.java.simpleName + } } diff --git a/services/permission/java/com/android/server/permission/access/appop/AppOpService.kt b/services/permission/java/com/android/server/permission/access/appop/AppOpService.kt index 8f464d41792d..3ee7430fc486 100644 --- a/services/permission/java/com/android/server/permission/access/appop/AppOpService.kt +++ b/services/permission/java/com/android/server/permission/access/appop/AppOpService.kt @@ -17,29 +17,62 @@ package com.android.server.permission.access.appop import android.app.AppOpsManager +import android.os.Binder import android.os.Handler import android.os.UserHandle +import android.permission.flags.Flags import android.util.ArrayMap import android.util.ArraySet +import android.util.LongSparseArray +import android.util.Slog +import android.util.SparseArray import android.util.SparseBooleanArray import android.util.SparseIntArray import com.android.internal.annotations.VisibleForTesting +import com.android.internal.util.IntPair import com.android.server.appop.AppOpsCheckingServiceInterface import com.android.server.appop.AppOpsCheckingServiceInterface.AppOpsModeChangedListener import com.android.server.permission.access.AccessCheckingService import com.android.server.permission.access.AppOpUri +import com.android.server.permission.access.GetStateScope import com.android.server.permission.access.PackageUri +import com.android.server.permission.access.PermissionUri import com.android.server.permission.access.UidUri +import com.android.server.permission.access.appop.AppOpModes.MODE_ALLOWED +import com.android.server.permission.access.appop.AppOpModes.MODE_FOREGROUND +import com.android.server.permission.access.appop.AppOpModes.MODE_IGNORED import com.android.server.permission.access.collection.forEachIndexed import com.android.server.permission.access.collection.set +import com.android.server.permission.access.permission.AppIdPermissionPolicy +import com.android.server.permission.access.permission.PermissionFlags +import com.android.server.permission.access.permission.PermissionService class AppOpService(private val service: AccessCheckingService) : AppOpsCheckingServiceInterface { private val packagePolicy = service.getSchemePolicy(PackageUri.SCHEME, AppOpUri.SCHEME) as PackageAppOpPolicy private val appIdPolicy = service.getSchemePolicy(UidUri.SCHEME, AppOpUri.SCHEME) as AppIdAppOpPolicy + private val permissionPolicy = + service.getSchemePolicy(UidUri.SCHEME, PermissionUri.SCHEME) as AppIdPermissionPolicy private val context = service.context + + // Maps appop code to its runtime permission + private val runtimeAppOpToPermissionNames = SparseArray<String>() + + // Maps runtime permission to its appop codes + private val runtimePermissionNameToAppOp = ArrayMap<String, Int>() + + private var foregroundableOps = SparseBooleanArray() + + /* Maps foreground permissions to their background permission. Background permissions aren't + required to be runtime */ + private val foregroundToBackgroundPermissionName = ArrayMap<String, String>() + + /* Maps background permissions to their foreground permissions. Background permissions aren't + required to be runtime */ + private val backgroundToForegroundPermissionNames = ArrayMap<String, ArraySet<String>>() + private lateinit var handler: Handler @Volatile private var listeners = ArraySet<AppOpsModeChangedListener>() @@ -68,11 +101,58 @@ class AppOpService(private val service: AccessCheckingService) : AppOpsCheckingS } override fun systemReady() { - // Not implemented because upgrades are handled automatically. + if (useRuntimePermissionAppOpMapping()) { + createPermissionAppOpMapping() + permissionPolicy.addOnPermissionFlagsChangedListener(OnPermissionFlagsChangedListener()) + } + } + + private fun createPermissionAppOpMapping() { + val permissions = service.getState { with(permissionPolicy) { getPermissions() } } + + for (appOpCode in 0 until AppOpsManager._NUM_OP) { + AppOpsManager.opToPermission(appOpCode)?.let { permissionName -> + // Multiple ops might map to a single permission but only one is considered the + // runtime appop calculations. + if (appOpCode == AppOpsManager.permissionToOpCode(permissionName)) { + val permission = permissions[permissionName]!! + if (permission.isRuntime) { + runtimePermissionNameToAppOp[permissionName] = appOpCode + runtimeAppOpToPermissionNames[appOpCode] = permissionName + permission.permissionInfo.backgroundPermission?.let { + backgroundPermissionName -> + // Note: background permission may not be runtime, + // e.g. microphone/camera. + foregroundableOps[appOpCode] = true + foregroundToBackgroundPermissionName[permissionName] = + backgroundPermissionName + backgroundToForegroundPermissionNames + .getOrPut(backgroundPermissionName, ::ArraySet) + .add(permissionName) + } + } + } + } + } } override fun getNonDefaultUidModes(uid: Int, persistentDeviceId: String): SparseIntArray { - return opNameMapToOpSparseArray(getUidModes(uid)) + val appId = UserHandle.getAppId(uid) + val userId = UserHandle.getUserId(uid) + service.getState { + val modes = + with(appIdPolicy) { opNameMapToOpSparseArray(getAppOpModes(appId, userId)?.map) } + if (useRuntimePermissionAppOpMapping()) { + runtimePermissionNameToAppOp.forEachIndexed { _, permissionName, appOpCode -> + val mode = getUidModeFromPermissionState(appId, userId, permissionName) + if (mode != AppOpsManager.opToDefaultMode(appOpCode)) { + modes[appOpCode] = mode + } + } + } + + return modes + } } override fun getNonDefaultPackageModes(packageName: String, userId: Int): SparseIntArray { @@ -83,7 +163,13 @@ class AppOpService(private val service: AccessCheckingService) : AppOpsCheckingS val appId = UserHandle.getAppId(uid) val userId = UserHandle.getUserId(uid) val opName = AppOpsManager.opToPublicName(op) - return service.getState { with(appIdPolicy) { getAppOpMode(appId, userId, opName) } } + val permissionName = runtimeAppOpToPermissionNames[op] + + return if (!useRuntimePermissionAppOpMapping() || permissionName == null) { + service.getState { with(appIdPolicy) { getAppOpMode(appId, userId, opName) } } + } else { + service.getState { getUidModeFromPermissionState(appId, userId, permissionName) } + } } private fun getUidModes(uid: Int): ArrayMap<String, Int>? { @@ -92,13 +178,63 @@ class AppOpService(private val service: AccessCheckingService) : AppOpsCheckingS return service.getState { with(appIdPolicy) { getAppOpModes(appId, userId) } }?.map } - override fun setUidMode(uid: Int, persistentDeviceId: String, op: Int, mode: Int): Boolean { + private fun GetStateScope.getUidModeFromPermissionState( + appId: Int, + userId: Int, + permissionName: String + ): Int { + val permissionFlags = + with(permissionPolicy) { getPermissionFlags(appId, userId, permissionName) } + val backgroundPermissionName = foregroundToBackgroundPermissionName[permissionName] + val backgroundPermissionFlags = + if (backgroundPermissionName != null) { + with(permissionPolicy) { + getPermissionFlags(appId, userId, backgroundPermissionName) + } + } else { + PermissionFlags.RUNTIME_GRANTED + } + val result = evaluateModeFromPermissionFlags(permissionFlags, backgroundPermissionFlags) + if (result != MODE_IGNORED) { + return result + } + + val fullerPermissionName = + PermissionService.getFullerPermission(permissionName) ?: return result + return getUidModeFromPermissionState(appId, userId, fullerPermissionName) + } + + private fun evaluateModeFromPermissionFlags( + foregroundFlags: Int, + backgroundFlags: Int = PermissionFlags.RUNTIME_GRANTED + ): Int = + if (PermissionFlags.isAppOpGranted(foregroundFlags)) { + if (PermissionFlags.isAppOpGranted(backgroundFlags)) { + MODE_ALLOWED + } else { + MODE_FOREGROUND + } + } else { + MODE_IGNORED + } + + override fun setUidMode(uid: Int, persistentDeviceId: String, code: Int, mode: Int): Boolean { + if (useRuntimePermissionAppOpMapping() && code in runtimeAppOpToPermissionNames) { + Slog.w( + LOG_TAG, + "Cannot set UID mode for runtime permission app op, uid = $uid," + + " code = ${AppOpsManager.opToName(code)}, mode = ${AppOpsManager.modeToName(mode)}", + RuntimeException() + ) + return false + } + val appId = UserHandle.getAppId(uid) val userId = UserHandle.getUserId(uid) - val opName = AppOpsManager.opToPublicName(op) - var wasChanged = false + val appOpName = AppOpsManager.opToPublicName(code) + var wasChanged: Boolean service.mutateState { - wasChanged = with(appIdPolicy) { setAppOpMode(appId, userId, opName, mode) } + wasChanged = with(appIdPolicy) { setAppOpMode(appId, userId, appOpName, mode) } } return wasChanged } @@ -113,10 +249,22 @@ class AppOpService(private val service: AccessCheckingService) : AppOpsCheckingS private fun getPackageModes(packageName: String, userId: Int): ArrayMap<String, Int>? = service.getState { with(packagePolicy) { getAppOpModes(packageName, userId) } }?.map - override fun setPackageMode(packageName: String, op: Int, mode: Int, userId: Int) { - val opName = AppOpsManager.opToPublicName(op) + override fun setPackageMode(packageName: String, appOpCode: Int, mode: Int, userId: Int) { + val appOpName = AppOpsManager.opToPublicName(appOpCode) + + if ( + useRuntimePermissionAppOpMapping() && runtimeAppOpToPermissionNames.contains(appOpCode) + ) { + Slog.w( + LOG_TAG, + "(packageName=$packageName, userId=$userId)'s appop state" + + " for runtime op $appOpName should not be set directly.", + RuntimeException() + ) + return + } service.mutateState { - with(packagePolicy) { setAppOpMode(packageName, userId, opName, mode) } + with(packagePolicy) { setAppOpMode(packageName, userId, appOpName, mode) } } } @@ -127,7 +275,7 @@ class AppOpService(private val service: AccessCheckingService) : AppOpsCheckingS } override fun removePackage(packageName: String, userId: Int): Boolean { - var wasChanged = false + var wasChanged: Boolean service.mutateState { wasChanged = with(packagePolicy) { removeAppOpModes(packageName, userId) } } @@ -157,6 +305,13 @@ class AppOpService(private val service: AccessCheckingService) : AppOpsCheckingS this[AppOpsManager.strOpToOp(op)] = true } } + if (useRuntimePermissionAppOpMapping()) { + foregroundableOps.forEachIndexed { _, op, _ -> + if (getUidMode(uid, persistentDeviceId, op) == AppOpsManager.MODE_FOREGROUND) { + this[op] = true + } + } + } } } @@ -167,6 +322,13 @@ class AppOpService(private val service: AccessCheckingService) : AppOpsCheckingS this[AppOpsManager.strOpToOp(op)] = true } } + if (useRuntimePermissionAppOpMapping()) { + foregroundableOps.forEachIndexed { _, op, _ -> + if (getPackageMode(packageName, op, userId) == AppOpsManager.MODE_FOREGROUND) { + this[op] = true + } + } + } } } @@ -188,9 +350,10 @@ class AppOpService(private val service: AccessCheckingService) : AppOpsCheckingS } } - inner class OnAppIdAppOpModeChangedListener : AppIdAppOpPolicy.OnAppOpModeChangedListener() { + private inner class OnAppIdAppOpModeChangedListener : + AppIdAppOpPolicy.OnAppOpModeChangedListener() { // (uid, appOpCode) -> newMode - val pendingChanges = ArrayMap<Pair<Int, Int>, Int>() + private val pendingChanges = LongSparseArray<Int>() override fun onAppOpModeChanged( appId: Int, @@ -201,7 +364,7 @@ class AppOpService(private val service: AccessCheckingService) : AppOpsCheckingS ) { val uid = UserHandle.getUid(userId, appId) val appOpCode = AppOpsManager.strOpToOp(appOpName) - val key = Pair(uid, appOpCode) + val key = IntPair.of(uid, appOpCode) pendingChanges[key] = newMode } @@ -210,8 +373,8 @@ class AppOpService(private val service: AccessCheckingService) : AppOpsCheckingS val listenersLocal = listeners pendingChanges.forEachIndexed { _, key, mode -> listenersLocal.forEachIndexed { _, listener -> - val uid = key.first - val appOpCode = key.second + val uid = IntPair.first(key) + val appOpCode = IntPair.second(key) listener.onUidModeChanged(uid, appOpCode, mode) } @@ -224,7 +387,7 @@ class AppOpService(private val service: AccessCheckingService) : AppOpsCheckingS private inner class OnPackageAppOpModeChangedListener : PackageAppOpPolicy.OnAppOpModeChangedListener() { // (packageName, userId, appOpCode) -> newMode - val pendingChanges = ArrayMap<Triple<String, Int, Int>, Int>() + private val pendingChanges = ArrayMap<Triple<String, Int, Int>, Int>() override fun onAppOpModeChanged( packageName: String, @@ -254,4 +417,115 @@ class AppOpService(private val service: AccessCheckingService) : AppOpsCheckingS pendingChanges.clear() } } + + private inner class OnPermissionFlagsChangedListener : + AppIdPermissionPolicy.OnPermissionFlagsChangedListener { + // (uid, appOpCode) -> newMode + private val pendingChanges = LongSparseArray<Int>() + + override fun onPermissionFlagsChanged( + appId: Int, + userId: Int, + permissionName: String, + oldFlags: Int, + newFlags: Int + ) { + backgroundToForegroundPermissionNames[permissionName]?.let { foregroundPermissions -> + // This is a background permission; there may be multiple foreground permissions + // affected. + foregroundPermissions.forEachIndexed { _, foregroundPermissionName -> + runtimePermissionNameToAppOp[foregroundPermissionName]?.let { appOpCode -> + val foregroundPermissionFlags = + getPermissionFlags(appId, userId, foregroundPermissionName) + addPendingChangedModeIfNeeded( + appId, + userId, + appOpCode, + foregroundPermissionFlags, + oldFlags, + foregroundPermissionFlags, + newFlags + ) + } + } + } + ?: foregroundToBackgroundPermissionName[permissionName]?.let { backgroundPermission + -> + runtimePermissionNameToAppOp[permissionName]?.let { appOpCode -> + val backgroundPermissionFlags = + getPermissionFlags(appId, userId, backgroundPermission) + addPendingChangedModeIfNeeded( + appId, + userId, + appOpCode, + oldFlags, + backgroundPermissionFlags, + newFlags, + backgroundPermissionFlags + ) + } + } + ?: runtimePermissionNameToAppOp[permissionName]?.let { appOpCode -> + addPendingChangedModeIfNeeded( + appId, + userId, + appOpCode, + oldFlags, + PermissionFlags.RUNTIME_GRANTED, + newFlags, + PermissionFlags.RUNTIME_GRANTED + ) + } + } + + private fun getPermissionFlags(appId: Int, userId: Int, permissionName: String): Int = + service.getState { + with(permissionPolicy) { getPermissionFlags(appId, userId, permissionName) } + } + + private fun addPendingChangedModeIfNeeded( + appId: Int, + userId: Int, + appOpCode: Int, + oldForegroundFlags: Int, + oldBackgroundFlags: Int, + newForegroundFlags: Int, + newBackgroundFlags: Int, + ) { + val oldMode = evaluateModeFromPermissionFlags(oldForegroundFlags, oldBackgroundFlags) + val newMode = evaluateModeFromPermissionFlags(newForegroundFlags, newBackgroundFlags) + + if (oldMode != newMode) { + val uid = UserHandle.getUid(userId, appId) + pendingChanges[IntPair.of(uid, appOpCode)] = newMode + } + } + + override fun onStateMutated() { + val listenersLocal = listeners + pendingChanges.forEachIndexed { _, key, mode -> + listenersLocal.forEachIndexed { _, listener -> + val uid = IntPair.first(key) + val appOpCode = IntPair.second(key) + + listener.onUidModeChanged(uid, appOpCode, mode) + } + } + + pendingChanges.clear() + } + } + + companion object { + private val LOG_TAG = AppOpService::class.java.simpleName + + private fun useRuntimePermissionAppOpMapping(): Boolean { + val token = Binder.clearCallingIdentity() + return try { + Flags.runtimePermissionAppopsMapping() + } finally { + Binder.restoreCallingIdentity(token) + } + } + } } diff --git a/services/permission/java/com/android/server/permission/access/appop/PackageAppOpPolicy.kt b/services/permission/java/com/android/server/permission/access/appop/PackageAppOpPolicy.kt index 0d9470edc4ea..2f15dc7b232a 100644 --- a/services/permission/java/com/android/server/permission/access/appop/PackageAppOpPolicy.kt +++ b/services/permission/java/com/android/server/permission/access/appop/PackageAppOpPolicy.kt @@ -17,6 +17,7 @@ package com.android.server.permission.access.appop import android.app.AppOpsManager +import android.util.Slog import com.android.server.permission.access.GetStateScope import com.android.server.permission.access.MutableAccessState import com.android.server.permission.access.MutateStateScope @@ -87,6 +88,10 @@ class PackageAppOpPolicy : BaseAppOpPolicy(PackageAppOpPersistence()) { appOpName: String, mode: Int ): Boolean { + if (userId !in newState.userStates) { + Slog.e(LOG_TAG, "Unable to set app op mode for missing user $userId") + return false + } val defaultMode = AppOpsManager.opToDefaultMode(appOpName) val oldMode = newState.userStates[userId]!! @@ -155,4 +160,8 @@ class PackageAppOpPolicy : BaseAppOpPolicy(PackageAppOpPersistence()) { */ abstract fun onStateMutated() } + + companion object { + private val LOG_TAG = PackageAppOpPolicy::class.java.simpleName + } } diff --git a/services/permission/java/com/android/server/permission/access/collection/LongSparseArrayExtensions.kt b/services/permission/java/com/android/server/permission/access/collection/LongSparseArrayExtensions.kt new file mode 100644 index 000000000000..827dd0e5d292 --- /dev/null +++ b/services/permission/java/com/android/server/permission/access/collection/LongSparseArrayExtensions.kt @@ -0,0 +1,105 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.permission.access.collection + +import android.util.LongSparseArray + +inline fun <T> LongSparseArray<T>.allIndexed(predicate: (Int, Long, T) -> Boolean): Boolean { + forEachIndexed { index, key, value -> + if (!predicate(index, key, value)) { + return false + } + } + return true +} + +inline fun <T> LongSparseArray<T>.anyIndexed(predicate: (Int, Long, T) -> Boolean): Boolean { + forEachIndexed { index, key, value -> + if (predicate(index, key, value)) { + return true + } + } + return false +} + +inline fun <T> LongSparseArray<T>.forEachIndexed(action: (Int, Long, T) -> Unit) { + for (index in 0 until size) { + action(index, keyAt(index), valueAt(index)) + } +} + +inline fun <T> LongSparseArray<T>.forEachReversedIndexed(action: (Int, Long, T) -> Unit) { + for (index in lastIndex downTo 0) { + action(index, keyAt(index), valueAt(index)) + } +} + +inline fun <T> LongSparseArray<T>.getOrPut(key: Long, defaultValue: () -> T): T { + val index = indexOfKey(key) + return if (index >= 0) { + valueAt(index) + } else { + defaultValue().also { put(key, it) } + } +} + +inline val <T> LongSparseArray<T>.lastIndex: Int + get() = size - 1 + +@Suppress("NOTHING_TO_INLINE") +inline operator fun <T> LongSparseArray<T>.minusAssign(key: Long) { + delete(key) +} + +inline fun <T> LongSparseArray<T>.noneIndexed(predicate: (Int, Long, T) -> Boolean): Boolean { + forEachIndexed { index, key, value -> + if (predicate(index, key, value)) { + return false + } + } + return true +} + +inline fun <T> LongSparseArray<T>.removeAllIndexed(predicate: (Int, Long, T) -> Boolean): Boolean { + var isChanged = false + forEachReversedIndexed { index, key, value -> + if (predicate(index, key, value)) { + removeAt(index) + isChanged = true + } + } + return isChanged +} + +inline fun <T> LongSparseArray<T>.retainAllIndexed(predicate: (Int, Long, T) -> Boolean): Boolean { + var isChanged = false + forEachReversedIndexed { index, key, value -> + if (!predicate(index, key, value)) { + removeAt(index) + isChanged = true + } + } + return isChanged +} + +inline val <T> LongSparseArray<T>.size: Int + get() = size() + +@Suppress("NOTHING_TO_INLINE") +inline operator fun <T> LongSparseArray<T>.set(key: Long, value: T) { + put(key, value) +} diff --git a/services/permission/java/com/android/server/permission/access/collection/SparseIntArrayExtensions.kt b/services/permission/java/com/android/server/permission/access/collection/SparseIntArrayExtensions.kt new file mode 100644 index 000000000000..a582431aa83c --- /dev/null +++ b/services/permission/java/com/android/server/permission/access/collection/SparseIntArrayExtensions.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.server.permission.access.collection + +import android.util.SparseIntArray + +inline fun SparseIntArray.allIndexed(predicate: (Int, Int, Int) -> Boolean): Boolean { + forEachIndexed { index, key, value -> + if (!predicate(index, key, value)) { + return false + } + } + return true +} + +inline fun SparseIntArray.anyIndexed(predicate: (Int, Int, Int) -> Boolean): Boolean { + forEachIndexed { index, key, value -> + if (predicate(index, key, value)) { + return true + } + } + return false +} + +inline fun SparseIntArray.forEachIndexed(action: (Int, Int, Int) -> Unit) { + for (index in 0 until size) { + action(index, keyAt(index), valueAt(index)) + } +} + +inline fun SparseIntArray.forEachReversedIndexed(action: (Int, Int, Int) -> Unit) { + for (index in lastIndex downTo 0) { + action(index, keyAt(index), valueAt(index)) + } +} + +inline fun SparseIntArray.getOrPut(key: Int, defaultValue: () -> Int): Int { + val index = indexOfKey(key) + return if (index >= 0) { + valueAt(index) + } else { + defaultValue().also { put(key, it) } + } +} + +inline val SparseIntArray.lastIndex: Int + get() = size - 1 + +@Suppress("NOTHING_TO_INLINE") +inline operator fun SparseIntArray.minusAssign(key: Int) { + delete(key) +} + +inline fun SparseIntArray.noneIndexed(predicate: (Int, Int, Int) -> Boolean): Boolean { + forEachIndexed { index, key, value -> + if (predicate(index, key, value)) { + return false + } + } + return true +} + +fun SparseIntArray.remove(key: Int) { + delete(key) +} + +fun SparseIntArray.remove(key: Int, defaultValue: Int): Int { + val index = indexOfKey(key) + return if (index >= 0) { + val oldValue = valueAt(index) + removeAt(index) + oldValue + } else { + defaultValue + } +} + +inline fun SparseIntArray.removeAllIndexed(predicate: (Int, Int, Int) -> Boolean): Boolean { + var isChanged = false + forEachReversedIndexed { index, key, value -> + if (predicate(index, key, value)) { + removeAt(index) + isChanged = true + } + } + return isChanged +} + +inline fun SparseIntArray.retainAllIndexed(predicate: (Int, Int, Int) -> Boolean): Boolean { + var isChanged = false + forEachReversedIndexed { index, key, value -> + if (!predicate(index, key, value)) { + removeAt(index) + isChanged = true + } + } + return isChanged +} + +@Suppress("NOTHING_TO_INLINE") +inline operator fun SparseIntArray.set(key: Int, value: Int) { + put(key, value) +} + +inline val SparseIntArray.size: Int + get() = size() 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 022268df4a63..62d2d7ee848a 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 @@ -134,7 +134,9 @@ class AppIdPermissionPolicy : SchemePolicy() { ) { val changedPermissionNames = MutableIndexedSet<String>() 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 adoptPermissions(packageState, changedPermissionNames) addPermissionGroups(packageState) addPermissions(packageState, changedPermissionNames) @@ -147,12 +149,14 @@ class AppIdPermissionPolicy : SchemePolicy() { } packageNames.forEachIndexed { _, packageName -> - val packageState = newState.externalState.packageStates[packageName]!! + val packageState = newState.externalState.packageStates[packageName] + ?: return@forEachIndexed val installedPackageState = if (isSystemUpdated) packageState else null evaluateAllPermissionStatesForPackage(packageState, installedPackageState) } packageNames.forEachIndexed { _, packageName -> - val packageState = newState.externalState.packageStates[packageName]!! + val packageState = newState.externalState.packageStates[packageName] + ?: return@forEachIndexed newState.externalState.userIds.forEachIndexed { _, userId -> inheritImplicitPermissionStates(packageState.appId, userId) } @@ -1607,6 +1611,13 @@ class AppIdPermissionPolicy : SchemePolicy() { 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]!! .appIdPermissionFlags[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 f469ab547763..b162a1b88b76 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 @@ -2870,5 +2870,8 @@ class PermissionService(private val service: AccessCheckingService) : } else { emptySet<String>() } + + fun getFullerPermission(permissionName: String): String? = + FULLER_PERMISSIONS[permissionName] } } diff --git a/services/profcollect/Android.bp b/services/profcollect/Android.bp index 2040bb6e544f..fe431f5c8867 100644 --- a/services/profcollect/Android.bp +++ b/services/profcollect/Android.bp @@ -22,24 +22,25 @@ package { } filegroup { - name: "services.profcollect-javasources", - srcs: ["src/**/*.java"], - path: "src", - visibility: ["//frameworks/base/services"], + name: "services.profcollect-javasources", + srcs: ["src/**/*.java"], + path: "src", + visibility: ["//frameworks/base/services"], } filegroup { - name: "services.profcollect-sources", - srcs: [ - ":services.profcollect-javasources", - ":profcollectd_aidl", - ], - visibility: ["//frameworks/base/services:__subpackages__"], + name: "services.profcollect-sources", + srcs: [ + ":services.profcollect-javasources", + ":profcollectd_aidl", + ], + visibility: ["//frameworks/base/services:__subpackages__"], } java_library_static { - name: "services.profcollect", - defaults: ["platform_service_defaults"], - srcs: [":services.profcollect-sources"], - libs: ["services.core"], + name: "services.profcollect", + defaults: ["platform_service_defaults"], + srcs: [":services.profcollect-sources"], + static_libs: ["services.core"], + libs: ["service-art.stubs.system_server"], } diff --git a/services/profcollect/src/com/android/server/profcollect/ProfcollectForwardingService.java b/services/profcollect/src/com/android/server/profcollect/ProfcollectForwardingService.java index 4007672a0599..582b712ec3fc 100644 --- a/services/profcollect/src/com/android/server/profcollect/ProfcollectForwardingService.java +++ b/services/profcollect/src/com/android/server/profcollect/ProfcollectForwardingService.java @@ -41,12 +41,15 @@ import android.util.Log; import com.android.internal.R; import com.android.internal.os.BackgroundThread; import com.android.server.IoThread; +import com.android.server.LocalManagerRegistry; import com.android.server.LocalServices; import com.android.server.SystemService; +import com.android.server.art.ArtManagerLocal; import com.android.server.wm.ActivityMetricsLaunchObserver; import com.android.server.wm.ActivityMetricsLaunchObserverRegistry; import com.android.server.wm.ActivityTaskManagerInternal; +import java.util.concurrent.ForkJoinPool; import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.TimeUnit; @@ -261,6 +264,7 @@ public final class ProfcollectForwardingService extends SystemService { BackgroundThread.get().getThreadHandler().post( () -> { registerAppLaunchObserver(); + registerDex2oatObserver(); registerOTAObserver(); }); } @@ -304,6 +308,44 @@ public final class ProfcollectForwardingService extends SystemService { } } + private void registerDex2oatObserver() { + ArtManagerLocal aml = LocalManagerRegistry.getManager(ArtManagerLocal.class); + if (aml == null) { + Log.w(LOG_TAG, "Couldn't get ArtManagerLocal"); + return; + } + aml.setBatchDexoptStartCallback(ForkJoinPool.commonPool(), + (snapshot, reason, defaultPackages, builder, passedSignal) -> { + traceOnDex2oatStart(); + }); + } + + private void traceOnDex2oatStart() { + if (mIProfcollect == null) { + return; + } + // Sample for a fraction of dex2oat runs. + final int traceFrequency = + DeviceConfig.getInt(DeviceConfig.NAMESPACE_PROFCOLLECT_NATIVE_BOOT, + "dex2oat_trace_freq", 10); + int randomNum = ThreadLocalRandom.current().nextInt(100); + if (randomNum < traceFrequency) { + if (DEBUG) { + Log.d(LOG_TAG, "Tracing on dex2oat event"); + } + BackgroundThread.get().getThreadHandler().post(() -> { + try { + // Dex2oat could take a while before it starts. Add a short delay before start + // tracing. + Thread.sleep(1000); + mIProfcollect.trace_once("dex2oat"); + } catch (RemoteException | InterruptedException e) { + Log.e(LOG_TAG, "Failed to initiate trace: " + e.getMessage()); + } + }); + } + } + private void registerOTAObserver() { UpdateEngine updateEngine = new UpdateEngine(); updateEngine.bind(new UpdateEngineCallback() { 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 3c8f5c9578d3..30afa72e0f03 100644 --- a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/ClientControllerTest.java +++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/ClientControllerTest.java @@ -15,9 +15,16 @@ */ 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.eq; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.content.pm.PackageManagerInternal; @@ -38,6 +45,8 @@ import org.junit.Test; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; // This test is designed to run on both device and host (Ravenwood) side. public final class ClientControllerTest { @@ -58,9 +67,6 @@ public final class ClientControllerTest { @Mock private IRemoteInputConnection mConnection; - @Mock - private IBinder.DeathRecipient mDeathRecipient; - private Handler mHandler; private ClientController mController; @@ -68,9 +74,10 @@ public final class ClientControllerTest { @Before public void setUp() { MockitoAnnotations.initMocks(this); + when(mClient.asBinder()).thenReturn((IBinder) mClient); + mHandler = new Handler(Looper.getMainLooper()); mController = new ClientController(mMockPackageManagerInternal); - when(mClient.asBinder()).thenReturn((IBinder) mClient); } @Test @@ -80,18 +87,77 @@ public final class ClientControllerTest { var invoker = IInputMethodClientInvoker.create(mClient, mHandler); synchronized (ImfLock.class) { - mController.addClient(invoker, mConnection, ANY_DISPLAY_ID, mDeathRecipient, - ANY_CALLER_UID, ANY_CALLER_PID); + mController.addClient(invoker, mConnection, ANY_DISPLAY_ID, ANY_CALLER_UID, + ANY_CALLER_PID); SecurityException thrown = assertThrows(SecurityException.class, () -> { synchronized (ImfLock.class) { mController.addClient(invoker, mConnection, ANY_DISPLAY_ID, - mDeathRecipient, ANY_CALLER_UID, ANY_CALLER_PID); + ANY_CALLER_UID, ANY_CALLER_PID); } }); assertThat(thrown.getMessage()).isEqualTo( "uid=1/pid=1/displayId=0 is already registered"); } } + + @Test + // TODO(b/314150112): Enable host side mode for this test once b/315544364 is fixed. + @IgnoreUnderRavenwood(blockedBy = {InputBinding.class, IInputMethodClientInvoker.class}) + public void testAddClient() throws Exception { + synchronized (ImfLock.class) { + var invoker = IInputMethodClientInvoker.create(mClient, mHandler); + var added = mController.addClient(invoker, mConnection, ANY_DISPLAY_ID, ANY_CALLER_UID, + ANY_CALLER_PID); + + verify(invoker.asBinder()).linkToDeath(any(IBinder.DeathRecipient.class), eq(0)); + assertThat(mController.mClients).containsEntry(invoker.asBinder(), added); + } + } + + @Test + // TODO(b/314150112): Enable host side mode for this test once b/315544364 is fixed. + @IgnoreUnderRavenwood(blockedBy = {InputBinding.class, IInputMethodClientInvoker.class}) + public void testRemoveClient() { + var callback = new TestClientControllerCallback(); + ClientState added; + synchronized (ImfLock.class) { + mController.addClientControllerCallback(callback); + + var invoker = IInputMethodClientInvoker.create(mClient, mHandler); + added = mController.addClient(invoker, mConnection, ANY_DISPLAY_ID, ANY_CALLER_UID, + ANY_CALLER_PID); + assertThat(mController.mClients).containsEntry(invoker.asBinder(), added); + assertThat(mController.removeClient(mClient)).isTrue(); + } + + // Test callback + var removed = callback.waitForRemovedClient(5, TimeUnit.SECONDS); + assertThat(removed).isSameInstanceAs(added); + } + + private static class TestClientControllerCallback implements ClientControllerCallback { + + private final CountDownLatch mLatch = new CountDownLatch(1); + + private ClientState mRemoved; + + @Override + public void onClientRemoved(ClientState removed) { + mRemoved = removed; + mLatch.countDown(); + } + + ClientState waitForRemovedClient(long timeout, TimeUnit unit) { + try { + assertWithMessage("ClientController callback wasn't called on user removed").that( + mLatch.await(timeout, unit)).isTrue(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new IllegalStateException("Unexpected thread interruption", e); + } + return mRemoved; + } + } } diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceConfigTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceConfigTest.java index c67e7c5ae61e..b29fc8828f58 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceConfigTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceConfigTest.java @@ -824,6 +824,16 @@ public final class DisplayDeviceConfigTest { mDisplayDeviceConfig.getAutoBrightnessBrighteningLevels( AUTO_BRIGHTNESS_MODE_DOZE, Settings.System.SCREEN_BRIGHTNESS_AUTOMATIC_BRIGHT), SMALL_DELTA); + + // Should fall back to the normal preset + assertArrayEquals(new float[]{0.0f, 95}, + mDisplayDeviceConfig.getAutoBrightnessBrighteningLevelsLux( + AUTO_BRIGHTNESS_MODE_DOZE, + Settings.System.SCREEN_BRIGHTNESS_AUTOMATIC_DIM), ZERO_DELTA); + assertArrayEquals(new float[]{0.35f, 0.45f}, + mDisplayDeviceConfig.getAutoBrightnessBrighteningLevels( + AUTO_BRIGHTNESS_MODE_DOZE, + Settings.System.SCREEN_BRIGHTNESS_AUTOMATIC_DIM), SMALL_DELTA); } @Test diff --git a/services/tests/displayservicetests/src/com/android/server/display/mode/BrightnessObserverTest.kt b/services/tests/displayservicetests/src/com/android/server/display/mode/BrightnessObserverTest.kt new file mode 100644 index 000000000000..638924eeb2a3 --- /dev/null +++ b/services/tests/displayservicetests/src/com/android/server/display/mode/BrightnessObserverTest.kt @@ -0,0 +1,104 @@ +/* + * 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.display.mode + +import android.content.Context +import android.content.ContextWrapper +import android.hardware.display.BrightnessInfo +import android.view.Display +import androidx.test.core.app.ApplicationProvider +import androidx.test.filters.SmallTest +import com.android.server.display.DisplayDeviceConfig +import com.android.server.display.feature.DisplayManagerFlags +import com.android.server.testutils.TestHandler +import com.google.common.truth.Truth.assertThat +import com.google.testing.junit.testparameterinjector.TestParameter +import com.google.testing.junit.testparameterinjector.TestParameterInjector +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito +import org.mockito.junit.MockitoJUnit +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever + +@SmallTest +@RunWith(TestParameterInjector::class) +class BrightnessObserverTest { + + @get:Rule + val mockitoRule = MockitoJUnit.rule() + + private lateinit var spyContext: Context + private val mockInjector = mock<DisplayModeDirector.Injector>() + private val mockFlags = mock<DisplayManagerFlags>() + private val mockDeviceConfig = mock<DisplayDeviceConfig>() + + private val testHandler = TestHandler(null) + + @Before + fun setUp() { + spyContext = Mockito.spy(ContextWrapper(ApplicationProvider.getApplicationContext())) + } + + @Test + fun testLowLightBlockingZoneVotes(@TestParameter testCase: LowLightTestCase) { + setUpLowBrightnessZone() + whenever(mockFlags.isVsyncLowLightVoteEnabled).thenReturn(testCase.vsyncLowLightVoteEnabled) + val displayModeDirector = DisplayModeDirector( + spyContext, testHandler, mockInjector, mockFlags) + val brightnessObserver = displayModeDirector.BrightnessObserver( + spyContext, testHandler, mockInjector, testCase.vrrSupported, mockFlags) + + brightnessObserver.onRefreshRateSettingChangedLocked(0.0f, 120.0f) + brightnessObserver.updateBlockingZoneThresholds(mockDeviceConfig, false) + brightnessObserver.onDeviceConfigRefreshRateInLowZoneChanged(60) + + brightnessObserver.onDisplayChanged(Display.DEFAULT_DISPLAY) + + assertThat(displayModeDirector.getVote(VotesStorage.GLOBAL_ID, + Vote.PRIORITY_FLICKER_REFRESH_RATE_SWITCH)).isEqualTo(testCase.expectedVote) + } + + private fun setUpLowBrightnessZone() { + whenever(mockInjector.getBrightnessInfo(Display.DEFAULT_DISPLAY)).thenReturn( + BrightnessInfo(/* brightness = */ 0.05f, /* adjustedBrightness = */ 0.05f, + /* brightnessMinimum = */ 0.0f, /* brightnessMaximum = */ 1.0f, + BrightnessInfo.HIGH_BRIGHTNESS_MODE_OFF, + /* highBrightnessTransitionPoint = */ 1.0f, + BrightnessInfo.BRIGHTNESS_MAX_REASON_NONE)) + whenever(mockDeviceConfig.highDisplayBrightnessThresholds).thenReturn(floatArrayOf()) + whenever(mockDeviceConfig.highAmbientBrightnessThresholds).thenReturn(floatArrayOf()) + whenever(mockDeviceConfig.lowDisplayBrightnessThresholds).thenReturn(floatArrayOf(0.1f)) + whenever(mockDeviceConfig.lowAmbientBrightnessThresholds).thenReturn(floatArrayOf(10f)) + } + + enum class LowLightTestCase( + val vrrSupported: Boolean, + val vsyncLowLightVoteEnabled: Boolean, + internal val expectedVote: Vote + ) { + ALL_ENABLED(true, true, CombinedVote( + listOf(DisableRefreshRateSwitchingVote(true), + SupportedModesVote( + listOf(SupportedModesVote.SupportedMode(60f, 60f), + SupportedModesVote.SupportedMode(120f, 120f)))))), + VRR_NOT_SUPPORTED(false, true, DisableRefreshRateSwitchingVote(true)), + VSYNC_VOTE_DISABLED(true, false, DisableRefreshRateSwitchingVote(true)) + } +}
\ No newline at end of file diff --git a/services/tests/displayservicetests/src/com/android/server/display/mode/DisplayObserverTest.java b/services/tests/displayservicetests/src/com/android/server/display/mode/DisplayObserverTest.java index ff91d34470d4..92016dfc631b 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/mode/DisplayObserverTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/mode/DisplayObserverTest.java @@ -20,11 +20,10 @@ package com.android.server.display.mode; import static android.view.Display.DEFAULT_DISPLAY; import static android.view.Display.Mode.INVALID_MODE_ID; - import static com.android.server.display.mode.DisplayModeDirector.SYNCHRONIZED_REFRESH_RATE_TOLERANCE; import static com.android.server.display.mode.Vote.PRIORITY_LIMIT_MODE; -import static com.android.server.display.mode.Vote.PRIORITY_USER_SETTING_DISPLAY_PREFERRED_SIZE; import static com.android.server.display.mode.Vote.PRIORITY_SYNCHRONIZED_REFRESH_RATE; +import static com.android.server.display.mode.Vote.PRIORITY_USER_SETTING_DISPLAY_PREFERRED_SIZE; import static com.android.server.display.mode.VotesStorage.GLOBAL_ID; import static com.google.common.truth.Truth.assertThat; @@ -43,6 +42,7 @@ import android.hardware.display.DisplayManager; import android.os.Handler; import android.os.Looper; import android.provider.DeviceConfigInterface; +import android.test.mock.MockContentResolver; import android.view.Display; import android.view.DisplayInfo; @@ -51,21 +51,26 @@ import androidx.test.core.app.ApplicationProvider; import androidx.test.filters.SmallTest; import com.android.internal.R; +import com.android.internal.util.test.FakeSettingsProvider; +import com.android.internal.util.test.FakeSettingsProviderRule; import com.android.server.display.feature.DisplayManagerFlags; import com.android.server.sensors.SensorManagerInternal; +import junitparams.JUnitParamsRunner; + import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; -import junitparams.JUnitParamsRunner; - - @SmallTest @RunWith(JUnitParamsRunner.class) public class DisplayObserverTest { + @Rule + public FakeSettingsProviderRule mSettingsProviderRule = FakeSettingsProvider.rule(); + private static final int EXTERNAL_DISPLAY = 1; private static final int MAX_WIDTH = 1920; private static final int MAX_HEIGHT = 1080; @@ -120,6 +125,8 @@ public class DisplayObserverTest { mContext = spy(new ContextWrapper(ApplicationProvider.getApplicationContext())); mResources = mock(Resources.class); when(mContext.getResources()).thenReturn(mResources); + MockContentResolver resolver = mSettingsProviderRule.mockContentResolver(mContext); + when(mContext.getContentResolver()).thenReturn(resolver); when(mResources.getInteger(R.integer.config_externalDisplayPeakRefreshRate)) .thenReturn(0); when(mResources.getInteger(R.integer.config_externalDisplayPeakWidth)) diff --git a/services/tests/mockingservicestests/src/com/android/server/SensitiveContentProtectionManagerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/SensitiveContentProtectionManagerServiceTest.java index b363fd4cc7cb..d78143381ae5 100644 --- a/services/tests/mockingservicestests/src/com/android/server/SensitiveContentProtectionManagerServiceTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/SensitiveContentProtectionManagerServiceTest.java @@ -20,11 +20,13 @@ import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentat import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doCallRealMethod; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.doThrow; 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.media.projection.MediaProjectionInfo; @@ -35,6 +37,7 @@ import android.service.notification.StatusBarNotification; import android.testing.AndroidTestingRunner; import android.testing.TestableContext; import android.testing.TestableLooper.RunWithLooper; +import android.util.ArraySet; import androidx.test.filters.SmallTest; @@ -52,7 +55,6 @@ import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.MockitoAnnotations; -import java.util.Collections; import java.util.Set; @SmallTest @@ -68,6 +70,8 @@ public class SensitiveContentProtectionManagerServiceTest { private static final int NOTIFICATION_UID_1 = 5; private static final int NOTIFICATION_UID_2 = 6; + private static final ArraySet<PackageInfo> EMPTY_SET = new ArraySet<>(); + @Rule public final TestableContext mContext = new TestableContext(getInstrumentation().getTargetContext(), null); @@ -107,6 +111,9 @@ public class SensitiveContentProtectionManagerServiceTest { mSensitiveContentProtectionManagerService.mNotificationListener = spy(mSensitiveContentProtectionManagerService.mNotificationListener); + doCallRealMethod() + .when(mSensitiveContentProtectionManagerService.mNotificationListener) + .onListenerConnected(); // Setup RankingMap and two possilbe rankings when(mSensitiveRanking.hasSensitiveContent()).thenReturn(true); @@ -128,7 +135,7 @@ public class SensitiveContentProtectionManagerServiceTest { mSensitiveContentProtectionManagerService.onDestroy(); } - private Set<PackageInfo> setupSensitiveNotification() { + private ArraySet<PackageInfo> setupSensitiveNotification() { // Setup Notification Values when(mNotification1.getKey()).thenReturn(NOTIFICATION_KEY_1); when(mNotification1.getPackageName()).thenReturn(NOTIFICATION_PKG_1); @@ -149,10 +156,11 @@ public class SensitiveContentProtectionManagerServiceTest { when(mRankingMap.getRawRankingObject(eq(NOTIFICATION_KEY_2))) .thenReturn(mNonSensitiveRanking); - return Set.of(new PackageInfo(NOTIFICATION_PKG_1, NOTIFICATION_UID_1)); + return new ArraySet<>( + Set.of(new PackageInfo(NOTIFICATION_PKG_1, NOTIFICATION_UID_1))); } - private Set<PackageInfo> setupMultipleSensitiveNotificationsFromSamePackageAndUid() { + private ArraySet<PackageInfo> setupMultipleSensitiveNotificationsFromSamePackageAndUid() { // Setup Notification Values when(mNotification1.getKey()).thenReturn(NOTIFICATION_KEY_1); when(mNotification1.getPackageName()).thenReturn(NOTIFICATION_PKG_1); @@ -173,10 +181,11 @@ public class SensitiveContentProtectionManagerServiceTest { when(mRankingMap.getRawRankingObject(eq(NOTIFICATION_KEY_2))) .thenReturn(mSensitiveRanking); - return Set.of(new PackageInfo(NOTIFICATION_PKG_1, NOTIFICATION_UID_1)); + return new ArraySet<>( + Set.of(new PackageInfo(NOTIFICATION_PKG_1, NOTIFICATION_UID_1))); } - private Set<PackageInfo> setupMultipleSensitiveNotificationsFromDifferentPackage() { + private ArraySet<PackageInfo> setupMultipleSensitiveNotificationsFromDifferentPackage() { // Setup Notification Values when(mNotification1.getKey()).thenReturn(NOTIFICATION_KEY_1); when(mNotification1.getPackageName()).thenReturn(NOTIFICATION_PKG_1); @@ -197,11 +206,12 @@ public class SensitiveContentProtectionManagerServiceTest { when(mRankingMap.getRawRankingObject(eq(NOTIFICATION_KEY_2))) .thenReturn(mSensitiveRanking); - return Set.of(new PackageInfo(NOTIFICATION_PKG_1, NOTIFICATION_UID_1), - new PackageInfo(NOTIFICATION_PKG_2, NOTIFICATION_UID_1)); + return new ArraySet<>( + Set.of(new PackageInfo(NOTIFICATION_PKG_1, NOTIFICATION_UID_1), + new PackageInfo(NOTIFICATION_PKG_2, NOTIFICATION_UID_1))); } - private Set<PackageInfo> setupMultipleSensitiveNotificationsFromDifferentUid() { + private ArraySet<PackageInfo> setupMultipleSensitiveNotificationsFromDifferentUid() { // Setup Notification Values when(mNotification1.getKey()).thenReturn(NOTIFICATION_KEY_1); when(mNotification1.getPackageName()).thenReturn(NOTIFICATION_PKG_1); @@ -222,8 +232,9 @@ public class SensitiveContentProtectionManagerServiceTest { when(mRankingMap.getRawRankingObject(eq(NOTIFICATION_KEY_2))) .thenReturn(mSensitiveRanking); - return Set.of(new PackageInfo(NOTIFICATION_PKG_1, NOTIFICATION_UID_1), - new PackageInfo(NOTIFICATION_PKG_1, NOTIFICATION_UID_2)); + return new ArraySet<>( + Set.of(new PackageInfo(NOTIFICATION_PKG_1, NOTIFICATION_UID_1), + new PackageInfo(NOTIFICATION_PKG_1, NOTIFICATION_UID_2))); } private void setupNoSensitiveNotifications() { @@ -251,11 +262,11 @@ public class SensitiveContentProtectionManagerServiceTest { @Test public void mediaProjectionOnStart_onProjectionStart_setWmBlockedPackages() { - Set<PackageInfo> expectedBlockedPackages = setupSensitiveNotification(); + ArraySet<PackageInfo> expectedBlockedPackages = setupSensitiveNotification(); mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class)); - verify(mWindowManager).setShouldBlockScreenCaptureForApp(expectedBlockedPackages); + verify(mWindowManager).addBlockScreenCaptureForApps(expectedBlockedPackages); } @Test @@ -264,7 +275,7 @@ public class SensitiveContentProtectionManagerServiceTest { mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class)); - verify(mWindowManager).setShouldBlockScreenCaptureForApp(Collections.emptySet()); + verify(mWindowManager).addBlockScreenCaptureForApps(EMPTY_SET); } @Test @@ -273,37 +284,37 @@ public class SensitiveContentProtectionManagerServiceTest { mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class)); - verify(mWindowManager).setShouldBlockScreenCaptureForApp(Collections.emptySet()); + verify(mWindowManager).addBlockScreenCaptureForApps(EMPTY_SET); } @Test public void mediaProjectionOnStart_multipleNotifications_setWmBlockedPackages() { - Set<PackageInfo> expectedBlockedPackages = + ArraySet<PackageInfo> expectedBlockedPackages = setupMultipleSensitiveNotificationsFromSamePackageAndUid(); mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class)); - verify(mWindowManager).setShouldBlockScreenCaptureForApp(expectedBlockedPackages); + verify(mWindowManager).addBlockScreenCaptureForApps(expectedBlockedPackages); } @Test public void mediaProjectionOnStart_multiplePackages_setWmBlockedPackages() { - Set<PackageInfo> expectedBlockedPackages = + ArraySet<PackageInfo> expectedBlockedPackages = setupMultipleSensitiveNotificationsFromDifferentPackage(); mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class)); - verify(mWindowManager).setShouldBlockScreenCaptureForApp(expectedBlockedPackages); + verify(mWindowManager).addBlockScreenCaptureForApps(expectedBlockedPackages); } @Test public void mediaProjectionOnStart_multipleUid_setWmBlockedPackages() { - Set<PackageInfo> expectedBlockedPackages = + ArraySet<PackageInfo> expectedBlockedPackages = setupMultipleSensitiveNotificationsFromDifferentUid(); mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class)); - verify(mWindowManager).setShouldBlockScreenCaptureForApp(expectedBlockedPackages); + verify(mWindowManager).addBlockScreenCaptureForApps(expectedBlockedPackages); } @Test @@ -316,12 +327,12 @@ public class SensitiveContentProtectionManagerServiceTest { mMediaProjectionCallbackCaptor.getValue().onStop(mediaProjectionInfo); - verify(mWindowManager).setShouldBlockScreenCaptureForApp(Collections.emptySet()); + verify(mWindowManager).clearBlockedApps(); } @Test public void mediaProjectionOnStart_afterOnStop_onProjectionStart_setWmBlockedPackages() { - Set<PackageInfo> expectedBlockedPackages = setupSensitiveNotification(); + ArraySet<PackageInfo> expectedBlockedPackages = setupSensitiveNotification(); MediaProjectionInfo mediaProjectionInfo = mock(MediaProjectionInfo.class); mMediaProjectionCallbackCaptor.getValue().onStart(mediaProjectionInfo); @@ -330,7 +341,7 @@ public class SensitiveContentProtectionManagerServiceTest { mMediaProjectionCallbackCaptor.getValue().onStart(mediaProjectionInfo); - verify(mWindowManager).setShouldBlockScreenCaptureForApp(expectedBlockedPackages); + verify(mWindowManager).addBlockScreenCaptureForApps(expectedBlockedPackages); } @Test @@ -341,7 +352,7 @@ public class SensitiveContentProtectionManagerServiceTest { mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class)); - verify(mWindowManager).setShouldBlockScreenCaptureForApp(Collections.emptySet()); + verify(mWindowManager).addBlockScreenCaptureForApps(EMPTY_SET); } @Test @@ -352,7 +363,7 @@ public class SensitiveContentProtectionManagerServiceTest { mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class)); - verify(mWindowManager).setShouldBlockScreenCaptureForApp(Collections.emptySet()); + verify(mWindowManager).addBlockScreenCaptureForApps(EMPTY_SET); } @Test @@ -363,7 +374,7 @@ public class SensitiveContentProtectionManagerServiceTest { mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class)); - verify(mWindowManager).setShouldBlockScreenCaptureForApp(Collections.emptySet()); + verify(mWindowManager).addBlockScreenCaptureForApps(EMPTY_SET); } @Test @@ -376,6 +387,314 @@ public class SensitiveContentProtectionManagerServiceTest { mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class)); - verify(mWindowManager).setShouldBlockScreenCaptureForApp(Collections.emptySet()); + verify(mWindowManager).addBlockScreenCaptureForApps(EMPTY_SET); + } + + @Test + public void nlsOnListenerConnected_projectionNotStarted_noop() { + // Sets up mNotification1 & mRankingMap to be a sensitive notification, and mNotification2 + // as non-sensitive + setupSensitiveNotification(); + + mSensitiveContentProtectionManagerService.mNotificationListener.onListenerConnected(); + + verifyZeroInteractions(mWindowManager); + } + + @Test + public void nlsOnListenerConnected_projectionStopped_noop() { + // Sets up mNotification1 & mRankingMap to be a sensitive notification, and mNotification2 + // as non-sensitive + setupSensitiveNotification(); + mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class)); + mMediaProjectionCallbackCaptor.getValue().onStop(mock(MediaProjectionInfo.class)); + Mockito.reset(mWindowManager); + + mSensitiveContentProtectionManagerService.mNotificationListener.onListenerConnected(); + + verifyZeroInteractions(mWindowManager); + } + + @Test + public void nlsOnListenerConnected_projectionStarted_setWmBlockedPackages() { + // Sets up mNotification1 & mRankingMap to be a sensitive notification, and mNotification2 + // as non-sensitive + ArraySet<PackageInfo> expectedBlockedPackages = setupSensitiveNotification(); + mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class)); + Mockito.reset(mWindowManager); + + mSensitiveContentProtectionManagerService.mNotificationListener.onListenerConnected(); + + verify(mWindowManager).addBlockScreenCaptureForApps(expectedBlockedPackages); + } + + @Test + public void nlsOnListenerConnected_noSensitiveNotifications_noBlockedPackages() { + setupNoSensitiveNotifications(); + mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class)); + Mockito.reset(mWindowManager); + + mSensitiveContentProtectionManagerService.mNotificationListener.onListenerConnected(); + + verify(mWindowManager).addBlockScreenCaptureForApps(EMPTY_SET); + } + + @Test + public void nlsOnListenerConnected_noNotifications_noBlockedPackages() { + setupNoNotifications(); + mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class)); + Mockito.reset(mWindowManager); + + mSensitiveContentProtectionManagerService.mNotificationListener.onListenerConnected(); + + verify(mWindowManager).addBlockScreenCaptureForApps(EMPTY_SET); + } + + @Test + public void nlsOnListenerConnected_nullRankingMap_noBlockedPackages() { + // Sets up mNotification1 & mRankingMap to be a sensitive notification, and mNotification2 + // as non-sensitive + setupSensitiveNotification(); + mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class)); + Mockito.reset(mWindowManager); + doReturn(null) + .when(mSensitiveContentProtectionManagerService.mNotificationListener) + .getCurrentRanking(); + + mSensitiveContentProtectionManagerService.mNotificationListener.onListenerConnected(); + + verify(mWindowManager).addBlockScreenCaptureForApps(EMPTY_SET); + } + + @Test + public void nlsOnListenerConnected_missingRanking_noBlockedPackages() { + // Sets up mNotification1 & mRankingMap to be a sensitive notification, and mNotification2 + // as non-sensitive + setupSensitiveNotification(); + mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class)); + Mockito.reset(mWindowManager); + when(mRankingMap.getRawRankingObject(eq(NOTIFICATION_KEY_1))).thenReturn(null); + doReturn(mRankingMap) + .when(mSensitiveContentProtectionManagerService.mNotificationListener) + .getCurrentRanking(); + + mSensitiveContentProtectionManagerService.mNotificationListener.onListenerConnected(); + + verify(mWindowManager).addBlockScreenCaptureForApps(EMPTY_SET); + } + + @Test + public void nlsOnNotificationRankingUpdate_projectionNotStarted_noop() { + // Sets up mNotification1 & mRankingMap to be a sensitive notification, and mNotification2 + // as non-sensitive + setupSensitiveNotification(); + + mSensitiveContentProtectionManagerService.mNotificationListener + .onNotificationRankingUpdate(mRankingMap); + + verifyZeroInteractions(mWindowManager); + } + + @Test + public void nlsOnNotificationRankingUpdate_projectionStopped_noop() { + // Sets up mNotification1 & mRankingMap to be a sensitive notification, and mNotification2 + // as non-sensitive + setupSensitiveNotification(); + mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class)); + mMediaProjectionCallbackCaptor.getValue().onStop(mock(MediaProjectionInfo.class)); + Mockito.reset(mWindowManager); + + mSensitiveContentProtectionManagerService.mNotificationListener + .onNotificationRankingUpdate(mRankingMap); + + verifyZeroInteractions(mWindowManager); + } + + @Test + public void nlsOnNotificationRankingUpdate_projectionStarted_setWmBlockedPackages() { + // Sets up mNotification1 & mRankingMap to be a sensitive notification, and mNotification2 + // as non-sensitive + ArraySet<PackageInfo> expectedBlockedPackages = setupSensitiveNotification(); + mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class)); + Mockito.reset(mWindowManager); + + mSensitiveContentProtectionManagerService.mNotificationListener + .onNotificationRankingUpdate(mRankingMap); + + verify(mWindowManager).addBlockScreenCaptureForApps(expectedBlockedPackages); + } + + @Test + public void nlsOnNotificationRankingUpdate_noSensitiveNotifications_noBlockedPackages() { + setupNoSensitiveNotifications(); + mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class)); + Mockito.reset(mWindowManager); + + mSensitiveContentProtectionManagerService.mNotificationListener + .onNotificationRankingUpdate(mRankingMap); + + verify(mWindowManager).addBlockScreenCaptureForApps(EMPTY_SET); + } + + @Test + public void nlsOnNotificationRankingUpdate_noNotifications_noBlockedPackages() { + setupNoNotifications(); + mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class)); + Mockito.reset(mWindowManager); + + mSensitiveContentProtectionManagerService.mNotificationListener + .onNotificationRankingUpdate(mRankingMap); + + verify(mWindowManager).addBlockScreenCaptureForApps(EMPTY_SET); + } + + @Test + public void nlsOnNotificationRankingUpdate_nullRankingMap_noBlockedPackages() { + // Sets up mNotification1 & mRankingMap to be a sensitive notification, and mNotification2 + // as non-sensitive + setupSensitiveNotification(); + mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class)); + Mockito.reset(mWindowManager); + + mSensitiveContentProtectionManagerService.mNotificationListener + .onNotificationRankingUpdate(null); + + verify(mWindowManager).addBlockScreenCaptureForApps(EMPTY_SET); + } + + @Test + public void nlsOnNotificationRankingUpdate_missingRanking_noBlockedPackages() { + // Sets up mNotification1 & mRankingMap to be a sensitive notification, and mNotification2 + // as non-sensitive + setupSensitiveNotification(); + mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class)); + Mockito.reset(mWindowManager); + when(mRankingMap.getRawRankingObject(eq(NOTIFICATION_KEY_1))).thenReturn(null); + doReturn(mRankingMap) + .when(mSensitiveContentProtectionManagerService.mNotificationListener) + .getCurrentRanking(); + + mSensitiveContentProtectionManagerService.mNotificationListener + .onNotificationRankingUpdate(mRankingMap); + + verify(mWindowManager).addBlockScreenCaptureForApps(EMPTY_SET); + } + + @Test + public void nlsOnNotificationRankingUpdate_getActiveNotificationsThrows_noBlockedPackages() { + // Sets up mNotification1 & mRankingMap to be a sensitive notification, and mNotification2 + // as non-sensitive + setupSensitiveNotification(); + mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class)); + Mockito.reset(mWindowManager); + + doThrow(SecurityException.class) + .when(mSensitiveContentProtectionManagerService.mNotificationListener) + .getActiveNotifications(); + + mSensitiveContentProtectionManagerService.mNotificationListener + .onNotificationRankingUpdate(mRankingMap); + + verify(mWindowManager).addBlockScreenCaptureForApps(EMPTY_SET); + } + + @Test + public void nlsOnNotificationPosted_projectionNotStarted_noop() { + // Sets up mNotification1 & mRankingMap to be a sensitive notification, and mNotification2 + // as non-sensitive + setupSensitiveNotification(); + + mSensitiveContentProtectionManagerService.mNotificationListener + .onNotificationPosted(mNotification1, mRankingMap); + + verifyZeroInteractions(mWindowManager); + } + + @Test + public void nlsOnNotificationPosted_projectionStopped_noop() { + // Sets up mNotification1 & mRankingMap to be a sensitive notification, and mNotification2 + // as non-sensitive + setupSensitiveNotification(); + mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class)); + mMediaProjectionCallbackCaptor.getValue().onStop(mock(MediaProjectionInfo.class)); + Mockito.reset(mWindowManager); + + mSensitiveContentProtectionManagerService.mNotificationListener + .onNotificationPosted(mNotification1, mRankingMap); + + verifyZeroInteractions(mWindowManager); + } + + @Test + public void nlsOnNotificationPosted_projectionStarted_setWmBlockedPackages() { + // Sets up mNotification1 & mRankingMap to be a sensitive notification, and mNotification2 + // as non-sensitive + setupSensitiveNotification(); + mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class)); + Mockito.reset(mWindowManager); + + mSensitiveContentProtectionManagerService.mNotificationListener + .onNotificationPosted(mNotification1, mRankingMap); + + ArraySet<PackageInfo> expectedBlockedPackages = new ArraySet<>( + Set.of(new PackageInfo(NOTIFICATION_PKG_1, NOTIFICATION_UID_1))); + verify(mWindowManager).addBlockScreenCaptureForApps(expectedBlockedPackages); + } + + @Test + public void nlsOnNotificationPosted_noSensitiveNotifications_noBlockedPackages() { + // Sets up mNotification1 & mRankingMap to be a sensitive notification, and mNotification2 + // as non-sensitive + setupSensitiveNotification(); + mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class)); + Mockito.reset(mWindowManager); + + mSensitiveContentProtectionManagerService.mNotificationListener + .onNotificationPosted(mNotification2, mRankingMap); + + verifyZeroInteractions(mWindowManager); + } + + @Test + public void nlsOnNotificationPosted_noNotifications_noBlockedPackages() { + // Sets up mNotification1 & mRankingMap to be a sensitive notification, and mNotification2 + // as non-sensitive + setupSensitiveNotification(); + mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class)); + Mockito.reset(mWindowManager); + + mSensitiveContentProtectionManagerService.mNotificationListener + .onNotificationPosted(null, mRankingMap); + + verifyZeroInteractions(mWindowManager); + } + + @Test + public void nlsOnNotificationPosted_nullRankingMap_noBlockedPackages() { + // Sets up mNotification1 & mRankingMap to be a sensitive notification, and mNotification2 + // as non-sensitive + setupSensitiveNotification(); + mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class)); + Mockito.reset(mWindowManager); + + mSensitiveContentProtectionManagerService.mNotificationListener + .onNotificationPosted(mNotification1, null); + + verifyZeroInteractions(mWindowManager); + } + + @Test + public void nlsOnNotificationPosted_missingRanking_noBlockedPackages() { + // Sets up mNotification1 & mRankingMap to be a sensitive notification, and mNotification2 + // as non-sensitive + setupSensitiveNotification(); + mMediaProjectionCallbackCaptor.getValue().onStart(mock(MediaProjectionInfo.class)); + Mockito.reset(mWindowManager); + when(mRankingMap.getRawRankingObject(eq(NOTIFICATION_KEY_1))).thenReturn(null); + + mSensitiveContentProtectionManagerService.mNotificationListener + .onNotificationPosted(mNotification1, mRankingMap); + + verifyZeroInteractions(mWindowManager); } } diff --git a/services/tests/mockingservicestests/src/com/android/server/am/ActiveServicesTest.java b/services/tests/mockingservicestests/src/com/android/server/am/ActiveServicesTest.java index e2c338ac8767..7e1dc08f301e 100644 --- a/services/tests/mockingservicestests/src/com/android/server/am/ActiveServicesTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/am/ActiveServicesTest.java @@ -202,7 +202,7 @@ public class ActiveServicesTest { final ServiceInfo regularService = new ServiceInfo(); regularService.processName = "com.foo"; String processName = ActiveServices.getProcessNameForService(regularService, null, null, - null, false, false); + null, false, false, false); assertEquals("com.foo", processName); // Isolated service @@ -211,29 +211,90 @@ public class ActiveServicesTest { isolatedService.flags = ServiceInfo.FLAG_ISOLATED_PROCESS; final ComponentName component = new ComponentName("com.foo", "barService"); processName = ActiveServices.getProcessNameForService(isolatedService, component, - null, null, false, false); + null, null, false, false, false); assertEquals("com.foo:barService", processName); + // Isolated Service in package private process. + final ServiceInfo isolatedService1 = new ServiceInfo(); + isolatedService1.processName = "com.foo:trusted_isolated"; + isolatedService1.flags = ServiceInfo.FLAG_ISOLATED_PROCESS; + final ComponentName componentName = new ComponentName("com.foo", "barService"); + processName = ActiveServices.getProcessNameForService(isolatedService1, componentName, + null, null, false, false, false); + assertEquals("com.foo:trusted_isolated:barService", processName); + + // Isolated service in package-private shared process (main process) + final ServiceInfo isolatedPackageSharedService = new ServiceInfo(); + final ComponentName componentName1 = new ComponentName("com.foo", "barService"); + isolatedPackageSharedService.processName = "com.foo"; + isolatedPackageSharedService.applicationInfo = new ApplicationInfo(); + isolatedPackageSharedService.applicationInfo.processName = "com.foo"; + isolatedPackageSharedService.flags = ServiceInfo.FLAG_ISOLATED_PROCESS; + String packageSharedIsolatedProcessName = ActiveServices.getProcessNameForService( + isolatedPackageSharedService, componentName1, null, null, false, false, true); + assertEquals("com.foo:barService", packageSharedIsolatedProcessName); + + // Isolated service in package-private shared process + final ServiceInfo isolatedPackageSharedService1 = new ServiceInfo( + isolatedPackageSharedService); + isolatedPackageSharedService1.processName = "com.foo:trusted_isolated"; + isolatedPackageSharedService1.applicationInfo = new ApplicationInfo(); + isolatedPackageSharedService1.applicationInfo.processName = "com.foo"; + isolatedPackageSharedService1.flags = ServiceInfo.FLAG_ISOLATED_PROCESS; + packageSharedIsolatedProcessName = ActiveServices.getProcessNameForService( + isolatedPackageSharedService1, componentName1, null, null, false, false, true); + assertEquals("com.foo:trusted_isolated", packageSharedIsolatedProcessName); + + + // Bind another one in the same isolated process + final ServiceInfo isolatedPackageSharedService2 = new ServiceInfo( + isolatedPackageSharedService1); + packageSharedIsolatedProcessName = ActiveServices.getProcessNameForService( + isolatedPackageSharedService2, componentName1, null, null, false, false, true); + assertEquals("com.foo:trusted_isolated", packageSharedIsolatedProcessName); + + // Simulate another app trying to do the bind. + final ServiceInfo isolatedPackageSharedService3 = new ServiceInfo( + isolatedPackageSharedService1); + final String auxCallingPackage = "com.bar"; + packageSharedIsolatedProcessName = ActiveServices.getProcessNameForService( + isolatedPackageSharedService3, componentName1, auxCallingPackage, null, + false, false, true); + assertEquals("com.foo:trusted_isolated", packageSharedIsolatedProcessName); + + // Simulate another app owning the service + final ServiceInfo isolatedOtherPackageSharedService = new ServiceInfo( + isolatedPackageSharedService1); + final ComponentName componentName2 = new ComponentName("com.bar", "barService"); + isolatedOtherPackageSharedService.processName = "com.bar:isolated"; + isolatedPackageSharedService.applicationInfo.processName = "com.bar"; + final String mainCallingPackage = "com.foo"; + packageSharedIsolatedProcessName = ActiveServices.getProcessNameForService( + isolatedOtherPackageSharedService, componentName2, mainCallingPackage, + null, false, false, true); + assertEquals("com.bar:isolated", packageSharedIsolatedProcessName); + // Isolated service in shared isolated process final ServiceInfo isolatedServiceShared1 = new ServiceInfo(); isolatedServiceShared1.flags = ServiceInfo.FLAG_ISOLATED_PROCESS; final String instanceName = "pool"; final String callingPackage = "com.foo"; final String sharedIsolatedProcessName1 = ActiveServices.getProcessNameForService( - isolatedServiceShared1, null, callingPackage, instanceName, false, true); + isolatedServiceShared1, null, callingPackage, instanceName, false, true, false); assertEquals("com.foo:ishared:pool", sharedIsolatedProcessName1); // Bind another one in the same isolated process final ServiceInfo isolatedServiceShared2 = new ServiceInfo(isolatedServiceShared1); final String sharedIsolatedProcessName2 = ActiveServices.getProcessNameForService( - isolatedServiceShared2, null, callingPackage, instanceName, false, true); + isolatedServiceShared2, null, callingPackage, instanceName, false, true, false); assertEquals(sharedIsolatedProcessName1, sharedIsolatedProcessName2); // Simulate another app trying to do the bind final ServiceInfo isolatedServiceShared3 = new ServiceInfo(isolatedServiceShared1); final String otherCallingPackage = "com.bar"; final String sharedIsolatedProcessName3 = ActiveServices.getProcessNameForService( - isolatedServiceShared3, null, otherCallingPackage, instanceName, false, true); + isolatedServiceShared3, null, otherCallingPackage, instanceName, false, true, + false); Assert.assertNotEquals(sharedIsolatedProcessName2, sharedIsolatedProcessName3); } diff --git a/services/tests/mockingservicestests/src/com/android/server/am/ApplicationStartInfoTest.java b/services/tests/mockingservicestests/src/com/android/server/am/ApplicationStartInfoTest.java new file mode 100644 index 000000000000..7d3a1103a044 --- /dev/null +++ b/services/tests/mockingservicestests/src/com/android/server/am/ApplicationStartInfoTest.java @@ -0,0 +1,587 @@ +/* + * 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.am; + +import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; + +import static com.android.server.am.ActivityManagerService.Injector; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.spy; + +import android.app.ApplicationStartInfo; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManagerInternal; +import android.os.FileUtils; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Process; +import android.platform.test.annotations.Presubmit; +import android.text.TextUtils; + +import com.android.server.LocalServices; +import com.android.server.ServiceThread; +import com.android.server.appop.AppOpsService; +import com.android.server.wm.ActivityTaskManagerService; + +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TestRule; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.io.File; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.ArrayList; + +/** + * Test class for {@link android.app.ApplicationStartInfo}. + * + * Build/Install/Run: + * atest ApplicationStartInfoTest + */ +@Presubmit +public class ApplicationStartInfoTest { + + private static final String TAG = ApplicationStartInfoTest.class.getSimpleName(); + private static final ComponentName COMPONENT = new ComponentName("com.android.test", ".Foo"); + + @Rule public ServiceThreadRule mServiceThreadRule = new ServiceThreadRule(); + @Mock private AppOpsService mAppOpsService; + @Mock private PackageManagerInternal mPackageManagerInt; + + private Context mContext = getInstrumentation().getTargetContext(); + private TestInjector mInjector; + private ActivityManagerService mAms; + private ProcessList mProcessList; + private AppStartInfoTracker mAppStartInfoTracker; + private Handler mHandler; + private HandlerThread mHandlerThread; + + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + + mHandlerThread = new HandlerThread(TAG); + mHandlerThread.start(); + mHandler = new Handler(mHandlerThread.getLooper()); + mProcessList = spy(new ProcessList()); + mAppStartInfoTracker = spy(new AppStartInfoTracker()); + mAppStartInfoTracker.mEnabled = true; + setFieldValue(ProcessList.class, mProcessList, "mAppStartInfoTracker", + mAppStartInfoTracker); + mInjector = new TestInjector(mContext); + mAms = new ActivityManagerService(mInjector, mServiceThreadRule.getThread()); + mAms.mActivityTaskManager = new ActivityTaskManagerService(mContext); + mAms.mActivityTaskManager.initialize(null, null, mContext.getMainLooper()); + mAms.mAtmInternal = spy(mAms.mActivityTaskManager.getAtmInternal()); + mAms.mPackageManagerInt = mPackageManagerInt; + mAppStartInfoTracker.mService = mAms; + doReturn(new ComponentName("", "")).when(mPackageManagerInt).getSystemUiServiceComponent(); + doReturn("com.android.test").when(mPackageManagerInt).getNameForUid(anyInt()); + // Remove stale instance of PackageManagerInternal if there is any + LocalServices.removeServiceForTest(PackageManagerInternal.class); + LocalServices.addService(PackageManagerInternal.class, mPackageManagerInt); + } + + @After + public void tearDown() { + mHandlerThread.quit(); + } + + @Test + public void testApplicationStartInfo() throws Exception { + mAppStartInfoTracker.clearProcessStartInfo(true); + mAppStartInfoTracker.mAppStartInfoLoaded.set(true); + mAppStartInfoTracker.mAppStartInfoHistoryListSize = + mAppStartInfoTracker.APP_START_INFO_HISTORY_LIST_SIZE; + mAppStartInfoTracker.mProcStartStoreDir = new File(mContext.getFilesDir(), + AppStartInfoTracker.APP_START_STORE_DIR); + assertTrue(FileUtils.createDir(mAppStartInfoTracker.mProcStartStoreDir)); + mAppStartInfoTracker.mProcStartInfoFile = new File(mAppStartInfoTracker.mProcStartStoreDir, + AppStartInfoTracker.APP_START_INFO_FILE); + + doNothing().when(mAppStartInfoTracker).schedulePersistProcessStartInfo(anyBoolean()); + + final int app1Uid = 10123; + final int app1Pid1 = 12345; + final int app1Pid2 = 12346; + final int app1DefiningUid = 23456; + final int app1UidUser2 = 1010123; + final int app1PidUser2 = 12347; + final String app1ProcessName = "com.android.test.stub1:process"; + final String app1PackageName = "com.android.test.stub1"; + final long appStartTimestampIntentStarted = 1000000; + final long appStartTimestampActivityLaunchFinished = 2000000; + final long appStartTimestampReportFullyDrawn = 3000000; + final long appStartTimestampService = 4000000; + final long appStartTimestampBroadcast = 5000000; + final long appStartTimestampRContentProvider = 6000000; + + ProcessRecord app = makeProcessRecord( + app1Pid1, // pid + app1Uid, // uid + app1Uid, // packageUid + null, // definingUid + app1ProcessName, // processName + app1PackageName); // packageName + + ArrayList<ApplicationStartInfo> list = new ArrayList<ApplicationStartInfo>(); + + // Case 1: Activity start intent failed + mAppStartInfoTracker.onIntentStarted(buildIntent(COMPONENT), + appStartTimestampIntentStarted); + mAppStartInfoTracker.getStartInfo(app1PackageName, app1Uid, app1Pid1, 0, list); + verifyInProgressRecordsSize(1); + assertEquals(list.size(), 0); + + verifyInProgApplicationStartInfo( + 0, // index + 0, // pid + 0, // uid + 0, // packageUid + null, // definingUid + null, // processName + ApplicationStartInfo.START_REASON_START_ACTIVITY, // reason + ApplicationStartInfo.STARTUP_STATE_STARTED, // startup state + ApplicationStartInfo.START_TYPE_UNSET, // state type + ApplicationStartInfo.LAUNCH_MODE_STANDARD); // launch mode + + mAppStartInfoTracker.onIntentFailed(appStartTimestampIntentStarted); + list.clear(); + mAppStartInfoTracker.getStartInfo(app1PackageName, app1Uid, app1Pid1, 0, list); + verifyInProgressRecordsSize(0); + assertEquals(list.size(), 0); + + mAppStartInfoTracker.clearProcessStartInfo(true); + + // Case 2: Activity start launch cancelled + mAppStartInfoTracker.onIntentStarted(buildIntent(COMPONENT), + appStartTimestampIntentStarted); + list.clear(); + mAppStartInfoTracker.getStartInfo(app1PackageName, app1Uid, app1Pid1, 0, list); + verifyInProgressRecordsSize(1); + assertEquals(list.size(), 0); + + mAppStartInfoTracker.onActivityLaunched(appStartTimestampIntentStarted, COMPONENT, + ApplicationStartInfo.START_TYPE_COLD, app); + list.clear(); + mAppStartInfoTracker.getStartInfo(app1PackageName, app1Uid, app1Pid1, 0, list); + verifyInProgressRecordsSize(1); + assertEquals(list.size(), 1); + + verifyInProgApplicationStartInfo( + 0, // index + app1Pid1, // pid + app1Uid, // uid + app1Uid, // packageUid + null, // definingUid + app1ProcessName, // processName + ApplicationStartInfo.START_REASON_START_ACTIVITY, // reason + ApplicationStartInfo.STARTUP_STATE_STARTED, // startup state + ApplicationStartInfo.START_TYPE_COLD, // state type + ApplicationStartInfo.LAUNCH_MODE_STANDARD); // launch mode + + mAppStartInfoTracker.onActivityLaunchCancelled(appStartTimestampIntentStarted); + list.clear(); + mAppStartInfoTracker.getStartInfo(app1PackageName, app1Uid, app1Pid1, 0, list); + verifyInProgressRecordsSize(0); + assertEquals(list.size(), 1); + + verifyApplicationStartInfo( + list.get(0), // info + app1Pid1, // pid + app1Uid, // uid + app1Uid, // packageUid + null, // definingUid + app1ProcessName, // processName + ApplicationStartInfo.START_REASON_START_ACTIVITY, // reason + ApplicationStartInfo.STARTUP_STATE_ERROR, // startup state + ApplicationStartInfo.START_TYPE_COLD, // state type + ApplicationStartInfo.LAUNCH_MODE_STANDARD); // launch mode + + mAppStartInfoTracker.clearProcessStartInfo(true); + + // Case 3: Activity start success + mAppStartInfoTracker.onIntentStarted(buildIntent(COMPONENT), + appStartTimestampIntentStarted); + list.clear(); + mAppStartInfoTracker.getStartInfo(app1PackageName, app1Uid, app1Pid1, 0, list); + verifyInProgressRecordsSize(1); + assertEquals(list.size(), 0); + + mAppStartInfoTracker.onActivityLaunched(appStartTimestampIntentStarted, COMPONENT, + ApplicationStartInfo.START_TYPE_COLD, app); + list.clear(); + mAppStartInfoTracker.getStartInfo(app1PackageName, app1Uid, app1Pid1, 0, list); + verifyInProgressRecordsSize(1); + assertEquals(list.size(), 1); + + verifyInProgApplicationStartInfo( + 0, // index + app1Pid1, // pid + app1Uid, // uid + app1Uid, // packageUid + null, // definingUid + app1ProcessName, // processName + ApplicationStartInfo.START_REASON_START_ACTIVITY, // reason + ApplicationStartInfo.STARTUP_STATE_STARTED, // startup state + ApplicationStartInfo.START_TYPE_COLD, // state type + ApplicationStartInfo.LAUNCH_MODE_STANDARD); // launch mode + + verifyApplicationStartInfo( + list.get(0), // info + app1Pid1, // pid + app1Uid, // uid + app1Uid, // packageUid + null, // definingUid + app1ProcessName, // processName + ApplicationStartInfo.START_REASON_START_ACTIVITY, // reason + ApplicationStartInfo.STARTUP_STATE_STARTED, // startup state + ApplicationStartInfo.START_TYPE_COLD, // state type + ApplicationStartInfo.LAUNCH_MODE_STANDARD); // launch mode + + mAppStartInfoTracker.onActivityLaunchFinished(appStartTimestampIntentStarted, COMPONENT, + appStartTimestampActivityLaunchFinished, ApplicationStartInfo.LAUNCH_MODE_STANDARD); + list.clear(); + mAppStartInfoTracker.getStartInfo(app1PackageName, app1Uid, app1Pid1, 0, list); + verifyInProgressRecordsSize(1); + assertEquals(list.size(), 1); + + verifyInProgApplicationStartInfo( + 0, // index + app1Pid1, // pid + app1Uid, // uid + app1Uid, // packageUid + null, // definingUid + app1ProcessName, // processName + ApplicationStartInfo.START_REASON_START_ACTIVITY, // reason + ApplicationStartInfo.STARTUP_STATE_FIRST_FRAME_DRAWN, // startup state + ApplicationStartInfo.START_TYPE_COLD, // state type + ApplicationStartInfo.LAUNCH_MODE_STANDARD); // launch mode + + mAppStartInfoTracker.onReportFullyDrawn(appStartTimestampIntentStarted, + appStartTimestampReportFullyDrawn); + list.clear(); + mAppStartInfoTracker.getStartInfo(app1PackageName, app1Uid, app1Pid1, 0, list); + verifyInProgressRecordsSize(0); + assertEquals(list.size(), 1); + + verifyApplicationStartInfo( + list.get(0), // info + app1Pid1, // pid + app1Uid, // uid + app1Uid, // packageUid + null, // definingUid + app1ProcessName, // processName + ApplicationStartInfo.START_REASON_START_ACTIVITY, // reason + ApplicationStartInfo.STARTUP_STATE_FIRST_FRAME_DRAWN, // startup state + ApplicationStartInfo.START_TYPE_COLD, // state type + ApplicationStartInfo.LAUNCH_MODE_STANDARD); // launch mode + + // Don't clear records for use in subsequent cases. + + // Case 4: Create an other app1 record with different pid started for a service + sleep(1); + app = makeProcessRecord( + app1Pid2, // pid + app1Uid, // uid + app1Uid, // packageUid + app1DefiningUid, // definingUid + app1ProcessName, // processName + app1PackageName); // packageName + ServiceRecord service = ServiceRecord.newEmptyInstanceForTest(mAms); + + mAppStartInfoTracker.handleProcessServiceStart(appStartTimestampService, app, service, + false); + list.clear(); + mAppStartInfoTracker.getStartInfo(app1PackageName, app1Uid, 0, 0, list); + assertEquals(list.size(), 2); + + verifyApplicationStartInfo( + list.get(0), // info + app1Pid2, // pid + app1Uid, // uid + app1Uid, // packageUid + app1DefiningUid, // definingUid + app1ProcessName, // processName + ApplicationStartInfo.START_REASON_SERVICE, // reason + ApplicationStartInfo.STARTUP_STATE_STARTED, // startup state + ApplicationStartInfo.START_TYPE_WARM, // state type + ApplicationStartInfo.LAUNCH_MODE_STANDARD); // launch mode + + // Case 5: Create an instance of app1 with a different user started for a broadcast + sleep(1); + app = makeProcessRecord( + app1PidUser2, // pid + app1UidUser2, // uid + app1UidUser2, // packageUid + null, // definingUid + app1ProcessName, // processName + app1PackageName); // packageName + + mAppStartInfoTracker.handleProcessBroadcastStart(appStartTimestampBroadcast, app, + null, true /* isColdStart */); + list.clear(); + mAppStartInfoTracker.getStartInfo(app1PackageName, app1UidUser2, app1PidUser2, 0, list); + assertEquals(list.size(), 1); + + verifyApplicationStartInfo( + list.get(0), // info + app1PidUser2, // pid + app1UidUser2, // uid + app1UidUser2, // packageUid + null, // definingUid + app1ProcessName, // processName + ApplicationStartInfo.START_REASON_BROADCAST, // reason + ApplicationStartInfo.STARTUP_STATE_STARTED, // startup state + ApplicationStartInfo.START_TYPE_COLD, // state type + ApplicationStartInfo.LAUNCH_MODE_STANDARD); // launch mode + + // Case 6: User 2 gets removed + mAppStartInfoTracker.onPackageRemoved(app1PackageName, app1UidUser2, false); + list.clear(); + mAppStartInfoTracker.getStartInfo(app1PackageName, app1UidUser2, app1PidUser2, 0, list); + assertEquals(list.size(), 0); + + list.clear(); + mAppStartInfoTracker.getStartInfo(app1PackageName, app1Uid, app1PidUser2, 0, list); + assertEquals(list.size(), 2); + + + // Case 7: Create a process from another package started for a content provider + final int app2UidUser2 = 1010234; + final int app2PidUser2 = 12348; + final String app2ProcessName = "com.android.test.stub2:process"; + final String app2PackageName = "com.android.test.stub2"; + + sleep(1); + + app = makeProcessRecord( + app2PidUser2, // pid + app2UidUser2, // uid + app2UidUser2, // packageUid + null, // definingUid + app2ProcessName, // processName + app2PackageName); // packageName + + mAppStartInfoTracker.handleProcessContentProviderStart(appStartTimestampRContentProvider, + app, false); + list.clear(); + mAppStartInfoTracker.getStartInfo(app2PackageName, app2UidUser2, app2PidUser2, 0, list); + assertEquals(list.size(), 1); + + verifyApplicationStartInfo( + list.get(0), // info + app2PidUser2, // pid + app2UidUser2, // uid + app2UidUser2, // packageUid + null, // definingUid + app2ProcessName, // processName + ApplicationStartInfo.START_REASON_CONTENT_PROVIDER, // reason + ApplicationStartInfo.STARTUP_STATE_STARTED, // startup state + ApplicationStartInfo.START_TYPE_WARM, // state type + ApplicationStartInfo.LAUNCH_MODE_STANDARD); // launch mode + + // Case 8: Save and load again + ArrayList<ApplicationStartInfo> original = new ArrayList<ApplicationStartInfo>(); + mAppStartInfoTracker.getStartInfo(null, app1Uid, 0, 0, original); + assertTrue(original.size() > 0); + + mAppStartInfoTracker.persistProcessStartInfo(); + assertTrue(mAppStartInfoTracker.mProcStartInfoFile.exists()); + + mAppStartInfoTracker.clearProcessStartInfo(false); + list.clear(); + mAppStartInfoTracker.getStartInfo(null, app1Uid, 0, 0, list); + assertEquals(0, list.size()); + + mAppStartInfoTracker.loadExistingProcessStartInfo(); + list.clear(); + mAppStartInfoTracker.getStartInfo(null, app1Uid, 0, 0, list); + assertEquals(original.size(), list.size()); + + for (int i = list.size() - 1; i >= 0; i--) { + assertTrue(list.get(i).equals(original.get(i))); + } + } + + private static <T> void setFieldValue(Class clazz, Object obj, String fieldName, T val) { + try { + Field field = clazz.getDeclaredField(fieldName); + field.setAccessible(true); + Field mfield = Field.class.getDeclaredField("accessFlags"); + mfield.setAccessible(true); + mfield.setInt(field, mfield.getInt(field) & ~(Modifier.FINAL | Modifier.PRIVATE)); + field.set(obj, val); + } catch (NoSuchFieldException | IllegalAccessException e) { + } + } + + private void sleep(long ms) { + try { + Thread.sleep(ms); + } catch (InterruptedException e) { + } + } + + private ProcessRecord makeProcessRecord(int pid, int uid, int packageUid, Integer definingUid, + String processName, String packageName) { + return makeProcessRecord(pid, uid, packageUid, definingUid, processName, packageName, mAms); + } + + @SuppressWarnings("GuardedBy") + static ProcessRecord makeProcessRecord(int pid, int uid, int packageUid, Integer definingUid, + String processName, String packageName, ActivityManagerService ams) { + ApplicationInfo ai = new ApplicationInfo(); + ai.packageName = packageName; + ProcessRecord app = new ProcessRecord(ams, ai, processName, uid); + app.setPid(pid); + app.info.uid = packageUid; + if (definingUid != null) { + app.setHostingRecord(HostingRecord.byAppZygote(COMPONENT, "", definingUid, "")); + } + return app; + } + + private static Intent buildIntent(ComponentName componentName) throws Exception { + Intent intent = new Intent(); + intent.setComponent(componentName); + intent.setPackage(componentName.getPackageName()); + return intent; + } + + private void verifyInProgressRecordsSize(int expectedSize) { + synchronized (mAppStartInfoTracker.mLock) { + assertEquals(mAppStartInfoTracker.mInProgRecords.size(), expectedSize); + } + } + + private void verifyInProgApplicationStartInfo(int index, + Integer pid, Integer uid, Integer packageUid, + Integer definingUid, String processName, + Integer reason, Integer startupState, Integer startType, Integer launchMode) { + synchronized (mAppStartInfoTracker.mLock) { + verifyApplicationStartInfo(mAppStartInfoTracker.mInProgRecords.valueAt(index), + pid, uid, packageUid, definingUid, processName, reason, startupState, + startType, launchMode); + } + } + + private void verifyApplicationStartInfo(ApplicationStartInfo info, + Integer pid, Integer uid, Integer packageUid, + Integer definingUid, String processName, + Integer reason, Integer startupState, Integer startType, Integer launchMode) { + assertNotNull(info); + + if (pid != null) { + assertEquals(pid.intValue(), info.getPid()); + } + if (uid != null) { + assertEquals(uid.intValue(), info.getRealUid()); + } + if (packageUid != null) { + assertEquals(packageUid.intValue(), info.getPackageUid()); + } + if (definingUid != null) { + assertEquals(definingUid.intValue(), info.getDefiningUid()); + } + if (processName != null) { + assertTrue(TextUtils.equals(processName, info.getProcessName())); + } + if (reason != null) { + assertEquals(reason.intValue(), info.getReason()); + } + if (startupState != null) { + assertEquals(startupState.intValue(), info.getStartupState()); + } + if (startType != null) { + assertEquals(startType.intValue(), info.getStartType()); + } + if (launchMode != null) { + assertEquals(launchMode.intValue(), info.getLaunchMode()); + } + } + + private class TestInjector extends Injector { + TestInjector(Context context) { + super(context); + } + + @Override + public AppOpsService getAppOpsService(File recentAccessesFile, File storageFile, + Handler handler) { + return mAppOpsService; + } + + @Override + public Handler getUiHandler(ActivityManagerService service) { + return mHandler; + } + + @Override + public ProcessList getProcessList(ActivityManagerService service) { + return mProcessList; + } + } + + static class ServiceThreadRule implements TestRule { + + private ServiceThread mThread; + + ServiceThread getThread() { + return mThread; + } + + @Override + public Statement apply(Statement base, Description description) { + return new Statement() { + @Override + public void evaluate() throws Throwable { + mThread = new ServiceThread("TestServiceThread", + Process.THREAD_PRIORITY_DEFAULT, true /* allowIo */); + mThread.start(); + try { + base.evaluate(); + } finally { + mThread.getThreadHandler().runWithScissors(mThread::quit, 0 /* timeout */); + } + } + }; + } + } + + // TODO: [b/302724778] Remove manual JNI load + static { + System.loadLibrary("mockingservicestestjni"); + } +} 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 650c473533ed..97767a5dbd89 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 @@ -26,15 +26,18 @@ import static android.text.format.DateUtils.HOUR_IN_MILLIS; import static android.text.format.DateUtils.MINUTE_IN_MILLIS; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession; +import static com.android.server.job.JobSchedulerService.ACTIVE_INDEX; +import static com.android.server.job.JobSchedulerService.EXEMPTED_INDEX; +import static com.android.server.job.controllers.FlexibilityController.FLEXIBLE_CONSTRAINTS; import static com.android.server.job.controllers.FlexibilityController.FcConfig.DEFAULT_FALLBACK_FLEXIBILITY_DEADLINE_MS; import static com.android.server.job.controllers.FlexibilityController.FcConfig.DEFAULT_UNSEEN_CONSTRAINT_GRACE_PERIOD_MS; import static com.android.server.job.controllers.FlexibilityController.FcConfig.KEY_APPLIED_CONSTRAINTS; import static com.android.server.job.controllers.FlexibilityController.FcConfig.KEY_DEADLINE_PROXIMITY_LIMIT; import static com.android.server.job.controllers.FlexibilityController.FcConfig.KEY_FALLBACK_FLEXIBILITY_DEADLINE; import static com.android.server.job.controllers.FlexibilityController.FcConfig.KEY_PERCENTS_TO_DROP_NUM_FLEXIBLE_CONSTRAINTS; -import static com.android.server.job.controllers.FlexibilityController.FLEXIBLE_CONSTRAINTS; import static com.android.server.job.controllers.FlexibilityController.SYSTEM_WIDE_FLEXIBLE_CONSTRAINTS; import static com.android.server.job.controllers.JobStatus.CONSTRAINT_BATTERY_NOT_LOW; import static com.android.server.job.controllers.JobStatus.CONSTRAINT_CHARGING; @@ -50,24 +53,32 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.app.AlarmManager; import android.app.AppGlobals; import android.app.job.JobInfo; +import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.Context; +import android.content.Intent; import android.content.pm.PackageManager; import android.content.pm.PackageManagerInternal; import android.net.NetworkRequest; import android.os.Looper; +import android.os.PowerManager; import android.provider.DeviceConfig; import android.util.ArraySet; +import android.util.EmptyArray; import com.android.server.AppSchedulingModuleThread; +import com.android.server.DeviceIdleInternal; import com.android.server.LocalServices; import com.android.server.job.JobSchedulerService; import com.android.server.job.JobStore; @@ -77,6 +88,7 @@ import libcore.junit.util.compat.CoreCompatChangeRule; import org.junit.After; import org.junit.Before; import org.junit.Test; +import org.mockito.ArgumentCaptor; import org.mockito.ArgumentMatchers; import org.mockito.Mock; import org.mockito.MockitoSession; @@ -95,6 +107,7 @@ public class FlexibilityControllerTest { private static final long FROZEN_TIME = 100L; private MockitoSession mMockingSession; + private BroadcastReceiver mBroadcastReceiver; private FlexibilityController mFlexibilityController; private DeviceConfig.Properties.Builder mDeviceConfigPropertiesBuilder; private JobStore mJobStore; @@ -106,6 +119,8 @@ public class FlexibilityControllerTest { @Mock private Context mContext; @Mock + private DeviceIdleInternal mDeviceIdleInternal; + @Mock private JobSchedulerService mJobSchedulerService; @Mock private PrefetchController mPrefetchController; @@ -128,10 +143,13 @@ public class FlexibilityControllerTest { // Called in FlexibilityController constructor. when(mContext.getMainLooper()).thenReturn(Looper.getMainLooper()); when(mContext.getSystemService(Context.ALARM_SERVICE)).thenReturn(mAlarmManager); + doNothing().when(mAlarmManager).setExact(anyInt(), anyLong(), anyString(), any(), any()); when(mContext.getPackageManager()).thenReturn(mPackageManager); when(mPackageManager.hasSystemFeature( PackageManager.FEATURE_AUTOMOTIVE)).thenReturn(false); when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_EMBEDDED)).thenReturn(false); + doReturn(mDeviceIdleInternal) + .when(() -> LocalServices.getService(DeviceIdleInternal.class)); // Used in FlexibilityController.FcConstants. doAnswer((Answer<Void>) invocationOnMock -> null) .when(() -> DeviceConfig.addOnPropertiesChangedListener( @@ -146,7 +164,7 @@ public class FlexibilityControllerTest { eq(DeviceConfig.NAMESPACE_JOB_SCHEDULER), ArgumentMatchers.<String>any())); //used to get jobs by UID mJobStore = JobStore.initAndGetForTesting(mContext, mContext.getFilesDir()); - when(mJobSchedulerService.getJobStore()).thenReturn(mJobStore); + doReturn(mJobStore).when(mJobSchedulerService).getJobStore(); // Used in JobStatus. doReturn(mock(PackageManagerInternal.class)) .when(() -> LocalServices.getService(PackageManagerInternal.class)); @@ -156,6 +174,8 @@ public class FlexibilityControllerTest { JobSchedulerService.sElapsedRealtimeClock = Clock.fixed(Instant.ofEpochMilli(FROZEN_TIME), ZoneOffset.UTC); // Initialize real objects. + ArgumentCaptor<BroadcastReceiver> receiverCaptor = + ArgumentCaptor.forClass(BroadcastReceiver.class); mFlexibilityController = new FlexibilityController(mJobSchedulerService, mPrefetchController); mFcConfig = mFlexibilityController.getFcConfig(); @@ -166,6 +186,11 @@ public class FlexibilityControllerTest { setDeviceConfigLong(KEY_DEADLINE_PROXIMITY_LIMIT, 0L); setDeviceConfigInt(KEY_APPLIED_CONSTRAINTS, FLEXIBLE_CONSTRAINTS); waitForQuietModuleThread(); + + verify(mContext).registerReceiver(receiverCaptor.capture(), + ArgumentMatchers.argThat(filter -> + filter.hasAction(PowerManager.ACTION_POWER_SAVE_WHITELIST_CHANGED))); + mBroadcastReceiver = receiverCaptor.getValue(); } @After @@ -212,6 +237,7 @@ public class FlexibilityControllerTest { JobStatus js = JobStatus.createFromJobInfo( jobInfo, 1000, SOURCE_PACKAGE, SOURCE_USER_ID, "FCTest", testTag); js.enqueueTime = FROZEN_TIME; + js.setStandbyBucket(ACTIVE_INDEX); if (js.hasFlexibilityConstraint()) { js.setNumAppliedFlexibleConstraints(Integer.bitCount( mFlexibilityController.getRelevantAppliedConstraintsLocked(js))); @@ -400,6 +426,8 @@ public class FlexibilityControllerTest { @Test public void testGetNextConstraintDropTimeElapsedLocked() { + setDeviceConfigLong(KEY_FALLBACK_FLEXIBILITY_DEADLINE, 200 * HOUR_IN_MILLIS); + long nextTimeToDropNumConstraints; // no delay, deadline @@ -431,15 +459,18 @@ public class FlexibilityControllerTest { nextTimeToDropNumConstraints = mFlexibilityController .getNextConstraintDropTimeElapsedLocked(js); - assertEquals(130400100, nextTimeToDropNumConstraints); + assertEquals(FROZEN_TIME + 800000L + (200 * HOUR_IN_MILLIS) / 2, + nextTimeToDropNumConstraints); js.setNumDroppedFlexibleConstraints(1); nextTimeToDropNumConstraints = mFlexibilityController .getNextConstraintDropTimeElapsedLocked(js); - assertEquals(156320100L, nextTimeToDropNumConstraints); + assertEquals(FROZEN_TIME + 800000L + (200 * HOUR_IN_MILLIS) * 6 / 10, + nextTimeToDropNumConstraints); js.setNumDroppedFlexibleConstraints(2); nextTimeToDropNumConstraints = mFlexibilityController .getNextConstraintDropTimeElapsedLocked(js); - assertEquals(182240100L, nextTimeToDropNumConstraints); + assertEquals(FROZEN_TIME + 800000L + (200 * HOUR_IN_MILLIS) * 7 / 10, + nextTimeToDropNumConstraints); // no delay, no deadline jb = createJob(0); @@ -447,15 +478,15 @@ public class FlexibilityControllerTest { nextTimeToDropNumConstraints = mFlexibilityController .getNextConstraintDropTimeElapsedLocked(js); - assertEquals(129600100, nextTimeToDropNumConstraints); + assertEquals(FROZEN_TIME + (200 * HOUR_IN_MILLIS) / 2, nextTimeToDropNumConstraints); js.setNumDroppedFlexibleConstraints(1); nextTimeToDropNumConstraints = mFlexibilityController .getNextConstraintDropTimeElapsedLocked(js); - assertEquals(155520100L, nextTimeToDropNumConstraints); + assertEquals(FROZEN_TIME + (200 * HOUR_IN_MILLIS) * 6 / 10, nextTimeToDropNumConstraints); js.setNumDroppedFlexibleConstraints(2); nextTimeToDropNumConstraints = mFlexibilityController .getNextConstraintDropTimeElapsedLocked(js); - assertEquals(181440100L, nextTimeToDropNumConstraints); + assertEquals(FROZEN_TIME + (200 * HOUR_IN_MILLIS) * 7 / 10, nextTimeToDropNumConstraints); // delay, deadline jb = createJob(0) @@ -598,10 +629,10 @@ public class FlexibilityControllerTest { @Test public void testGetLifeCycleBeginningElapsedLocked_Prefetch() { // prefetch with lifecycle - when(mPrefetchController.getLaunchTimeThresholdMs()).thenReturn(700L); + doReturn(700L).when(mPrefetchController).getLaunchTimeThresholdMs(); JobInfo.Builder jb = createJob(0).setPrefetch(true); JobStatus js = createJobStatus("time", jb); - when(mPrefetchController.getNextEstimatedLaunchTimeLocked(js)).thenReturn(900L); + doReturn(900L).when(mPrefetchController).getNextEstimatedLaunchTimeLocked(js); assertEquals(900L - 700L, mFlexibilityController.getLifeCycleBeginningElapsedLocked(js)); // prefetch with enqueue jb = createJob(0).setPrefetch(true); @@ -616,7 +647,7 @@ public class FlexibilityControllerTest { // prefetch without estimate mFlexibilityController.mPrefetchLifeCycleStart .add(js.getUserId(), js.getSourcePackageName(), 500L); - when(mPrefetchController.getNextEstimatedLaunchTimeLocked(js)).thenReturn(Long.MAX_VALUE); + doReturn(Long.MAX_VALUE).when(mPrefetchController).getNextEstimatedLaunchTimeLocked(js); jb = createJob(0).setPrefetch(true); js = createJobStatus("time", jb); assertEquals(500L, mFlexibilityController.getLifeCycleBeginningElapsedLocked(js)); @@ -642,12 +673,12 @@ public class FlexibilityControllerTest { // prefetch no estimate JobInfo.Builder jb = createJob(0).setPrefetch(true); JobStatus js = createJobStatus("time", jb); - when(mPrefetchController.getNextEstimatedLaunchTimeLocked(js)).thenReturn(Long.MAX_VALUE); + doReturn(Long.MAX_VALUE).when(mPrefetchController).getNextEstimatedLaunchTimeLocked(js); assertEquals(Long.MAX_VALUE, mFlexibilityController.getLifeCycleEndElapsedLocked(js, 0)); // prefetch with estimate jb = createJob(0).setPrefetch(true); js = createJobStatus("time", jb); - when(mPrefetchController.getNextEstimatedLaunchTimeLocked(js)).thenReturn(1000L); + doReturn(1000L).when(mPrefetchController).getNextEstimatedLaunchTimeLocked(js); assertEquals(1000L, mFlexibilityController.getLifeCycleEndElapsedLocked(js, 0)); } @@ -696,7 +727,7 @@ public class FlexibilityControllerTest { // Stop satisfied constraints from causing a false positive. js.setNumAppliedFlexibleConstraints(100); synchronized (mFlexibilityController.mLock) { - when(mJobSchedulerService.isCurrentlyRunningLocked(js)).thenReturn(true); + doReturn(true).when(mJobSchedulerService).isCurrentlyRunningLocked(js); assertTrue(mFlexibilityController.isFlexibilitySatisfiedLocked(js)); } } @@ -847,14 +878,85 @@ public class FlexibilityControllerTest { } @Test + public void testAllowlistedAppBypass() { + setPowerWhitelistExceptIdle(); + mFlexibilityController.onSystemServicesReady(); + + JobStatus jsHigh = createJobStatus("testAllowlistedAppBypass", + createJob(0).setPriority(JobInfo.PRIORITY_HIGH)); + JobStatus jsDefault = createJobStatus("testAllowlistedAppBypass", + createJob(0).setPriority(JobInfo.PRIORITY_DEFAULT)); + JobStatus jsLow = createJobStatus("testAllowlistedAppBypass", + createJob(0).setPriority(JobInfo.PRIORITY_LOW)); + JobStatus jsMin = createJobStatus("testAllowlistedAppBypass", + createJob(0).setPriority(JobInfo.PRIORITY_MIN)); + jsHigh.setStandbyBucket(EXEMPTED_INDEX); + jsDefault.setStandbyBucket(EXEMPTED_INDEX); + jsLow.setStandbyBucket(EXEMPTED_INDEX); + jsMin.setStandbyBucket(EXEMPTED_INDEX); + + setPowerWhitelistExceptIdle(); + synchronized (mFlexibilityController.mLock) { + assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsHigh)); + assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsDefault)); + assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsLow)); + assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsMin)); + } + + setPowerWhitelistExceptIdle(SOURCE_PACKAGE); + synchronized (mFlexibilityController.mLock) { + assertTrue(mFlexibilityController.isFlexibilitySatisfiedLocked(jsHigh)); + assertTrue(mFlexibilityController.isFlexibilitySatisfiedLocked(jsDefault)); + assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsLow)); + assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsMin)); + } + } + + @Test + public void testForegroundAppBypass() { + JobStatus jsHigh = createJobStatus("testAllowlistedAppBypass", + createJob(0).setPriority(JobInfo.PRIORITY_HIGH)); + JobStatus jsDefault = createJobStatus("testAllowlistedAppBypass", + createJob(0).setPriority(JobInfo.PRIORITY_DEFAULT)); + JobStatus jsLow = createJobStatus("testAllowlistedAppBypass", + createJob(0).setPriority(JobInfo.PRIORITY_LOW)); + JobStatus jsMin = createJobStatus("testAllowlistedAppBypass", + createJob(0).setPriority(JobInfo.PRIORITY_MIN)); + + doReturn(JobInfo.BIAS_DEFAULT).when(mJobSchedulerService).getUidBias(mSourceUid); + synchronized (mFlexibilityController.mLock) { + assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsHigh)); + assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsDefault)); + assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsLow)); + assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsMin)); + } + + setUidBias(mSourceUid, JobInfo.BIAS_BOUND_FOREGROUND_SERVICE); + synchronized (mFlexibilityController.mLock) { + assertTrue(mFlexibilityController.isFlexibilitySatisfiedLocked(jsHigh)); + assertTrue(mFlexibilityController.isFlexibilitySatisfiedLocked(jsDefault)); + assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsLow)); + assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsMin)); + } + + setUidBias(mSourceUid, JobInfo.BIAS_FOREGROUND_SERVICE); + synchronized (mFlexibilityController.mLock) { + assertTrue(mFlexibilityController.isFlexibilitySatisfiedLocked(jsHigh)); + assertTrue(mFlexibilityController.isFlexibilitySatisfiedLocked(jsDefault)); + assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsLow)); + assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(jsMin)); + } + } + + @Test public void testTopAppBypass() { - JobInfo.Builder jb = createJob(0); + JobInfo.Builder jb = createJob(0).setPriority(JobInfo.PRIORITY_MIN); JobStatus js = createJobStatus("testTopAppBypass", jb); mJobStore.add(js); // Needed because if before and after Uid bias is the same, nothing happens. when(mJobSchedulerService.getUidBias(mSourceUid)) - .thenReturn(JobInfo.BIAS_FOREGROUND_SERVICE); + .thenReturn(JobInfo.BIAS_DEFAULT); synchronized (mFlexibilityController.mLock) { mFlexibilityController.maybeStartTrackingJobLocked(js, null); @@ -865,7 +967,7 @@ public class FlexibilityControllerTest { assertTrue(mFlexibilityController.isFlexibilitySatisfiedLocked(js)); assertTrue(js.isConstraintSatisfied(CONSTRAINT_FLEXIBLE)); - setUidBias(mSourceUid, JobInfo.BIAS_FOREGROUND_SERVICE); + setUidBias(mSourceUid, JobInfo.BIAS_SYNC_INITIALIZATION); assertFalse(mFlexibilityController.isFlexibilitySatisfiedLocked(js)); assertFalse(js.isConstraintSatisfied(CONSTRAINT_FLEXIBLE)); @@ -1187,9 +1289,9 @@ public class FlexibilityControllerTest { JobInfo.Builder jb = createJob(22).setPrefetch(true); JobStatus js = createJobStatus("onPrefetchCacheUpdated", jb); jobs.add(js); - when(mPrefetchController.getLaunchTimeThresholdMs()).thenReturn(7 * HOUR_IN_MILLIS); - when(mPrefetchController.getNextEstimatedLaunchTimeLocked(js)).thenReturn( - 1150L + mFlexibilityController.mConstants.PREFETCH_FORCE_BATCH_RELAX_THRESHOLD_MS); + doReturn(7 * HOUR_IN_MILLIS).when(mPrefetchController).getLaunchTimeThresholdMs(); + doReturn(1150L + mFlexibilityController.mConstants.PREFETCH_FORCE_BATCH_RELAX_THRESHOLD_MS) + .when(mPrefetchController).getNextEstimatedLaunchTimeLocked(js); mFlexibilityController.maybeStartTrackingJobLocked(js, null); @@ -1231,7 +1333,7 @@ public class FlexibilityControllerTest { final ArraySet<String> pkgs = new ArraySet<>(); pkgs.add(js.getSourcePackageName()); - when(mJobSchedulerService.getPackagesForUidLocked(mSourceUid)).thenReturn(pkgs); + doReturn(pkgs).when(mJobSchedulerService).getPackagesForUidLocked(mSourceUid); setUidBias(mSourceUid, BIAS_TOP_APP); setUidBias(mSourceUid, BIAS_FOREGROUND_SERVICE); @@ -1245,7 +1347,6 @@ public class FlexibilityControllerTest { setUidBias(mSourceUid, BIAS_FOREGROUND_SERVICE); assertEquals(100L, (long) mFlexibilityController .mPrefetchLifeCycleStart.get(js.getSourceUserId(), js.getSourcePackageName())); - } @Test @@ -1259,7 +1360,7 @@ public class FlexibilityControllerTest { } private void runTestUnsupportedDevice(String feature) { - when(mPackageManager.hasSystemFeature(feature)).thenReturn(true); + doReturn(true).when(mPackageManager).hasSystemFeature(feature); mFlexibilityController = new FlexibilityController(mJobSchedulerService, mPrefetchController); assertFalse(mFlexibilityController.isEnabled()); @@ -1279,6 +1380,16 @@ public class FlexibilityControllerTest { } } + private void setPowerWhitelistExceptIdle(String... packages) { + doReturn(packages == null ? EmptyArray.STRING : packages) + .when(mDeviceIdleInternal).getFullPowerWhitelistExceptIdle(); + if (mBroadcastReceiver != null) { + mBroadcastReceiver.onReceive(mContext, + new Intent(PowerManager.ACTION_POWER_SAVE_WHITELIST_CHANGED)); + waitForQuietModuleThread(); + } + } + private void setUidBias(int uid, int bias) { int prevBias = mJobSchedulerService.getUidBias(uid); doReturn(bias).when(mJobSchedulerService).getUidBias(uid); 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 0403c64fc624..ec7e35982311 100644 --- a/services/tests/mockingservicestests/src/com/android/server/pm/PackageArchiverTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/pm/PackageArchiverTest.java @@ -233,7 +233,7 @@ public class PackageArchiverTest { Exception e = assertThrows( SecurityException.class, () -> mArchiveManager.requestArchive(PACKAGE, "different", mIntentSender, - UserHandle.CURRENT, 0)); + UserHandle.CURRENT)); assertThat(e).hasMessageThat().isEqualTo( String.format( "The UID %s of callerPackageName set by the caller doesn't match the " @@ -250,7 +250,7 @@ public class PackageArchiverTest { Exception e = assertThrows( ParcelableException.class, () -> mArchiveManager.requestArchive(PACKAGE, CALLER_PACKAGE, mIntentSender, - UserHandle.CURRENT, 0)); + UserHandle.CURRENT)); assertThat(e.getCause()).isInstanceOf(PackageManager.NameNotFoundException.class); assertThat(e.getCause()).hasMessageThat().isEqualTo( String.format("Package %s not found.", PACKAGE)); @@ -260,8 +260,7 @@ public class PackageArchiverTest { public void archiveApp_packageNotInstalledForUser() throws IntentSender.SendIntentException { mPackageSetting.modifyUserState(UserHandle.CURRENT.getIdentifier()).setInstalled(false); - mArchiveManager.requestArchive(PACKAGE, CALLER_PACKAGE, mIntentSender, UserHandle.CURRENT, - 0); + mArchiveManager.requestArchive(PACKAGE, CALLER_PACKAGE, mIntentSender, UserHandle.CURRENT); rule.mocks().getHandler().flush(); ArgumentCaptor<Intent> intentCaptor = ArgumentCaptor.forClass(Intent.class); @@ -291,7 +290,7 @@ public class PackageArchiverTest { Exception e = assertThrows( ParcelableException.class, () -> mArchiveManager.requestArchive(PACKAGE, CALLER_PACKAGE, mIntentSender, - UserHandle.CURRENT, 0)); + UserHandle.CURRENT)); assertThat(e.getCause()).isInstanceOf(PackageManager.NameNotFoundException.class); assertThat(e.getCause()).hasMessageThat().isEqualTo("No installer found"); } @@ -305,7 +304,7 @@ public class PackageArchiverTest { Exception e = assertThrows( ParcelableException.class, () -> mArchiveManager.requestArchive(PACKAGE, CALLER_PACKAGE, mIntentSender, - UserHandle.CURRENT, 0)); + UserHandle.CURRENT)); assertThat(e.getCause()).isInstanceOf(PackageManager.NameNotFoundException.class); assertThat(e.getCause()).hasMessageThat().isEqualTo( "Installer does not support unarchival"); @@ -319,7 +318,7 @@ public class PackageArchiverTest { Exception e = assertThrows( ParcelableException.class, () -> mArchiveManager.requestArchive(PACKAGE, CALLER_PACKAGE, mIntentSender, - UserHandle.CURRENT, 0)); + UserHandle.CURRENT)); assertThat(e.getCause()).isInstanceOf(PackageManager.NameNotFoundException.class); assertThat(e.getCause()).hasMessageThat().isEqualTo( TextUtils.formatSimple("The app %s does not have a main activity.", PACKAGE)); @@ -331,8 +330,7 @@ public class PackageArchiverTest { doThrow(e).when(mArchiveManager).storeIcon(eq(PACKAGE), any(LauncherActivityInfo.class), eq(mUserId), anyInt(), anyInt()); - mArchiveManager.requestArchive(PACKAGE, CALLER_PACKAGE, mIntentSender, UserHandle.CURRENT, - 0); + mArchiveManager.requestArchive(PACKAGE, CALLER_PACKAGE, mIntentSender, UserHandle.CURRENT); rule.mocks().getHandler().flush(); ArgumentCaptor<Intent> intentCaptor = ArgumentCaptor.forClass(Intent.class); @@ -355,7 +353,7 @@ public class PackageArchiverTest { Exception e = assertThrows( ParcelableException.class, () -> mArchiveManager.requestArchive(PACKAGE, CALLER_PACKAGE, mIntentSender, - UserHandle.CURRENT, 0)); + UserHandle.CURRENT)); assertThat(e.getCause()).isInstanceOf(PackageManager.NameNotFoundException.class); assertThat(e.getCause()).hasMessageThat().isEqualTo( TextUtils.formatSimple("The app %s is opted out of archiving.", PACKAGE)); @@ -363,8 +361,7 @@ public class PackageArchiverTest { @Test public void archiveApp_withNoAdditionalFlags_success() { - mArchiveManager.requestArchive(PACKAGE, CALLER_PACKAGE, mIntentSender, UserHandle.CURRENT, - 0); + mArchiveManager.requestArchive(PACKAGE, CALLER_PACKAGE, mIntentSender, UserHandle.CURRENT); rule.mocks().getHandler().flush(); verify(mInstallerService).uninstall( @@ -386,14 +383,13 @@ public class PackageArchiverTest { @Test public void archiveApp_withAdditionalFlags_success() { - mArchiveManager.requestArchive(PACKAGE, CALLER_PACKAGE, mIntentSender, UserHandle.CURRENT, - PackageManager.DELETE_SHOW_DIALOG); + mArchiveManager.requestArchive(PACKAGE, CALLER_PACKAGE, mIntentSender, UserHandle.CURRENT); rule.mocks().getHandler().flush(); verify(mInstallerService).uninstall( eq(new VersionedPackage(PACKAGE, PackageManager.VERSION_CODE_HIGHEST)), eq(CALLER_PACKAGE), - eq(DELETE_ARCHIVE | DELETE_KEEP_DATA | PackageManager.DELETE_SHOW_DIALOG), + eq(DELETE_ARCHIVE | DELETE_KEEP_DATA), eq(mIntentSender), eq(UserHandle.CURRENT.getIdentifier()), anyInt()); 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 fd6aa0c1ffab..e6298eeccafb 100644 --- a/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerServiceTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerServiceTest.java @@ -19,9 +19,11 @@ import static android.os.UserManager.DISALLOW_OUTGOING_CALLS; import static android.os.UserManager.DISALLOW_SMS; import static android.os.UserManager.DISALLOW_USER_SWITCH; import static android.os.UserManager.USER_TYPE_FULL_SECONDARY; +import static android.os.UserManager.USER_TYPE_PROFILE_PRIVATE; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertWithMessage; @@ -30,20 +32,27 @@ import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.any; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.when; import android.annotation.UserIdInt; import android.app.ActivityManagerInternal; +import android.app.KeyguardManager; import android.content.Context; import android.content.pm.PackageManagerInternal; import android.content.pm.UserInfo; +import android.multiuser.Flags; +import android.os.PowerManager; import android.os.SystemProperties; import android.os.UserHandle; import android.os.UserManager; import android.os.storage.StorageManager; +import android.platform.test.flag.junit.SetFlagsRule; import android.provider.Settings; import android.util.Log; import android.util.Pair; @@ -52,6 +61,7 @@ import android.util.Xml; import androidx.test.annotation.UiThreadTest; +import com.android.dx.mockito.inline.extended.MockedVoidMethod; import com.android.internal.widget.LockSettingsInternal; import com.android.modules.utils.TypedXmlPullParser; import com.android.modules.utils.testing.ExtendedMockitoRule; @@ -65,6 +75,7 @@ import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.mockito.Mock; +import org.mockito.Mockito; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; @@ -115,8 +126,11 @@ public final class UserManagerServiceTest { .spyStatic(LocalServices.class) .spyStatic(SystemProperties.class) .mockStatic(Settings.Global.class) + .mockStatic(Settings.Secure.class) .build(); + @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + private final Object mPackagesLock = new Object(); private final Context mRealContext = androidx.test.InstrumentationRegistry.getInstrumentation() .getTargetContext(); @@ -133,6 +147,8 @@ public final class UserManagerServiceTest { private @Mock StorageManager mStorageManager; private @Mock LockSettingsInternal mLockSettingsInternal; private @Mock PackageManagerInternal mPackageManagerInternal; + private @Mock KeyguardManager mKeyguardManager; + private @Mock PowerManager mPowerManager; /** * Reference to the {@link UserManagerService} being tested. @@ -156,6 +172,8 @@ public final class UserManagerServiceTest { when(mDeviceStorageMonitorInternal.isMemoryLow()).thenReturn(false); mockGetLocalService(DeviceStorageMonitorInternal.class, mDeviceStorageMonitorInternal); when(mSpiedContext.getSystemService(StorageManager.class)).thenReturn(mStorageManager); + when(mSpiedContext.getSystemService(KeyguardManager.class)).thenReturn(mKeyguardManager); + when(mSpiedContext.getSystemService(PowerManager.class)).thenReturn(mPowerManager); mockGetLocalService(LockSettingsInternal.class, mLockSettingsInternal); mockGetLocalService(PackageManagerInternal.class, mPackageManagerInternal); doNothing().when(mSpiedContext).sendBroadcastAsUser(any(), any(), any()); @@ -550,6 +568,143 @@ public final class UserManagerServiceTest { assertTrue(hasRestrictionsInUserXMLFile(user.id)); } + @Test + public void testAutoLockOnDeviceLockForPrivateProfile() { + mSetFlagsRule.enableFlags(Flags.FLAG_SUPPORT_AUTOLOCK_FOR_PRIVATE_SPACE); + UserManagerService mSpiedUms = spy(mUms); + UserInfo privateProfileUser = + mSpiedUms.createProfileForUserEvenWhenDisallowedWithThrow("TestPrivateProfile", + USER_TYPE_PROFILE_PRIVATE, 0, 0, null); + mockAutoLockForPrivateSpace(Settings.Secure.PRIVATE_SPACE_AUTO_LOCK_ON_DEVICE_LOCK); + Mockito.doNothing().when(mSpiedUms).setQuietModeEnabledAsync( + eq(privateProfileUser.getUserHandle().getIdentifier()), eq(true), any(), + any()); + + mSpiedUms.tryAutoLockingPrivateSpaceOnKeyguardChanged(true); + + Mockito.verify(mSpiedUms).setQuietModeEnabledAsync( + eq(privateProfileUser.getUserHandle().getIdentifier()), eq(true), + any(), any()); + } + + @Test + public void testAutoLockOnDeviceLockForPrivateProfile_keyguardUnlocked() { + mSetFlagsRule.enableFlags(Flags.FLAG_SUPPORT_AUTOLOCK_FOR_PRIVATE_SPACE); + UserManagerService mSpiedUms = spy(mUms); + UserInfo privateProfileUser = + mSpiedUms.createProfileForUserEvenWhenDisallowedWithThrow("TestPrivateProfile", + USER_TYPE_PROFILE_PRIVATE, 0, 0, null); + mockAutoLockForPrivateSpace(Settings.Secure.PRIVATE_SPACE_AUTO_LOCK_ON_DEVICE_LOCK); + + mSpiedUms.tryAutoLockingPrivateSpaceOnKeyguardChanged(false); + + // Verify that no operation to disable quiet mode is not called + Mockito.verify(mSpiedUms, never()).setQuietModeEnabledAsync( + eq(privateProfileUser.getUserHandle().getIdentifier()), eq(true), + any(), any()); + } + + @Test + public void testAutoLockOnDeviceLockForPrivateProfile_flagDisabled() { + mSetFlagsRule.disableFlags(Flags.FLAG_SUPPORT_AUTOLOCK_FOR_PRIVATE_SPACE); + UserManagerService mSpiedUms = spy(mUms); + UserInfo privateProfileUser = + mSpiedUms.createProfileForUserEvenWhenDisallowedWithThrow("TestPrivateProfile", + USER_TYPE_PROFILE_PRIVATE, 0, 0, null); + + mSpiedUms.tryAutoLockingPrivateSpaceOnKeyguardChanged(true); + + // Verify that no auto-lock operations take place + verify((MockedVoidMethod) () -> Settings.Secure.getInt(any(), + eq(Settings.Secure.PRIVATE_SPACE_AUTO_LOCK), anyInt()), never()); + Mockito.verify(mSpiedUms, never()).setQuietModeEnabledAsync( + eq(privateProfileUser.getUserHandle().getIdentifier()), eq(true), + any(), any()); + } + + @Test + public void testAutoLockAfterInactityForPrivateProfile() { + mSetFlagsRule.enableFlags(Flags.FLAG_SUPPORT_AUTOLOCK_FOR_PRIVATE_SPACE); + UserManagerService mSpiedUms = spy(mUms); + mockAutoLockForPrivateSpace(Settings.Secure.PRIVATE_SPACE_AUTO_LOCK_AFTER_INACTIVITY); + when(mPowerManager.isInteractive()).thenReturn(false); + + UserInfo privateProfileUser = + mSpiedUms.createProfileForUserEvenWhenDisallowedWithThrow("TestPrivateProfile", + USER_TYPE_PROFILE_PRIVATE, 0, 0, null); + Mockito.doNothing().when(mSpiedUms).scheduleMessageToAutoLockPrivateSpace( + eq(privateProfileUser.getUserHandle().getIdentifier()), any(), + anyLong()); + + + mSpiedUms.maybeScheduleMessageToAutoLockPrivateSpace(); + + Mockito.verify(mSpiedUms).scheduleMessageToAutoLockPrivateSpace( + eq(privateProfileUser.getUserHandle().getIdentifier()), any(), anyLong()); + } + + @Test + public void testSetOrUpdateAutoLockPreference_noPrivateProfile() { + mSetFlagsRule.enableFlags(Flags.FLAG_SUPPORT_AUTOLOCK_FOR_PRIVATE_SPACE); + + mUms.setOrUpdateAutoLockPreferenceForPrivateProfile( + Settings.Secure.PRIVATE_SPACE_AUTO_LOCK_AFTER_INACTIVITY); + + Mockito.verify(mSpiedContext, never()).registerReceiver(any(), any(), any(), any()); + Mockito.verify(mSpiedContext, never()).unregisterReceiver(any()); + Mockito.verify(mKeyguardManager, never()).removeKeyguardLockedStateListener((any())); + Mockito.verify(mKeyguardManager, never()).addKeyguardLockedStateListener(any(), any()); + } + + @Test + public void testSetOrUpdateAutoLockPreference() { + mSetFlagsRule.enableFlags(Flags.FLAG_SUPPORT_AUTOLOCK_FOR_PRIVATE_SPACE); + mUms.createProfileForUserEvenWhenDisallowedWithThrow("TestPrivateProfile", + USER_TYPE_PROFILE_PRIVATE, 0, 0, null); + + // Set the preference to auto lock on device lock + mUms.setOrUpdateAutoLockPreferenceForPrivateProfile( + Settings.Secure.PRIVATE_SPACE_AUTO_LOCK_ON_DEVICE_LOCK); + + // Verify that keyguard state listener was added + Mockito.verify(mKeyguardManager).addKeyguardLockedStateListener(any(), any()); + //Verity that keyguard state listener was not removed + Mockito.verify(mKeyguardManager, never()).removeKeyguardLockedStateListener(any()); + // Broadcasts are already unregistered when UserManagerService starts and the flag + // isDeviceInactivityBroadcastReceiverRegistered is false + Mockito.verify(mSpiedContext, never()).registerReceiver(any(), any(), any(), any()); + Mockito.verify(mSpiedContext, never()).unregisterReceiver(any()); + + Mockito.clearInvocations(mKeyguardManager); + Mockito.clearInvocations(mSpiedContext); + + // Now set the preference to auto-lock on inactivity + mUms.setOrUpdateAutoLockPreferenceForPrivateProfile( + Settings.Secure.PRIVATE_SPACE_AUTO_LOCK_AFTER_INACTIVITY); + + // Verify that inactivity broadcasts are registered + Mockito.verify(mSpiedContext, times(2)).registerReceiver(any(), any(), any(), any()); + // Verify that keyguard state listener is removed + Mockito.verify(mKeyguardManager).removeKeyguardLockedStateListener(any()); + // Verify that all other operations don't take place + Mockito.verify(mSpiedContext, never()).unregisterReceiver(any()); + Mockito.verify(mKeyguardManager, never()).addKeyguardLockedStateListener(any(), any()); + + Mockito.clearInvocations(mKeyguardManager); + Mockito.clearInvocations(mSpiedContext); + + // Finally, set the preference to don't auto-lock + mUms.setOrUpdateAutoLockPreferenceForPrivateProfile( + Settings.Secure.PRIVATE_SPACE_AUTO_LOCK_NEVER); + + // Verify that inactivity broadcasts are unregistered and keyguard listener was removed + Mockito.verify(mSpiedContext).unregisterReceiver(any()); + Mockito.verify(mKeyguardManager).removeKeyguardLockedStateListener(any()); + // Verify that no broadcasts were registered and no listeners were added + Mockito.verify(mSpiedContext, never()).registerReceiver(any(), any(), any(), any()); + Mockito.verify(mKeyguardManager, never()).addKeyguardLockedStateListener(any(), any()); + } + /** * Returns true if the user's XML file has Default restrictions * @param userId Id of the user. @@ -632,6 +787,12 @@ public final class UserManagerServiceTest { SystemProperties.getBoolean(eq("fw.show_multiuserui"), anyBoolean())); } + private void mockAutoLockForPrivateSpace(int val) { + doReturn(val).when(() -> + Settings.Secure.getIntForUser(any(), eq(Settings.Secure.PRIVATE_SPACE_AUTO_LOCK), + anyInt(), anyInt())); + } + private void mockCurrentUser(@UserIdInt int userId) { mockGetLocalService(ActivityManagerInternal.class, mActivityManagerInternal); diff --git a/services/tests/servicestests/Android.bp b/services/tests/servicestests/Android.bp index 00450267ee79..0831086b28ca 100644 --- a/services/tests/servicestests/Android.bp +++ b/services/tests/servicestests/Android.bp @@ -29,8 +29,6 @@ android_test { "src/**/*.java", "src/**/*.kt", - "test-apps/JobTestApp/src/**/*.java", - "test-apps/SuspendTestApp/src/**/*.java", ], static_libs: [ @@ -124,7 +122,6 @@ android_test { }, data: [ - ":JobTestApp", ":SimpleServiceTestApp1", ":SimpleServiceTestApp2", ":SimpleServiceTestApp3", diff --git a/services/tests/servicestests/AndroidTest.xml b/services/tests/servicestests/AndroidTest.xml index b1d50399416a..27c522d68119 100644 --- a/services/tests/servicestests/AndroidTest.xml +++ b/services/tests/servicestests/AndroidTest.xml @@ -29,7 +29,6 @@ <option name="cleanup-apks" value="true" /> <option name="install-arg" value="-t" /> <option name="test-file-name" value="FrameworksServicesTests.apk" /> - <option name="test-file-name" value="JobTestApp.apk" /> <option name="test-file-name" value="SuspendTestApp.apk" /> <option name="test-file-name" value="SimpleServiceTestApp1.apk" /> <option name="test-file-name" value="SimpleServiceTestApp2.apk" /> diff --git a/services/tests/servicestests/src/com/android/server/BootReceiverTest.java b/services/tests/servicestests/src/com/android/server/BootReceiverTest.java deleted file mode 100644 index 523c5c060cf5..000000000000 --- a/services/tests/servicestests/src/com/android/server/BootReceiverTest.java +++ /dev/null @@ -1,97 +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.server; - -import static com.google.common.truth.Truth.assertThat; - -import android.test.AndroidTestCase; - -import com.android.server.os.TombstoneProtos; -import com.android.server.os.TombstoneProtos.Tombstone; - -public class BootReceiverTest extends AndroidTestCase { - private static final String TAG = "BootReceiverTest"; - - public void testRemoveMemoryFromTombstone() { - Tombstone tombstoneBase = Tombstone.newBuilder() - .setBuildFingerprint("build_fingerprint") - .setRevision("revision") - .setPid(123) - .setTid(23) - .setUid(34) - .setSelinuxLabel("selinux_label") - .addCommandLine("cmd1") - .addCommandLine("cmd2") - .addCommandLine("cmd3") - .setProcessUptime(300) - .setAbortMessage("abort") - .addCauses(TombstoneProtos.Cause.newBuilder() - .setHumanReadable("cause1") - .setMemoryError(TombstoneProtos.MemoryError.newBuilder() - .setTool(TombstoneProtos.MemoryError.Tool.SCUDO) - .setType(TombstoneProtos.MemoryError.Type.DOUBLE_FREE))) - .addLogBuffers(TombstoneProtos.LogBuffer.newBuilder().setName("name").addLogs( - TombstoneProtos.LogMessage.newBuilder() - .setTimestamp("123") - .setMessage("message"))) - .addOpenFds(TombstoneProtos.FD.newBuilder().setFd(1).setPath("path")) - .build(); - - Tombstone tombstoneWithoutMemory = tombstoneBase.toBuilder() - .putThreads(1, TombstoneProtos.Thread.newBuilder() - .setId(1) - .setName("thread1") - .addRegisters(TombstoneProtos.Register.newBuilder().setName("r1").setU64(1)) - .addRegisters(TombstoneProtos.Register.newBuilder().setName("r2").setU64(2)) - .addBacktraceNote("backtracenote1") - .addUnreadableElfFiles("files1") - .setTaggedAddrCtrl(1) - .setPacEnabledKeys(10) - .build()) - .build(); - - Tombstone tombstoneWithMemory = tombstoneBase.toBuilder() - .addMemoryMappings(TombstoneProtos.MemoryMapping.newBuilder() - .setBeginAddress(1) - .setEndAddress(100) - .setOffset(10) - .setRead(true) - .setWrite(true) - .setExecute(false) - .setMappingName("mapping") - .setBuildId("build") - .setLoadBias(70)) - .putThreads(1, TombstoneProtos.Thread.newBuilder() - .setId(1) - .setName("thread1") - .addRegisters(TombstoneProtos.Register.newBuilder().setName("r1").setU64(1)) - .addRegisters(TombstoneProtos.Register.newBuilder().setName("r2").setU64(2)) - .addBacktraceNote("backtracenote1") - .addUnreadableElfFiles("files1") - .addMemoryDump(TombstoneProtos.MemoryDump.newBuilder() - .setRegisterName("register1") - .setMappingName("mapping") - .setBeginAddress(10)) - .setTaggedAddrCtrl(1) - .setPacEnabledKeys(10) - .build()) - .build(); - - assertThat(BootReceiver.removeMemoryFromTombstone(tombstoneWithMemory)) - .isEqualTo(tombstoneWithoutMemory); - } -} diff --git a/services/tests/servicestests/src/com/android/server/biometrics/log/BiometricContextProviderTest.java b/services/tests/servicestests/src/com/android/server/biometrics/log/BiometricContextProviderTest.java index 1b9e6fb6e247..a8eace05de97 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/log/BiometricContextProviderTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/log/BiometricContextProviderTest.java @@ -153,6 +153,15 @@ public class BiometricContextProviderTest { } @Test + public void testGetIsHardwareIgnoringTouches() throws RemoteException { + mListener.onHardwareIgnoreTouchesChanged(true); + assertThat(mProvider.isHardwareIgnoringTouches()).isTrue(); + + mListener.onHardwareIgnoreTouchesChanged(false); + assertThat(mProvider.isHardwareIgnoringTouches()).isFalse(); + } + + @Test public void testGetDockedState() { final List<Integer> states = List.of(Intent.EXTRA_DOCK_STATE_DESK, Intent.EXTRA_DOCK_STATE_CAR, Intent.EXTRA_DOCK_STATE_UNDOCKED); diff --git a/services/tests/servicestests/src/com/android/server/biometrics/log/BiometricFrameworkStatsLoggerTest.java b/services/tests/servicestests/src/com/android/server/biometrics/log/BiometricFrameworkStatsLoggerTest.java index 5cff48dc3c2a..41193527503b 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/log/BiometricFrameworkStatsLoggerTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/log/BiometricFrameworkStatsLoggerTest.java @@ -18,6 +18,7 @@ package com.android.server.biometrics.log; import static com.google.common.truth.Truth.assertThat; +import android.hardware.biometrics.BiometricAuthenticator; import android.hardware.biometrics.BiometricsProtoEnums; import android.hardware.biometrics.common.AuthenticateReason; import android.hardware.biometrics.common.OperationContext; @@ -48,11 +49,13 @@ public class BiometricFrameworkStatsLoggerTest { public void testConvertsWakeReason_whenPowerReason() { final OperationContext context = new OperationContext(); context.wakeReason = WakeReason.WAKE_MOTION; - final OperationContextExt ctx = new OperationContextExt(context, false); + final OperationContextExt ctx = new OperationContextExt(context, false, + BiometricAuthenticator.TYPE_NONE); final int reason = BiometricFrameworkStatsLogger.toProtoWakeReason(ctx); final int[] reasonDetails = BiometricFrameworkStatsLogger - .toProtoWakeReasonDetails(new OperationContextExt(context, false)); + .toProtoWakeReasonDetails( + new OperationContextExt(context, false, BiometricAuthenticator.TYPE_NONE)); assertThat(reason).isEqualTo(BiometricsProtoEnums.WAKE_REASON_WAKE_MOTION); assertThat(reasonDetails).isEmpty(); @@ -63,7 +66,8 @@ public class BiometricFrameworkStatsLoggerTest { final OperationContext context = new OperationContext(); context.authenticateReason = AuthenticateReason.faceAuthenticateReason( AuthenticateReason.Face.ASSISTANT_VISIBLE); - final OperationContextExt ctx = new OperationContextExt(context, false); + final OperationContextExt ctx = new OperationContextExt(context, false, + BiometricAuthenticator.TYPE_NONE); final int reason = BiometricFrameworkStatsLogger.toProtoWakeReason(ctx); final int[] reasonDetails = BiometricFrameworkStatsLogger @@ -79,7 +83,8 @@ public class BiometricFrameworkStatsLoggerTest { final OperationContext context = new OperationContext(); context.authenticateReason = AuthenticateReason.vendorAuthenticateReason( new AuthenticateReason.Vendor()); - final OperationContextExt ctx = new OperationContextExt(context, false); + final OperationContextExt ctx = new OperationContextExt(context, false, + BiometricAuthenticator.TYPE_NONE); final int reason = BiometricFrameworkStatsLogger.toProtoWakeReason(ctx); final int[] reasonDetails = BiometricFrameworkStatsLogger @@ -96,7 +101,8 @@ public class BiometricFrameworkStatsLoggerTest { context.wakeReason = WakeReason.WAKE_KEY; context.authenticateReason = AuthenticateReason.faceAuthenticateReason( AuthenticateReason.Face.PRIMARY_BOUNCER_SHOWN); - final OperationContextExt ctx = new OperationContextExt(context, false); + final OperationContextExt ctx = new OperationContextExt(context, false, + BiometricAuthenticator.TYPE_NONE); final int reason = BiometricFrameworkStatsLogger.toProtoWakeReason(ctx); final int[] reasonDetails = BiometricFrameworkStatsLogger @@ -113,7 +119,8 @@ public class BiometricFrameworkStatsLoggerTest { context.wakeReason = WakeReason.LID; context.authenticateReason = AuthenticateReason.vendorAuthenticateReason( new AuthenticateReason.Vendor()); - final OperationContextExt ctx = new OperationContextExt(context, false); + final OperationContextExt ctx = new OperationContextExt(context, false, + BiometricAuthenticator.TYPE_NONE); final int reason = BiometricFrameworkStatsLogger.toProtoWakeReason(ctx); final int[] reasonDetails = BiometricFrameworkStatsLogger diff --git a/services/tests/servicestests/src/com/android/server/biometrics/log/OperationContextExtTest.java b/services/tests/servicestests/src/com/android/server/biometrics/log/OperationContextExtTest.java index 32284fd7541a..767b4262bb18 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/log/OperationContextExtTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/log/OperationContextExtTest.java @@ -18,17 +18,19 @@ package com.android.server.biometrics.log; import static com.google.common.truth.Truth.assertThat; +import static org.mockito.Mockito.when; + import android.content.Intent; import android.hardware.biometrics.AuthenticateOptions; +import android.hardware.biometrics.BiometricAuthenticator; import android.hardware.biometrics.IBiometricContextListener; import android.hardware.biometrics.common.DisplayState; import android.hardware.biometrics.common.OperationContext; import android.hardware.biometrics.common.OperationReason; +import android.hardware.biometrics.common.OperationState; import android.platform.test.annotations.Presubmit; import android.view.Surface; -import static org.mockito.Mockito.when; - import androidx.test.filters.SmallTest; import com.android.internal.logging.InstanceId; @@ -58,7 +60,7 @@ public class OperationContextExtTest { final OperationContext aidlContext = newAidlContext(); - context = new OperationContextExt(aidlContext, false); + context = new OperationContextExt(aidlContext, false, BiometricAuthenticator.TYPE_NONE); assertThat(context.toAidlContext()).isSameInstanceAs(aidlContext); final int id = 5; @@ -96,7 +98,8 @@ public class OperationContextExtTest { ); for (Map.Entry<Integer, Integer> entry : map.entrySet()) { - final OperationContextExt context = new OperationContextExt(newAidlContext(), true); + final OperationContextExt context = new OperationContextExt(newAidlContext(), true, + BiometricAuthenticator.TYPE_NONE); when(mBiometricContext.getDisplayState()).thenReturn(entry.getKey()); assertThat(context.update(mBiometricContext, context.isCrypto()).getDisplayState()) .isEqualTo(entry.getValue()); @@ -124,7 +127,7 @@ public class OperationContextExtTest { updatesFromSource(null, OperationReason.UNKNOWN); } - private void updatesFromSource(BiometricContextSessionInfo sessionInfo, int sessionType) { + private void updatesFromSource(BiometricContextSessionInfo sessionInfo, int sessionType) { final int rotation = Surface.ROTATION_270; final int foldState = IBiometricContextListener.FoldState.HALF_OPENED; final int dockState = Intent.EXTRA_DOCK_STATE_CAR; @@ -135,9 +138,11 @@ public class OperationContextExtTest { when(mBiometricContext.getDockedState()).thenReturn(dockState); when(mBiometricContext.isDisplayOn()).thenReturn(true); when(mBiometricContext.getDisplayState()).thenReturn(displayState); + when(mBiometricContext.isHardwareIgnoringTouches()).thenReturn(true); final OperationContextExt context = new OperationContextExt(newAidlContext(), - sessionType == OperationReason.BIOMETRIC_PROMPT); + sessionType == OperationReason.BIOMETRIC_PROMPT, + BiometricAuthenticator.TYPE_FINGERPRINT); assertThat(context.update(mBiometricContext, context.isCrypto())).isSameInstanceAs(context); @@ -154,6 +159,46 @@ public class OperationContextExtTest { assertThat(context.getOrientation()).isEqualTo(rotation); assertThat(context.isDisplayOn()).isTrue(); assertThat(context.getDisplayState()).isEqualTo(DisplayState.AOD); + assertThat( + context.getOperationState().getFingerprintOperationState().isHardwareIgnoringTouches + ).isTrue(); + } + + @Test + public void hasNullOperationState() { + OperationContextExt context = new OperationContextExt(false); + assertThat(context.toAidlContext()).isNotNull(); + + final OperationContext aidlContext = newAidlContext(); + + context = new OperationContextExt(aidlContext, false, BiometricAuthenticator.TYPE_NONE); + assertThat(context.getOperationState()).isNull(); + } + + @Test + public void hasFaceOperationState() { + OperationContextExt context = new OperationContextExt(false); + assertThat(context.toAidlContext()).isNotNull(); + + final OperationContext aidlContext = newAidlContext(); + + context = new OperationContextExt(aidlContext, false, + BiometricAuthenticator.TYPE_FACE); + assertThat(context.getOperationState().getTag()).isEqualTo( + OperationState.faceOperationState); + } + + @Test + public void hasFingerprintOperationState() { + OperationContextExt context = new OperationContextExt(false); + assertThat(context.toAidlContext()).isNotNull(); + + final OperationContext aidlContext = newAidlContext(); + + context = new OperationContextExt(aidlContext, false, + BiometricAuthenticator.TYPE_FINGERPRINT); + assertThat(context.getOperationState().getTag()).isEqualTo( + OperationState.fingerprintOperationState); } private static OperationContext newAidlContext() { diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/AcquisitionClientTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/AcquisitionClientTest.java index 2d9d868f2f74..4604b310edf7 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/AcquisitionClientTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/AcquisitionClientTest.java @@ -72,6 +72,7 @@ public class AcquisitionClientTest { mToken, mClientCallback); client.start(mSchedulerCallback); assertTrue(client.mHalOperationRunning); + verify(mClientCallback).getModality(); verify(mSchedulerCallback).onClientStarted(eq(client)); // Pretend that it got canceled by the user. diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java index e1f490ae3e2f..d71844b00b3b 100644 --- a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java @@ -608,6 +608,20 @@ public class VirtualDeviceManagerServiceTest { } @Test + public void testIsInputDeviceOwnedByVirtualDevice() { + assertThat(mLocalService.isInputDeviceOwnedByVirtualDevice(INPUT_DEVICE_ID)).isFalse(); + + final int fd = 1; + mInputController.addDeviceForTesting(BINDER, fd, + InputController.InputDeviceDescriptor.TYPE_KEYBOARD, DISPLAY_ID_1, PHYS, + DEVICE_NAME_1, INPUT_DEVICE_ID); + assertThat(mLocalService.isInputDeviceOwnedByVirtualDevice(INPUT_DEVICE_ID)).isTrue(); + + mInputController.unregisterInputDevice(BINDER); + assertThat(mLocalService.isInputDeviceOwnedByVirtualDevice(INPUT_DEVICE_ID)).isFalse(); + } + + @Test public void getDeviceIdsForUid_noRunningApps_returnsNull() { assertThat(mLocalService.getDeviceIdsForUid(UID_1)).isEmpty(); assertThat(mVdmNative.getDeviceIdsForUid(UID_1)).isEmpty(); @@ -1957,7 +1971,7 @@ public class VirtualDeviceManagerServiceTest { mRunningAppsChangedCallback, params, new DisplayManagerGlobal(mIDisplayManager), - new VirtualCameraController()); + new VirtualCameraController(DEVICE_POLICY_DEFAULT)); mVdms.addVirtualDevice(virtualDeviceImpl); assertThat(virtualDeviceImpl.getAssociationId()).isEqualTo(mAssociationInfo.getId()); assertThat(virtualDeviceImpl.getPersistentDeviceId()) diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/camera/VirtualCameraControllerTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/camera/VirtualCameraControllerTest.java index 9b28b817a1b9..3e4f1df0e1d4 100644 --- a/services/tests/servicestests/src/com/android/server/companion/virtual/camera/VirtualCameraControllerTest.java +++ b/services/tests/servicestests/src/com/android/server/companion/virtual/camera/VirtualCameraControllerTest.java @@ -16,27 +16,35 @@ package com.android.server.companion.virtual.camera; +import static android.companion.virtual.VirtualDeviceParams.DEVICE_POLICY_CUSTOM; +import static android.companion.virtual.VirtualDeviceParams.DEVICE_POLICY_DEFAULT; +import static android.companion.virtual.camera.VirtualCameraConfig.SENSOR_ORIENTATION_0; +import static android.companion.virtual.camera.VirtualCameraConfig.SENSOR_ORIENTATION_90; +import static android.graphics.ImageFormat.YUV_420_888; +import static android.graphics.PixelFormat.RGBA_8888; +import static android.hardware.camera2.CameraMetadata.LENS_FACING_BACK; +import static android.hardware.camera2.CameraMetadata.LENS_FACING_FRONT; + import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import android.annotation.NonNull; import android.companion.virtual.camera.VirtualCameraCallback; import android.companion.virtual.camera.VirtualCameraConfig; -import android.companion.virtual.camera.VirtualCameraStreamConfig; import android.companion.virtualcamera.IVirtualCameraService; import android.companion.virtualcamera.VirtualCameraConfiguration; -import android.graphics.ImageFormat; import android.os.Handler; import android.os.HandlerExecutor; import android.os.Looper; import android.platform.test.annotations.Presubmit; -import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; -import android.view.Surface; + +import junitparams.JUnitParamsRunner; +import junitparams.Parameters; import org.junit.After; import org.junit.Before; @@ -49,21 +57,30 @@ import org.mockito.MockitoAnnotations; import java.util.List; @Presubmit -@RunWith(AndroidTestingRunner.class) +@RunWith(JUnitParamsRunner.class) @TestableLooper.RunWithLooper(setAsMainLooper = true) public class VirtualCameraControllerTest { private static final String CAMERA_NAME_1 = "Virtual camera 1"; private static final int CAMERA_WIDTH_1 = 100; private static final int CAMERA_HEIGHT_1 = 200; + private static final int CAMERA_FORMAT_1 = YUV_420_888; + private static final int CAMERA_MAX_FPS_1 = 30; + private static final int CAMERA_SENSOR_ORIENTATION_1 = SENSOR_ORIENTATION_0; + private static final int CAMERA_LENS_FACING_1 = LENS_FACING_BACK; private static final String CAMERA_NAME_2 = "Virtual camera 2"; private static final int CAMERA_WIDTH_2 = 400; private static final int CAMERA_HEIGHT_2 = 600; - private static final int CAMERA_FORMAT = ImageFormat.YUV_420_888; + private static final int CAMERA_FORMAT_2 = RGBA_8888; + private static final int CAMERA_MAX_FPS_2 = 60; + private static final int CAMERA_SENSOR_ORIENTATION_2 = SENSOR_ORIENTATION_90; + private static final int CAMERA_LENS_FACING_2 = LENS_FACING_FRONT; @Mock private IVirtualCameraService mVirtualCameraServiceMock; + @Mock + private VirtualCameraCallback mVirtualCameraCallbackMock; private VirtualCameraController mVirtualCameraController; private final HandlerExecutor mCallbackHandler = @@ -72,7 +89,8 @@ public class VirtualCameraControllerTest { @Before public void setUp() throws Exception { MockitoAnnotations.initMocks(this); - mVirtualCameraController = new VirtualCameraController(mVirtualCameraServiceMock); + mVirtualCameraController = new VirtualCameraController(mVirtualCameraServiceMock, + DEVICE_POLICY_CUSTOM); when(mVirtualCameraServiceMock.registerCamera(any(), any())).thenReturn(true); } @@ -81,10 +99,12 @@ public class VirtualCameraControllerTest { mVirtualCameraController.close(); } + @Parameters(method = "getAllLensFacingDirections") @Test - public void registerCamera_registersCamera() throws Exception { + public void registerCamera_registersCamera(int lensFacing) throws Exception { mVirtualCameraController.registerCamera(createVirtualCameraConfig( - CAMERA_WIDTH_1, CAMERA_HEIGHT_1, CAMERA_FORMAT, CAMERA_NAME_1)); + CAMERA_WIDTH_1, CAMERA_HEIGHT_1, CAMERA_FORMAT_1, CAMERA_MAX_FPS_1, CAMERA_NAME_1, + CAMERA_SENSOR_ORIENTATION_1, lensFacing)); ArgumentCaptor<VirtualCameraConfiguration> configurationCaptor = ArgumentCaptor.forClass(VirtualCameraConfiguration.class); @@ -92,13 +112,15 @@ public class VirtualCameraControllerTest { VirtualCameraConfiguration virtualCameraConfiguration = configurationCaptor.getValue(); assertThat(virtualCameraConfiguration.supportedStreamConfigs.length).isEqualTo(1); assertVirtualCameraConfiguration(virtualCameraConfiguration, CAMERA_WIDTH_1, - CAMERA_HEIGHT_1, CAMERA_FORMAT); + CAMERA_HEIGHT_1, CAMERA_FORMAT_1, CAMERA_MAX_FPS_1, CAMERA_SENSOR_ORIENTATION_1, + lensFacing); } @Test public void unregisterCamera_unregistersCamera() throws Exception { VirtualCameraConfig config = createVirtualCameraConfig( - CAMERA_WIDTH_1, CAMERA_HEIGHT_1, CAMERA_FORMAT, CAMERA_NAME_1); + CAMERA_WIDTH_1, CAMERA_HEIGHT_1, CAMERA_FORMAT_1, CAMERA_MAX_FPS_1, CAMERA_NAME_1, + CAMERA_SENSOR_ORIENTATION_1, CAMERA_LENS_FACING_1); mVirtualCameraController.registerCamera(config); mVirtualCameraController.unregisterCamera(config); @@ -109,9 +131,11 @@ public class VirtualCameraControllerTest { @Test public void close_unregistersAllCameras() throws Exception { mVirtualCameraController.registerCamera(createVirtualCameraConfig( - CAMERA_WIDTH_1, CAMERA_HEIGHT_1, CAMERA_FORMAT, CAMERA_NAME_1)); + CAMERA_WIDTH_1, CAMERA_HEIGHT_1, CAMERA_FORMAT_1, CAMERA_MAX_FPS_1, CAMERA_NAME_1, + CAMERA_SENSOR_ORIENTATION_1, CAMERA_LENS_FACING_1)); mVirtualCameraController.registerCamera(createVirtualCameraConfig( - CAMERA_WIDTH_2, CAMERA_HEIGHT_2, CAMERA_FORMAT, CAMERA_NAME_2)); + CAMERA_WIDTH_2, CAMERA_HEIGHT_2, CAMERA_FORMAT_2, CAMERA_MAX_FPS_2, CAMERA_NAME_2, + CAMERA_SENSOR_ORIENTATION_2, CAMERA_LENS_FACING_2)); mVirtualCameraController.close(); @@ -123,38 +147,66 @@ public class VirtualCameraControllerTest { configurationCaptor.getAllValues(); assertThat(virtualCameraConfigurations).hasSize(2); assertVirtualCameraConfiguration(virtualCameraConfigurations.get(0), CAMERA_WIDTH_1, - CAMERA_HEIGHT_1, CAMERA_FORMAT); + CAMERA_HEIGHT_1, CAMERA_FORMAT_1, CAMERA_MAX_FPS_1, CAMERA_SENSOR_ORIENTATION_1, + CAMERA_LENS_FACING_1); assertVirtualCameraConfiguration(virtualCameraConfigurations.get(1), CAMERA_WIDTH_2, - CAMERA_HEIGHT_2, CAMERA_FORMAT); + CAMERA_HEIGHT_2, CAMERA_FORMAT_2, CAMERA_MAX_FPS_2, CAMERA_SENSOR_ORIENTATION_2, + CAMERA_LENS_FACING_2); + } + + @Parameters(method = "getAllLensFacingDirections") + @Test + public void registerMultipleSameLensFacingCameras_withCustomCameraPolicy_throwsException( + int lensFacing) { + mVirtualCameraController.registerCamera(createVirtualCameraConfig( + CAMERA_WIDTH_1, CAMERA_HEIGHT_1, CAMERA_FORMAT_1, CAMERA_MAX_FPS_1, CAMERA_NAME_1, + CAMERA_SENSOR_ORIENTATION_1, lensFacing)); + assertThrows(IllegalArgumentException.class, + () -> mVirtualCameraController.registerCamera(createVirtualCameraConfig( + CAMERA_WIDTH_2, CAMERA_HEIGHT_2, CAMERA_FORMAT_2, CAMERA_MAX_FPS_2, + CAMERA_NAME_2, CAMERA_SENSOR_ORIENTATION_2, lensFacing))); + } + + @Parameters(method = "getAllLensFacingDirections") + @Test + public void registerCamera_withDefaultCameraPolicy_throwsException(int lensFacing) { + mVirtualCameraController.close(); + mVirtualCameraController = new VirtualCameraController( + mVirtualCameraServiceMock, DEVICE_POLICY_DEFAULT); + + assertThrows(IllegalArgumentException.class, + () -> mVirtualCameraController.registerCamera(createVirtualCameraConfig( + CAMERA_WIDTH_1, CAMERA_HEIGHT_1, CAMERA_FORMAT_1, CAMERA_MAX_FPS_1, + CAMERA_NAME_1, CAMERA_SENSOR_ORIENTATION_1, lensFacing))); } private VirtualCameraConfig createVirtualCameraConfig( - int width, int height, int format, String displayName) { + int width, int height, int format, int maximumFramesPerSecond, + String name, int sensorOrientation, int lensFacing) { return new VirtualCameraConfig.Builder() - .addStreamConfig(width, height, format) - .setName(displayName) - .setVirtualCameraCallback(mCallbackHandler, createNoOpCallback()) + .addStreamConfig(width, height, format, maximumFramesPerSecond) + .setName(name) + .setVirtualCameraCallback(mCallbackHandler, mVirtualCameraCallbackMock) + .setSensorOrientation(sensorOrientation) + .setLensFacing(lensFacing) .build(); } private static void assertVirtualCameraConfiguration( - VirtualCameraConfiguration configuration, int width, int height, int format) { + VirtualCameraConfiguration configuration, int width, int height, int format, + int maxFps, int sensorOrientation, int lensFacing) { assertThat(configuration.supportedStreamConfigs[0].width).isEqualTo(width); assertThat(configuration.supportedStreamConfigs[0].height).isEqualTo(height); assertThat(configuration.supportedStreamConfigs[0].pixelFormat).isEqualTo(format); + assertThat(configuration.supportedStreamConfigs[0].maxFps).isEqualTo(maxFps); + assertThat(configuration.sensorOrientation).isEqualTo(sensorOrientation); + assertThat(configuration.lensFacing).isEqualTo(lensFacing); } - private static VirtualCameraCallback createNoOpCallback() { - return new VirtualCameraCallback() { - - @Override - public void onStreamConfigured( - int streamId, - @NonNull Surface surface, - @NonNull VirtualCameraStreamConfig streamConfig) {} - - @Override - public void onStreamClosed(int streamId) {} + private static Integer[] getAllLensFacingDirections() { + return new Integer[] { + LENS_FACING_BACK, + LENS_FACING_FRONT }; } } diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/camera/VirtualCameraStreamConfigTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/camera/VirtualCameraStreamConfigTest.java index d9a38eb121ac..206c1115a32e 100644 --- a/services/tests/servicestests/src/com/android/server/companion/virtual/camera/VirtualCameraStreamConfigTest.java +++ b/services/tests/servicestests/src/com/android/server/companion/virtual/camera/VirtualCameraStreamConfigTest.java @@ -35,19 +35,20 @@ public class VirtualCameraStreamConfigTest { private static final int VGA_WIDTH = 640; private static final int VGA_HEIGHT = 480; + private static final int MAX_FPS_1 = 30; private static final int QVGA_WIDTH = 320; private static final int QVGA_HEIGHT = 240; + private static final int MAX_FPS_2 = 60; @Test public void testEquals() { VirtualCameraStreamConfig vgaYuvStreamConfig = new VirtualCameraStreamConfig(VGA_WIDTH, - VGA_HEIGHT, - ImageFormat.YUV_420_888); + VGA_HEIGHT, ImageFormat.YUV_420_888, MAX_FPS_1); VirtualCameraStreamConfig qvgaYuvStreamConfig = new VirtualCameraStreamConfig(QVGA_WIDTH, - QVGA_HEIGHT, ImageFormat.YUV_420_888); + QVGA_HEIGHT, ImageFormat.YUV_420_888, MAX_FPS_2); VirtualCameraStreamConfig vgaRgbaStreamConfig = new VirtualCameraStreamConfig(VGA_WIDTH, - VGA_HEIGHT, PixelFormat.RGBA_8888); + VGA_HEIGHT, PixelFormat.RGBA_8888, MAX_FPS_1); new EqualsTester() .addEqualityGroup(vgaYuvStreamConfig, reparcel(vgaYuvStreamConfig)) @@ -66,6 +67,4 @@ public class VirtualCameraStreamConfigTest { parcel.recycle(); } } - - } diff --git a/services/tests/servicestests/src/com/android/server/inputmethod/InputMethodSettingsTest.java b/services/tests/servicestests/src/com/android/server/inputmethod/InputMethodSettingsTest.java new file mode 100644 index 000000000000..a55d1c409fb6 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/inputmethod/InputMethodSettingsTest.java @@ -0,0 +1,108 @@ +/* + * 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 static org.junit.Assert.assertEquals; + +import android.text.TextUtils; +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, + @NonNull String enabledSubtypeHashCodesStr) { + assertEquals(expectedEnabledImeStr, + InputMethodSettings.updateEnabledImeString(initialEnabledImeStr, + imeId, createSubtypeHashCodeArrayFromStr(enabledSubtypeHashCodesStr))); + } + + private static IntArray createSubtypeHashCodeArrayFromStr(String subtypeHashCodesStr) { + final IntArray subtypes = new IntArray(); + final TextUtils.SimpleStringSplitter imeSubtypeSplitter = + new TextUtils.SimpleStringSplitter(';'); + if (TextUtils.isEmpty(subtypeHashCodesStr)) { + return subtypes; + } + imeSubtypeSplitter.setString(subtypeHashCodesStr); + while (imeSubtypeSplitter.hasNext()) { + subtypes.add(Integer.parseInt(imeSubtypeSplitter.next())); + } + return subtypes; + } + + @Test + public void updateEnabledImeStringTest() { + // No change cases + verifyUpdateEnabledImeString( + "com.android/.ime1", + "com.android/.ime1", "com.android/.ime1", ""); + verifyUpdateEnabledImeString( + "com.android/.ime1", + "com.android/.ime1", "com.android/.ime2", ""); + + // To enable subtypes + verifyUpdateEnabledImeString( + "com.android/.ime1", + "com.android/.ime1", "com.android/.ime2", ""); + verifyUpdateEnabledImeString( + "com.android/.ime1;1", + "com.android/.ime1", "com.android/.ime1", "1"); + + verifyUpdateEnabledImeString( + "com.android/.ime1;1;2;3", + "com.android/.ime1", "com.android/.ime1", "1;2;3"); + + verifyUpdateEnabledImeString( + "com.android/.ime1;1;2;3:com.android/.ime2", + "com.android/.ime1:com.android/.ime2", "com.android/.ime1", "1;2;3"); + verifyUpdateEnabledImeString( + "com.android/.ime0:com.android/.ime1;1;2;3", + "com.android/.ime0:com.android/.ime1", "com.android/.ime1", "1;2;3"); + verifyUpdateEnabledImeString( + "com.android/.ime0:com.android/.ime1;1;2;3:com.android/.ime2", + "com.android/.ime0:com.android/.ime1:com.android/.ime2", "com.android/.ime1", + "1;2;3"); + + // To reset enabled subtypes + verifyUpdateEnabledImeString( + "com.android/.ime1", + "com.android/.ime1;1", "com.android/.ime1", ""); + verifyUpdateEnabledImeString( + "com.android/.ime1", + "com.android/.ime1;1;2;3", "com.android/.ime1", ""); + verifyUpdateEnabledImeString( + "com.android/.ime1:com.android/.ime2", + "com.android/.ime1;1;2;3:com.android/.ime2", "com.android/.ime1", ""); + + verifyUpdateEnabledImeString( + "com.android/.ime0:com.android/.ime1", + "com.android/.ime0:com.android/.ime1;1;2;3", "com.android/.ime1", ""); + verifyUpdateEnabledImeString( + "com.android/.ime0:com.android/.ime1:com.android/.ime2", + "com.android/.ime0:com.android/.ime1;1;2;3:com.android/.ime2", "com.android/.ime1", + ""); + } +} diff --git a/services/tests/servicestests/src/com/android/server/inputmethod/InputMethodUtilsTest.java b/services/tests/servicestests/src/com/android/server/inputmethod/InputMethodUtilsTest.java index 95a9610b15d7..9688ef6cc83b 100644 --- a/services/tests/servicestests/src/com/android/server/inputmethod/InputMethodUtilsTest.java +++ b/services/tests/servicestests/src/com/android/server/inputmethod/InputMethodUtilsTest.java @@ -34,9 +34,7 @@ import android.content.res.Configuration; import android.os.Build; import android.os.LocaleList; import android.os.Parcel; -import android.text.TextUtils; import android.util.ArrayMap; -import android.util.IntArray; import android.view.inputmethod.InputMethodInfo; import android.view.inputmethod.InputMethodSubtype; import android.view.inputmethod.InputMethodSubtype.InputMethodSubtypeBuilder; @@ -1211,28 +1209,6 @@ public class InputMethodUtilsTest { StartInputFlags.VIEW_HAS_FOCUS | StartInputFlags.IS_TEXT_EDITOR)); } - private static IntArray createSubtypeHashCodeArrayFromStr(String subtypeHashCodesStr) { - final IntArray subtypes = new IntArray(); - final TextUtils.SimpleStringSplitter imeSubtypeSplitter = - new TextUtils.SimpleStringSplitter(';'); - if (TextUtils.isEmpty(subtypeHashCodesStr)) { - return subtypes; - } - imeSubtypeSplitter.setString(subtypeHashCodesStr); - while (imeSubtypeSplitter.hasNext()) { - subtypes.add(Integer.parseInt(imeSubtypeSplitter.next())); - } - return subtypes; - } - - private static void verifyUpdateEnabledImeString(@NonNull String expectedEnabledImeStr, - @NonNull String initialEnabledImeStr, @NonNull String imeId, - @NonNull String enabledSubtypeHashCodesStr) { - assertEquals(expectedEnabledImeStr, - InputMethodUtils.InputMethodSettings.updateEnabledImeString(initialEnabledImeStr, - imeId, createSubtypeHashCodeArrayFromStr(enabledSubtypeHashCodesStr))); - } - private static void verifySplitEnabledImeStr(@NonNull String enabledImeStr, @NonNull String... expected) { final ArrayList<String> actual = new ArrayList<>(); @@ -1280,57 +1256,4 @@ public class InputMethodUtilsTest { "com.android/.ime1:com.android/.ime2", "com.android/.ime3")) .isEqualTo("com.android/.ime1:com.android/.ime2:com.android/.ime3"); } - - @Test - public void updateEnabledImeStringTest() { - // No change cases - verifyUpdateEnabledImeString( - "com.android/.ime1", - "com.android/.ime1", "com.android/.ime1", ""); - verifyUpdateEnabledImeString( - "com.android/.ime1", - "com.android/.ime1", "com.android/.ime2", ""); - - // To enable subtypes - verifyUpdateEnabledImeString( - "com.android/.ime1", - "com.android/.ime1", "com.android/.ime2", ""); - verifyUpdateEnabledImeString( - "com.android/.ime1;1", - "com.android/.ime1", "com.android/.ime1", "1"); - - verifyUpdateEnabledImeString( - "com.android/.ime1;1;2;3", - "com.android/.ime1", "com.android/.ime1", "1;2;3"); - - verifyUpdateEnabledImeString( - "com.android/.ime1;1;2;3:com.android/.ime2", - "com.android/.ime1:com.android/.ime2", "com.android/.ime1", "1;2;3"); - verifyUpdateEnabledImeString( - "com.android/.ime0:com.android/.ime1;1;2;3", - "com.android/.ime0:com.android/.ime1", "com.android/.ime1", "1;2;3"); - verifyUpdateEnabledImeString( - "com.android/.ime0:com.android/.ime1;1;2;3:com.android/.ime2", - "com.android/.ime0:com.android/.ime1:com.android/.ime2", "com.android/.ime1", - "1;2;3"); - - // To reset enabled subtypes - verifyUpdateEnabledImeString( - "com.android/.ime1", - "com.android/.ime1;1", "com.android/.ime1", ""); - verifyUpdateEnabledImeString( - "com.android/.ime1", - "com.android/.ime1;1;2;3", "com.android/.ime1", ""); - verifyUpdateEnabledImeString( - "com.android/.ime1:com.android/.ime2", - "com.android/.ime1;1;2;3:com.android/.ime2", "com.android/.ime1", ""); - - verifyUpdateEnabledImeString( - "com.android/.ime0:com.android/.ime1", - "com.android/.ime0:com.android/.ime1;1;2;3", "com.android/.ime1", ""); - verifyUpdateEnabledImeString( - "com.android/.ime0:com.android/.ime1:com.android/.ime2", - "com.android/.ime0:com.android/.ime1;1;2;3:com.android/.ime2", "com.android/.ime1", - ""); - } } diff --git a/services/tests/servicestests/src/com/android/server/job/BackgroundRestrictionsTest.java b/services/tests/servicestests/src/com/android/server/job/BackgroundRestrictionsTest.java deleted file mode 100644 index e871fc567107..000000000000 --- a/services/tests/servicestests/src/com/android/server/job/BackgroundRestrictionsTest.java +++ /dev/null @@ -1,233 +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.server.job; - -import static com.android.servicestests.apps.jobtestapp.TestJobService.ACTION_JOB_STARTED; -import static com.android.servicestests.apps.jobtestapp.TestJobService.ACTION_JOB_STOPPED; -import static com.android.servicestests.apps.jobtestapp.TestJobService.JOB_PARAMS_EXTRA_KEY; - -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; - -import android.app.ActivityManager; -import android.app.AppOpsManager; -import android.app.IActivityManager; -import android.app.job.JobParameters; -import android.content.BroadcastReceiver; -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.os.IDeviceIdleController; -import android.os.RemoteException; -import android.os.ServiceManager; -import android.os.SystemClock; -import android.os.UserHandle; -import android.util.Log; - -import androidx.test.InstrumentationRegistry; -import androidx.test.filters.FlakyTest; -import androidx.test.filters.LargeTest; -import androidx.test.runner.AndroidJUnit4; - -import com.android.servicestests.apps.jobtestapp.TestJobActivity; - -import org.junit.After; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; - -/** - * Tests that background restrictions on jobs work as expected. - * This test requires test-apps/JobTestApp to be installed on the device. - * To run this test from root of checkout: - * <pre> - * mmm -j32 frameworks/base/services/tests/servicestests/ - * adb install -r $OUT/data/app/JobTestApp/JobTestApp.apk - * adb install -r $OUT/data/app/FrameworksServicesTests/FrameworksServicesTests.apk - * adb shell am instrument -e class 'com.android.server.job.BackgroundRestrictionsTest' -w \ - * com.android.frameworks.servicestests - * </pre> - */ -@RunWith(AndroidJUnit4.class) -@LargeTest -public class BackgroundRestrictionsTest { - private static final String TAG = BackgroundRestrictionsTest.class.getSimpleName(); - private static final String TEST_APP_PACKAGE = "com.android.servicestests.apps.jobtestapp"; - private static final String TEST_APP_ACTIVITY = TEST_APP_PACKAGE + ".TestJobActivity"; - private static final long POLL_INTERVAL = 500; - private static final long DEFAULT_WAIT_TIMEOUT = 10_000; - - private Context mContext; - private AppOpsManager mAppOpsManager; - private IDeviceIdleController mDeviceIdleController; - private IActivityManager mIActivityManager; - private volatile int mTestJobId = -1; - private int mTestPackageUid; - /* accesses must be synchronized on itself */ - private final TestJobStatus mTestJobStatus = new TestJobStatus(); - private final BroadcastReceiver mJobStateChangeReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - final JobParameters params = intent.getParcelableExtra(JOB_PARAMS_EXTRA_KEY); - Log.d(TAG, "Received action " + intent.getAction()); - synchronized (mTestJobStatus) { - switch (intent.getAction()) { - case ACTION_JOB_STARTED: - mTestJobStatus.running = true; - mTestJobStatus.jobId = params.getJobId(); - mTestJobStatus.stopReason = JobParameters.STOP_REASON_UNDEFINED; - break; - case ACTION_JOB_STOPPED: - mTestJobStatus.running = false; - mTestJobStatus.jobId = params.getJobId(); - mTestJobStatus.stopReason = params.getStopReason(); - break; - } - } - } - }; - - @Before - public void setUp() throws Exception { - mContext = InstrumentationRegistry.getTargetContext(); - mAppOpsManager = (AppOpsManager) mContext.getSystemService(Context.APP_OPS_SERVICE); - mDeviceIdleController = IDeviceIdleController.Stub.asInterface( - ServiceManager.getService(Context.DEVICE_IDLE_CONTROLLER)); - mIActivityManager = ActivityManager.getService(); - mTestPackageUid = mContext.getPackageManager().getPackageUid(TEST_APP_PACKAGE, 0); - mTestJobStatus.reset(); - final IntentFilter intentFilter = new IntentFilter(); - intentFilter.addAction(ACTION_JOB_STARTED); - intentFilter.addAction(ACTION_JOB_STOPPED); - mContext.registerReceiver(mJobStateChangeReceiver, intentFilter, - Context.RECEIVER_EXPORTED_UNAUDITED); - setAppOpsModeAllowed(true); - setPowerExemption(false); - } - - private void scheduleTestJob() { - mTestJobId = (int) (SystemClock.uptimeMillis() / 1000); - final Intent scheduleJobIntent = new Intent(TestJobActivity.ACTION_START_JOB); - scheduleJobIntent.putExtra(TestJobActivity.EXTRA_JOB_ID_KEY, mTestJobId); - scheduleJobIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - scheduleJobIntent.setComponent(new ComponentName(TEST_APP_PACKAGE, TEST_APP_ACTIVITY)); - mContext.startActivity(scheduleJobIntent); - } - - private void scheduleAndAssertJobStarted() throws Exception { - scheduleTestJob(); - Thread.sleep(TestJobActivity.JOB_MINIMUM_LATENCY); - assertTrue("Job did not start after scheduling", awaitJobStart(DEFAULT_WAIT_TIMEOUT)); - } - - @FlakyTest - @Test - public void testPowerExemption() throws Exception { - scheduleAndAssertJobStarted(); - setAppOpsModeAllowed(false); - mIActivityManager.makePackageIdle(TEST_APP_PACKAGE, UserHandle.USER_CURRENT); - assertTrue("Job did not stop after putting app under bg-restriction", - awaitJobStop(DEFAULT_WAIT_TIMEOUT, - JobParameters.STOP_REASON_BACKGROUND_RESTRICTION)); - - setPowerExemption(true); - scheduleTestJob(); - Thread.sleep(TestJobActivity.JOB_MINIMUM_LATENCY); - assertTrue("Job did not start when the app was in the power exemption list", - awaitJobStart(DEFAULT_WAIT_TIMEOUT)); - - setPowerExemption(false); - assertTrue("Job did not stop after removing from the power exemption list", - awaitJobStop(DEFAULT_WAIT_TIMEOUT, - JobParameters.STOP_REASON_BACKGROUND_RESTRICTION)); - - scheduleTestJob(); - Thread.sleep(TestJobActivity.JOB_MINIMUM_LATENCY); - assertFalse("Job started under bg-restrictions", awaitJobStart(DEFAULT_WAIT_TIMEOUT)); - setPowerExemption(true); - assertTrue("Job did not start when the app was in the power exemption list", - awaitJobStart(DEFAULT_WAIT_TIMEOUT)); - } - - @After - public void tearDown() throws Exception { - final Intent cancelJobsIntent = new Intent(TestJobActivity.ACTION_CANCEL_JOBS); - cancelJobsIntent.setComponent(new ComponentName(TEST_APP_PACKAGE, TEST_APP_ACTIVITY)); - cancelJobsIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - mContext.startActivity(cancelJobsIntent); - mContext.unregisterReceiver(mJobStateChangeReceiver); - Thread.sleep(500); // To avoid race with register in the next setUp - setAppOpsModeAllowed(true); - setPowerExemption(false); - } - - private void setPowerExemption(boolean exempt) throws RemoteException { - if (exempt) { - mDeviceIdleController.addPowerSaveWhitelistApp(TEST_APP_PACKAGE); - } else { - mDeviceIdleController.removePowerSaveWhitelistApp(TEST_APP_PACKAGE); - } - } - - private void setAppOpsModeAllowed(boolean allow) { - mAppOpsManager.setMode(AppOpsManager.OP_RUN_ANY_IN_BACKGROUND, mTestPackageUid, - TEST_APP_PACKAGE, allow ? AppOpsManager.MODE_ALLOWED : AppOpsManager.MODE_IGNORED); - } - - private boolean awaitJobStart(long timeout) throws InterruptedException { - return waitUntilTrue(timeout, () -> { - synchronized (mTestJobStatus) { - return (mTestJobStatus.jobId == mTestJobId) && mTestJobStatus.running; - } - }); - } - - private boolean awaitJobStop(long timeout, @JobParameters.StopReason int expectedStopReason) - throws InterruptedException { - return waitUntilTrue(timeout, () -> { - synchronized (mTestJobStatus) { - return (mTestJobStatus.jobId == mTestJobId) && !mTestJobStatus.running - && (expectedStopReason == JobParameters.STOP_REASON_UNDEFINED - || mTestJobStatus.stopReason == expectedStopReason); - } - }); - } - - private boolean waitUntilTrue(long timeout, Condition condition) throws InterruptedException { - final long deadLine = SystemClock.uptimeMillis() + timeout; - do { - Thread.sleep(POLL_INTERVAL); - } while (!condition.isTrue() && SystemClock.uptimeMillis() < deadLine); - return condition.isTrue(); - } - - private static final class TestJobStatus { - int jobId; - int stopReason; - boolean running; - - private void reset() { - running = false; - stopReason = jobId = 0; - } - } - - private interface Condition { - boolean isTrue(); - } -} diff --git a/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest1.java b/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest1.java index 0f5fb91a140f..d50affba1ea1 100644 --- a/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest1.java +++ b/services/tests/servicestests/src/com/android/server/pm/ShortcutManagerTest1.java @@ -406,8 +406,8 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { public void testPushDynamicShortcut() { // Change the max number of shortcuts. - mService.updateConfigurationLocked(ConfigConstants.KEY_MAX_SHORTCUTS + "=5"); - + mService.updateConfigurationLocked(ConfigConstants.KEY_MAX_SHORTCUTS + "=5," + + ShortcutService.ConfigConstants.KEY_SAVE_DELAY_MILLIS + "=1"); setCaller(CALLING_PACKAGE_1, USER_0); final ShortcutInfo s1 = makeShortcut("s1"); @@ -545,6 +545,57 @@ public class ShortcutManagerTest1 extends BaseShortcutManagerTest { eq(CALLING_PACKAGE_1), eq("s9"), eq(USER_0)); } + public void testPushDynamicShortcut_CallsToUsageStatsManagerAreThrottled() + throws InterruptedException { + mService.updateConfigurationLocked( + ShortcutService.ConfigConstants.KEY_SAVE_DELAY_MILLIS + "=500"); + + // Verify calls to UsageStatsManagerInternal#reportShortcutUsage are throttled. + setCaller(CALLING_PACKAGE_1, USER_0); + { + final ShortcutInfo si = makeShortcut("s0"); + mManager.pushDynamicShortcut(si); + } + verify(mMockUsageStatsManagerInternal, times(1)).reportShortcutUsage( + eq(CALLING_PACKAGE_1), eq("s0"), eq(USER_0)); + Mockito.reset(mMockUsageStatsManagerInternal); + for (int i = 2; i <= 10; i++) { + final ShortcutInfo si = makeShortcut("s" + i); + mManager.pushDynamicShortcut(si); + } + verify(mMockUsageStatsManagerInternal, times(0)).reportShortcutUsage( + any(), any(), anyInt()); + + // Verify pkg2 isn't blocked by pkg1, but consecutive calls from pkg2 are throttled as well. + setCaller(CALLING_PACKAGE_2, USER_0); + { + final ShortcutInfo si = makeShortcut("s1"); + mManager.pushDynamicShortcut(si); + } + verify(mMockUsageStatsManagerInternal, times(1)).reportShortcutUsage( + eq(CALLING_PACKAGE_2), eq("s1"), eq(USER_0)); + Mockito.reset(mMockUsageStatsManagerInternal); + for (int i = 2; i <= 10; i++) { + final ShortcutInfo si = makeShortcut("s" + i); + mManager.pushDynamicShortcut(si); + } + verify(mMockUsageStatsManagerInternal, times(0)).reportShortcutUsage( + any(), any(), anyInt()); + + Mockito.reset(mMockUsageStatsManagerInternal); + // Let time passes which resets the throttle + Thread.sleep(505); + // Verify UsageStatsManagerInternal#reportShortcutUsed can be called again + setCaller(CALLING_PACKAGE_1, USER_0); + mManager.pushDynamicShortcut(makeShortcut("s10")); + setCaller(CALLING_PACKAGE_2, USER_0); + mManager.pushDynamicShortcut(makeShortcut("s10")); + verify(mMockUsageStatsManagerInternal, times(1)).reportShortcutUsage( + eq(CALLING_PACKAGE_1), any(), eq(USER_0)); + verify(mMockUsageStatsManagerInternal, times(1)).reportShortcutUsage( + eq(CALLING_PACKAGE_2), any(), eq(USER_0)); + } + public void testUnlimitedCalls() { setCaller(CALLING_PACKAGE_1, USER_0); diff --git a/services/tests/servicestests/src/com/android/server/uri/UriGrantsManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/uri/UriGrantsManagerServiceTest.java index 769ec5fac023..321858685e38 100644 --- a/services/tests/servicestests/src/com/android/server/uri/UriGrantsManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/uri/UriGrantsManagerServiceTest.java @@ -345,15 +345,18 @@ public class UriGrantsManagerServiceTest { intent, UID_PRIMARY_CAMERA, PKG_SOCIAL, USER_PRIMARY), service); // Verify that everything is good with the world - assertTrue(mService.checkUriPermission(expectedGrant, UID_PRIMARY_SOCIAL, FLAG_READ)); + assertTrue(mService.checkUriPermission(expectedGrant, UID_PRIMARY_SOCIAL, FLAG_READ, + /* isFullAccessForContentUri */ false)); // Finish activity; service should hold permission activity.removeUriPermissions(); - assertTrue(mService.checkUriPermission(expectedGrant, UID_PRIMARY_SOCIAL, FLAG_READ)); + assertTrue(mService.checkUriPermission(expectedGrant, UID_PRIMARY_SOCIAL, FLAG_READ, + /* isFullAccessForContentUri */ false)); // And finishing service should wrap things up service.removeUriPermissions(); - assertFalse(mService.checkUriPermission(expectedGrant, UID_PRIMARY_SOCIAL, FLAG_READ)); + assertFalse(mService.checkUriPermission(expectedGrant, UID_PRIMARY_SOCIAL, FLAG_READ, + /* isFullAccessForContentUri */ false)); } @Test diff --git a/services/tests/servicestests/src/com/android/server/webkit/TestSystemImpl.java b/services/tests/servicestests/src/com/android/server/webkit/TestSystemImpl.java index 3530e38ef67c..ae0a758449b5 100644 --- a/services/tests/servicestests/src/com/android/server/webkit/TestSystemImpl.java +++ b/services/tests/servicestests/src/com/android/server/webkit/TestSystemImpl.java @@ -85,7 +85,7 @@ public class TestSystemImpl implements SystemInterface { private void enablePackageForUser(String packageName, boolean enable, int userId) { Map<Integer, PackageInfo> userPackages = mPackages.get(packageName); if (userPackages == null) { - throw new IllegalArgumentException("There is no package called " + packageName); + return; } PackageInfo packageInfo = userPackages.get(userId); packageInfo.applicationInfo.enabled = enable; diff --git a/services/tests/servicestests/src/com/android/server/webkit/WebViewUpdateServiceTest.java b/services/tests/servicestests/src/com/android/server/webkit/WebViewUpdateServiceTest.java index 32082e3d857e..5a06327fdde3 100644 --- a/services/tests/servicestests/src/com/android/server/webkit/WebViewUpdateServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/webkit/WebViewUpdateServiceTest.java @@ -127,12 +127,21 @@ public class WebViewUpdateServiceTest { private void checkCertainPackageUsedAfterWebViewBootPreparation(String expectedProviderName, WebViewProviderInfo[] webviewPackages) { checkCertainPackageUsedAfterWebViewBootPreparation( - expectedProviderName, webviewPackages, 1); + expectedProviderName, webviewPackages, 1, null); } private void checkCertainPackageUsedAfterWebViewBootPreparation(String expectedProviderName, - WebViewProviderInfo[] webviewPackages, int numRelros) { + WebViewProviderInfo[] webviewPackages, String userSetting) { + checkCertainPackageUsedAfterWebViewBootPreparation( + expectedProviderName, webviewPackages, 1, userSetting); + } + + private void checkCertainPackageUsedAfterWebViewBootPreparation(String expectedProviderName, + WebViewProviderInfo[] webviewPackages, int numRelros, String userSetting) { setupWithPackagesAndRelroCount(webviewPackages, numRelros); + if (userSetting != null) { + mTestSystemImpl.updateUserSetting(null, userSetting); + } // Add (enabled and valid) package infos for each provider setEnabledAndValidPackageInfos(webviewPackages); @@ -280,7 +289,7 @@ public class WebViewUpdateServiceTest { singlePackage, new WebViewProviderInfo[] { new WebViewProviderInfo(singlePackage, "", true /*def av*/, false, null)}, - 2); + 2, null); } // Ensure that package with valid signatures is chosen rather than package with invalid @@ -295,14 +304,16 @@ public class WebViewUpdateServiceTest { Signature invalidPackageSignature = new Signature("33"); WebViewProviderInfo[] packages = new WebViewProviderInfo[] { - new WebViewProviderInfo(invalidPackage, "", true, false, new String[]{ - Base64.encodeToString( - invalidExpectedSignature.toByteArray(), Base64.DEFAULT)}), new WebViewProviderInfo(validPackage, "", true, false, new String[]{ Base64.encodeToString( - validSignature.toByteArray(), Base64.DEFAULT)}) + validSignature.toByteArray(), Base64.DEFAULT)}), + new WebViewProviderInfo(invalidPackage, "", true, false, new String[]{ + Base64.encodeToString( + invalidExpectedSignature.toByteArray(), Base64.DEFAULT)}) }; setupWithPackagesNonDebuggable(packages); + // Start with the setting pointing to the invalid package + mTestSystemImpl.updateUserSetting(null, invalidPackage); mTestSystemImpl.setPackageInfo(createPackageInfo(invalidPackage, true /* enabled */, true /* valid */, true /* installed */, new Signature[]{invalidPackageSignature} , 0 /* updateTime */)); @@ -339,7 +350,9 @@ public class WebViewUpdateServiceTest { } @Test - public void testFailListingEmptyWebviewPackages() { + @RequiresFlagsDisabled("android.webkit.update_service_v2") + // If the flag is set, will throw an exception because of no available by default provider. + public void testEmptyConfig() { WebViewProviderInfo[] packages = new WebViewProviderInfo[0]; setupWithPackages(packages); setEnabledAndValidPackageInfos(packages); @@ -352,14 +365,26 @@ public class WebViewUpdateServiceTest { WebViewProviderResponse response = mWebViewUpdateServiceImpl.waitForAndGetProvider(); assertEquals(WebViewFactory.LIBLOAD_FAILED_LISTING_WEBVIEW_PACKAGES, response.status); assertEquals(null, mWebViewUpdateServiceImpl.getCurrentWebViewPackage()); + } - // Now install a package + @Test + public void testFailListingEmptyWebviewPackages() { String singlePackage = "singlePackage"; - packages = new WebViewProviderInfo[]{ + WebViewProviderInfo[] packages = new WebViewProviderInfo[]{ new WebViewProviderInfo(singlePackage, "", true, false, null)}; setupWithPackages(packages); - setEnabledAndValidPackageInfos(packages); + runWebViewBootPreparationOnMainSync(); + + Mockito.verify(mTestSystemImpl, Mockito.never()).onWebViewProviderChanged( + Matchers.anyObject()); + + WebViewProviderResponse response = mWebViewUpdateServiceImpl.waitForAndGetProvider(); + assertEquals(WebViewFactory.LIBLOAD_FAILED_LISTING_WEBVIEW_PACKAGES, response.status); + assertEquals(null, mWebViewUpdateServiceImpl.getCurrentWebViewPackage()); + + // Now install the package + setEnabledAndValidPackageInfos(packages); mWebViewUpdateServiceImpl.packageStateChanged(singlePackage, WebViewUpdateService.PACKAGE_ADDED, TestSystemImpl.PRIMARY_USER_ID); @@ -370,7 +395,7 @@ public class WebViewUpdateServiceTest { // Remove the package again mTestSystemImpl.removePackageInfo(singlePackage); mWebViewUpdateServiceImpl.packageStateChanged(singlePackage, - WebViewUpdateService.PACKAGE_ADDED, TestSystemImpl.PRIMARY_USER_ID); + WebViewUpdateService.PACKAGE_REMOVED, TestSystemImpl.PRIMARY_USER_ID); // Package removed - ensure our interface states that there is no package response = mWebViewUpdateServiceImpl.waitForAndGetProvider(); @@ -455,6 +480,8 @@ public class WebViewUpdateServiceTest { new WebViewProviderInfo(firstPackage, "", true, false, null), new WebViewProviderInfo(secondPackage, "", true, false, null)}; setupWithPackages(packages); + // Start with the setting pointing to the second package + mTestSystemImpl.updateUserSetting(null, secondPackage); // Have all packages be enabled, so that we can change provider however we want to setEnabledAndValidPackageInfos(packages); @@ -463,9 +490,9 @@ public class WebViewUpdateServiceTest { runWebViewBootPreparationOnMainSync(); Mockito.verify(mTestSystemImpl).onWebViewProviderChanged( - Mockito.argThat(new IsPackageInfoWithName(firstPackage))); + Mockito.argThat(new IsPackageInfoWithName(secondPackage))); - assertEquals(firstPackage, + assertEquals(secondPackage, mWebViewUpdateServiceImpl.getCurrentWebViewPackage().packageName); new Thread(new Runnable() { @@ -474,12 +501,13 @@ public class WebViewUpdateServiceTest { WebViewProviderResponse threadResponse = mWebViewUpdateServiceImpl.waitForAndGetProvider(); assertEquals(WebViewFactory.LIBLOAD_SUCCESS, threadResponse.status); - assertEquals(secondPackage, threadResponse.packageInfo.packageName); - // Verify that we killed the first package if we performed a settings change - - // otherwise we had to disable the first package, in which case its dependents + assertEquals(firstPackage, threadResponse.packageInfo.packageName); + // Verify that we killed the second package if we performed a settings change - + // otherwise we had to disable the second package, in which case its dependents // should have been killed by the framework. if (settingsChange) { - Mockito.verify(mTestSystemImpl).killPackageDependents(Mockito.eq(firstPackage)); + Mockito.verify(mTestSystemImpl) + .killPackageDependents(Mockito.eq(secondPackage)); } countdown.countDown(); } @@ -490,32 +518,36 @@ public class WebViewUpdateServiceTest { } if (settingsChange) { - mWebViewUpdateServiceImpl.changeProviderAndSetting(secondPackage); + mWebViewUpdateServiceImpl.changeProviderAndSetting(firstPackage); } else { - // Enable the second provider - mTestSystemImpl.setPackageInfo(createPackageInfo(secondPackage, true /* enabled */, + // Enable the first provider + mTestSystemImpl.setPackageInfo(createPackageInfo(firstPackage, true /* enabled */, true /* valid */, true /* installed */)); mWebViewUpdateServiceImpl.packageStateChanged( - secondPackage, WebViewUpdateService.PACKAGE_CHANGED, TestSystemImpl.PRIMARY_USER_ID); + firstPackage, + WebViewUpdateService.PACKAGE_CHANGED, + TestSystemImpl.PRIMARY_USER_ID); // Ensure we haven't changed package yet. - assertEquals(firstPackage, + assertEquals(secondPackage, mWebViewUpdateServiceImpl.getCurrentWebViewPackage().packageName); - // Switch provider by disabling the first one - mTestSystemImpl.setPackageInfo(createPackageInfo(firstPackage, false /* enabled */, + // Switch provider by disabling the second one + mTestSystemImpl.setPackageInfo(createPackageInfo(secondPackage, false /* enabled */, true /* valid */, true /* installed */)); mWebViewUpdateServiceImpl.packageStateChanged( - firstPackage, WebViewUpdateService.PACKAGE_CHANGED, TestSystemImpl.PRIMARY_USER_ID); + secondPackage, + WebViewUpdateService.PACKAGE_CHANGED, + TestSystemImpl.PRIMARY_USER_ID); } mWebViewUpdateServiceImpl.notifyRelroCreationCompleted(); - // first package done, should start on second + // second package done, should start on first Mockito.verify(mTestSystemImpl).onWebViewProviderChanged( - Mockito.argThat(new IsPackageInfoWithName(secondPackage))); + Mockito.argThat(new IsPackageInfoWithName(firstPackage))); mWebViewUpdateServiceImpl.notifyRelroCreationCompleted(); - // second package done, the other thread should now be unblocked + // first package done, the other thread should now be unblocked try { countdown.await(); } catch (InterruptedException e) { @@ -526,6 +558,7 @@ public class WebViewUpdateServiceTest { * Scenario for testing re-enabling a fallback package. */ @Test + @RequiresFlagsDisabled("android.webkit.update_service_v2") public void testFallbackPackageEnabling() { String testPackage = "testFallback"; WebViewProviderInfo[] packages = new WebViewProviderInfo[] { @@ -555,6 +588,9 @@ public class WebViewUpdateServiceTest { * 3. Primary should be used */ @Test + @RequiresFlagsDisabled("android.webkit.update_service_v2") + // If the flag is set, we don't automitally switch to secondary package unless it is + // chosen directly. public void testInstallingPrimaryPackage() { String primaryPackage = "primary"; String secondaryPackage = "secondary"; @@ -586,16 +622,16 @@ public class WebViewUpdateServiceTest { } @Test - public void testRemovingPrimarySelectsSecondarySingleUser() { + public void testRemovingSecondarySelectsPrimarySingleUser() { for (PackageRemovalType removalType : REMOVAL_TYPES) { - checkRemovingPrimarySelectsSecondary(false /* multiUser */, removalType); + checkRemovingSecondarySelectsPrimary(false /* multiUser */, removalType); } } @Test - public void testRemovingPrimarySelectsSecondaryMultiUser() { + public void testRemovingSecondarySelectsPrimaryMultiUser() { for (PackageRemovalType removalType : REMOVAL_TYPES) { - checkRemovingPrimarySelectsSecondary(true /* multiUser */, removalType); + checkRemovingSecondarySelectsPrimary(true /* multiUser */, removalType); } } @@ -609,7 +645,7 @@ public class WebViewUpdateServiceTest { private PackageRemovalType[] REMOVAL_TYPES = PackageRemovalType.class.getEnumConstants(); - public void checkRemovingPrimarySelectsSecondary(boolean multiUser, + private void checkRemovingSecondarySelectsPrimary(boolean multiUser, PackageRemovalType removalType) { String primaryPackage = "primary"; String secondaryPackage = "secondary"; @@ -620,6 +656,8 @@ public class WebViewUpdateServiceTest { secondaryPackage, "", true /* default available */, false /* fallback */, null)}; setupWithPackages(packages); + // Start with the setting pointing to the secondary package + mTestSystemImpl.updateUserSetting(null, secondaryPackage); int secondaryUserId = 10; int userIdToChangePackageFor = multiUser ? secondaryUserId : TestSystemImpl.PRIMARY_USER_ID; if (multiUser) { @@ -629,31 +667,31 @@ public class WebViewUpdateServiceTest { setEnabledAndValidPackageInfosForUser(TestSystemImpl.PRIMARY_USER_ID, packages); runWebViewBootPreparationOnMainSync(); - checkPreparationPhasesForPackage(primaryPackage, 1); + checkPreparationPhasesForPackage(secondaryPackage, 1); boolean enabled = !(removalType == PackageRemovalType.DISABLE); boolean installed = !(removalType == PackageRemovalType.UNINSTALL); boolean hidden = (removalType == PackageRemovalType.HIDE); - // Disable primary package and ensure secondary becomes used + // Disable secondary package and ensure primary becomes used mTestSystemImpl.setPackageInfoForUser(userIdToChangePackageFor, - createPackageInfo(primaryPackage, enabled /* enabled */, true /* valid */, + createPackageInfo(secondaryPackage, enabled /* enabled */, true /* valid */, installed /* installed */, null /* signature */, 0 /* updateTime */, hidden /* hidden */)); - mWebViewUpdateServiceImpl.packageStateChanged(primaryPackage, + mWebViewUpdateServiceImpl.packageStateChanged(secondaryPackage, removalType == PackageRemovalType.DISABLE ? WebViewUpdateService.PACKAGE_CHANGED : WebViewUpdateService.PACKAGE_REMOVED, userIdToChangePackageFor); // USER ID - checkPreparationPhasesForPackage(secondaryPackage, 1); + checkPreparationPhasesForPackage(primaryPackage, 1); - // Again enable primary package and verify primary is used + // Again enable secondary package and verify secondary is used mTestSystemImpl.setPackageInfoForUser(userIdToChangePackageFor, - createPackageInfo(primaryPackage, true /* enabled */, true /* valid */, + createPackageInfo(secondaryPackage, true /* enabled */, true /* valid */, true /* installed */)); - mWebViewUpdateServiceImpl.packageStateChanged(primaryPackage, + mWebViewUpdateServiceImpl.packageStateChanged(secondaryPackage, removalType == PackageRemovalType.DISABLE ? WebViewUpdateService.PACKAGE_CHANGED : WebViewUpdateService.PACKAGE_ADDED, userIdToChangePackageFor); - checkPreparationPhasesForPackage(primaryPackage, 2); + checkPreparationPhasesForPackage(secondaryPackage, 2); } /** @@ -671,18 +709,20 @@ public class WebViewUpdateServiceTest { secondaryPackage, "", true /* default available */, false /* fallback */, null)}; setupWithPackages(packages); + // Start with the setting pointing to the secondary package + mTestSystemImpl.updateUserSetting(null, secondaryPackage); setEnabledAndValidPackageInfosForUser(TestSystemImpl.PRIMARY_USER_ID, packages); int newUser = 100; mTestSystemImpl.addUser(newUser); - // Let the primary package be uninstalled for the new user + // Let the secondary package be uninstalled for the new user mTestSystemImpl.setPackageInfoForUser(newUser, - createPackageInfo(primaryPackage, true /* enabled */, true /* valid */, + createPackageInfo(secondaryPackage, true /* enabled */, true /* valid */, false /* installed */)); mTestSystemImpl.setPackageInfoForUser(newUser, - createPackageInfo(secondaryPackage, true /* enabled */, true /* valid */, + createPackageInfo(primaryPackage, true /* enabled */, true /* valid */, true /* installed */)); mWebViewUpdateServiceImpl.handleNewUser(newUser); - checkPreparationPhasesForPackage(secondaryPackage, 1 /* numRelros */); + checkPreparationPhasesForPackage(primaryPackage, 1 /* numRelros */); } /** @@ -780,9 +820,9 @@ public class WebViewUpdateServiceTest { String chosenPackage = "chosenPackage"; String nonChosenPackage = "non-chosenPackage"; WebViewProviderInfo[] packages = new WebViewProviderInfo[] { - new WebViewProviderInfo(chosenPackage, "", true /* default available */, - false /* fallback */, null), new WebViewProviderInfo(nonChosenPackage, "", true /* default available */, + false /* fallback */, null), + new WebViewProviderInfo(chosenPackage, "", true /* default available */, false /* fallback */, null)}; setupWithPackages(packages); @@ -810,6 +850,9 @@ public class WebViewUpdateServiceTest { } @Test + @RequiresFlagsDisabled("android.webkit.update_service_v2") + // If the flag is set, we don't automitally switch to second package unless it is chosen + // directly. public void testRecoverFailedListingWebViewPackagesAddedPackage() { checkRecoverAfterFailListingWebviewPackages(false); } @@ -874,22 +917,22 @@ public class WebViewUpdateServiceTest { false /* fallback */, null), new WebViewProviderInfo(secondPackage, "", true /* default available */, false /* fallback */, null)}; - checkCertainPackageUsedAfterWebViewBootPreparation(firstPackage, packages); + checkCertainPackageUsedAfterWebViewBootPreparation(secondPackage, packages, secondPackage); // Replace or remove the current webview package if (replaced) { mTestSystemImpl.setPackageInfo( - createPackageInfo(firstPackage, true /* enabled */, false /* valid */, + createPackageInfo(secondPackage, true /* enabled */, false /* valid */, true /* installed */)); - mWebViewUpdateServiceImpl.packageStateChanged(firstPackage, + mWebViewUpdateServiceImpl.packageStateChanged(secondPackage, WebViewUpdateService.PACKAGE_ADDED_REPLACED, TestSystemImpl.PRIMARY_USER_ID); } else { - mTestSystemImpl.removePackageInfo(firstPackage); - mWebViewUpdateServiceImpl.packageStateChanged(firstPackage, + mTestSystemImpl.removePackageInfo(secondPackage); + mWebViewUpdateServiceImpl.packageStateChanged(secondPackage, WebViewUpdateService.PACKAGE_REMOVED, TestSystemImpl.PRIMARY_USER_ID); } - checkPreparationPhasesForPackage(secondPackage, 1); + checkPreparationPhasesForPackage(firstPackage, 1); Mockito.verify(mTestSystemImpl, Mockito.never()).killPackageDependents( Mockito.anyObject()); @@ -1073,10 +1116,12 @@ public class WebViewUpdateServiceTest { } /** - * Ensure that the update service does use an uninstalled package when that is the only + * Ensure that the update service does not use an uninstalled package even if it is the only * package available. */ @Test + @RequiresFlagsDisabled("android.webkit.update_service_v2") + // If the flag is set, we return the package even if it is not installed. public void testWithSingleUninstalledPackage() { String testPackageName = "test.package.name"; WebViewProviderInfo[] webviewPackages = new WebViewProviderInfo[] { @@ -1115,12 +1160,14 @@ public class WebViewUpdateServiceTest { String installedPackage = "installedPackage"; String uninstalledPackage = "uninstalledPackage"; WebViewProviderInfo[] webviewPackages = new WebViewProviderInfo[] { - new WebViewProviderInfo(uninstalledPackage, "", true /* available by default */, - false /* fallback */, null), new WebViewProviderInfo(installedPackage, "", true /* available by default */, + false /* fallback */, null), + new WebViewProviderInfo(uninstalledPackage, "", true /* available by default */, false /* fallback */, null)}; setupWithPackages(webviewPackages); + // Start with the setting pointing to the uninstalled package + mTestSystemImpl.updateUserSetting(null, uninstalledPackage); int secondaryUserId = 5; if (multiUser) { mTestSystemImpl.addUser(secondaryUserId); @@ -1128,7 +1175,7 @@ public class WebViewUpdateServiceTest { setEnabledAndValidPackageInfosForUser(TestSystemImpl.PRIMARY_USER_ID, webviewPackages); mTestSystemImpl.setPackageInfoForUser(secondaryUserId, createPackageInfo( installedPackage, true /* enabled */, true /* valid */, true /* installed */)); - // Hide or uninstall the primary package for the second user + // Hide or uninstall the secondary package for the second user mTestSystemImpl.setPackageInfo(createPackageInfo(uninstalledPackage, true /* enabled */, true /* valid */, (testUninstalled ? false : true) /* installed */, null /* signatures */, 0 /* updateTime */, (testHidden ? true : false))); @@ -1166,12 +1213,14 @@ public class WebViewUpdateServiceTest { String installedPackage = "installedPackage"; String uninstalledPackage = "uninstalledPackage"; WebViewProviderInfo[] webviewPackages = new WebViewProviderInfo[] { - new WebViewProviderInfo(uninstalledPackage, "", true /* available by default */, - false /* fallback */, null), new WebViewProviderInfo(installedPackage, "", true /* available by default */, + false /* fallback */, null), + new WebViewProviderInfo(uninstalledPackage, "", true /* available by default */, false /* fallback */, null)}; setupWithPackages(webviewPackages); + // Start with the setting pointing to the uninstalled package + mTestSystemImpl.updateUserSetting(null, uninstalledPackage); int secondaryUserId = 412; mTestSystemImpl.addUser(secondaryUserId); @@ -1221,12 +1270,14 @@ public class WebViewUpdateServiceTest { String installedPackage = "installedPackage"; String uninstalledPackage = "uninstalledPackage"; WebViewProviderInfo[] webviewPackages = new WebViewProviderInfo[] { - new WebViewProviderInfo(uninstalledPackage, "", true /* available by default */, - false /* fallback */, null), new WebViewProviderInfo(installedPackage, "", true /* available by default */, + false /* fallback */, null), + new WebViewProviderInfo(uninstalledPackage, "", true /* available by default */, false /* fallback */, null)}; setupWithPackages(webviewPackages); + // Start with the setting pointing to the uninstalled package + mTestSystemImpl.updateUserSetting(null, uninstalledPackage); int secondaryUserId = 4; mTestSystemImpl.addUser(secondaryUserId); @@ -1433,11 +1484,16 @@ public class WebViewUpdateServiceTest { new WebViewProviderInfo(newSdkPackage.packageName, "", true, false, null); WebViewProviderInfo currentSdkProviderInfo = new WebViewProviderInfo(currentSdkPackage.packageName, "", true, false, null); - WebViewProviderInfo[] packages = new WebViewProviderInfo[] { - new WebViewProviderInfo(oldSdkPackage.packageName, "", true, false, null), - currentSdkProviderInfo, newSdkProviderInfo}; + WebViewProviderInfo[] packages = + new WebViewProviderInfo[] { + currentSdkProviderInfo, + new WebViewProviderInfo(oldSdkPackage.packageName, "", true, false, null), + newSdkProviderInfo + }; setupWithPackages(packages); -; + // Start with the setting pointing to the invalid package + mTestSystemImpl.updateUserSetting(null, oldSdkPackage.packageName); + mTestSystemImpl.setPackageInfo(newSdkPackage); mTestSystemImpl.setPackageInfo(currentSdkPackage); mTestSystemImpl.setPackageInfo(oldSdkPackage); @@ -1467,4 +1523,74 @@ public class WebViewUpdateServiceTest { assertEquals( defaultPackage1, mWebViewUpdateServiceImpl.getDefaultWebViewPackage().packageName); } + + @Test + @RequiresFlagsEnabled("android.webkit.update_service_v2") + public void testDefaultWebViewPackageEnabling() { + String testPackage = "testDefault"; + WebViewProviderInfo[] packages = + new WebViewProviderInfo[] { + new WebViewProviderInfo( + testPackage, + "", + true /* default available */, + false /* fallback */, + null) + }; + setupWithPackages(packages); + mTestSystemImpl.setPackageInfo( + createPackageInfo( + testPackage, false /* enabled */, true /* valid */, true /* installed */)); + + // Check that the boot time logic re-enables the default package. + runWebViewBootPreparationOnMainSync(); + Mockito.verify(mTestSystemImpl) + .enablePackageForAllUsers( + Matchers.anyObject(), Mockito.eq(testPackage), Mockito.eq(true)); + } + + private void testDefaultPackageChosen(PackageInfo packageInfo) { + WebViewProviderInfo[] packages = + new WebViewProviderInfo[] { + new WebViewProviderInfo(packageInfo.packageName, "", true, false, null) + }; + setupWithPackages(packages); + mTestSystemImpl.setPackageInfo(packageInfo); + + runWebViewBootPreparationOnMainSync(); + mWebViewUpdateServiceImpl.notifyRelroCreationCompleted(); + + assertEquals( + packageInfo.packageName, + mWebViewUpdateServiceImpl.getCurrentWebViewPackage().packageName); + + WebViewProviderResponse response = mWebViewUpdateServiceImpl.waitForAndGetProvider(); + assertEquals(packageInfo.packageName, response.packageInfo.packageName); + } + + @Test + @RequiresFlagsEnabled("android.webkit.update_service_v2") + public void testDisabledDefaultPackageChosen() { + PackageInfo disabledPackage = + createPackageInfo( + "disabledPackage", + false /* enabled */, + true /* valid */, + true /* installed */); + + testDefaultPackageChosen(disabledPackage); + } + + @Test + @RequiresFlagsEnabled("android.webkit.update_service_v2") + public void testUninstalledDefaultPackageChosen() { + PackageInfo uninstalledPackage = + createPackageInfo( + "uninstalledPackage", + true /* enabled */, + true /* valid */, + false /* installed */); + + testDefaultPackageChosen(uninstalledPackage); + } } diff --git a/services/tests/servicestests/test-apps/JobTestApp/AndroidManifest.xml b/services/tests/servicestests/test-apps/JobTestApp/AndroidManifest.xml deleted file mode 100644 index ac35805e8af6..000000000000 --- a/services/tests/servicestests/test-apps/JobTestApp/AndroidManifest.xml +++ /dev/null @@ -1,27 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- 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. ---> - -<manifest xmlns:android="http://schemas.android.com/apk/res/android" - package="com.android.servicestests.apps.jobtestapp"> - - <application> - <service android:name=".TestJobService" - android:permission="android.permission.BIND_JOB_SERVICE" /> - <activity android:name=".TestJobActivity" - android:exported="true" /> - </application> - -</manifest>
\ No newline at end of file diff --git a/services/tests/servicestests/test-apps/JobTestApp/OWNERS b/services/tests/servicestests/test-apps/JobTestApp/OWNERS deleted file mode 100644 index 6f207fb1a00e..000000000000 --- a/services/tests/servicestests/test-apps/JobTestApp/OWNERS +++ /dev/null @@ -1 +0,0 @@ -include /apex/jobscheduler/OWNERS diff --git a/services/tests/servicestests/test-apps/JobTestApp/src/com/android/servicestests/apps/jobtestapp/TestJobActivity.java b/services/tests/servicestests/test-apps/JobTestApp/src/com/android/servicestests/apps/jobtestapp/TestJobActivity.java deleted file mode 100644 index 99eb196e3298..000000000000 --- a/services/tests/servicestests/test-apps/JobTestApp/src/com/android/servicestests/apps/jobtestapp/TestJobActivity.java +++ /dev/null @@ -1,67 +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.servicestests.apps.jobtestapp; - -import android.app.Activity; -import android.app.job.JobInfo; -import android.app.job.JobScheduler; -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.os.Bundle; -import android.util.Log; - -public class TestJobActivity extends Activity { - private static final String TAG = TestJobActivity.class.getSimpleName(); - private static final String PACKAGE_NAME = "com.android.servicestests.apps.jobtestapp"; - - public static final String EXTRA_JOB_ID_KEY = PACKAGE_NAME + ".extra.JOB_ID"; - public static final String ACTION_START_JOB = PACKAGE_NAME + ".action.START_JOB"; - public static final String ACTION_CANCEL_JOBS = PACKAGE_NAME + ".action.CANCEL_JOBS"; - public static final int JOB_INITIAL_BACKOFF = 10_000; - public static final int JOB_MINIMUM_LATENCY = 5_000; - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - ComponentName jobServiceComponent = new ComponentName(this, TestJobService.class); - JobScheduler jobScheduler = (JobScheduler) getSystemService(Context.JOB_SCHEDULER_SERVICE); - final Intent intent = getIntent(); - switch (intent.getAction()) { - case ACTION_CANCEL_JOBS: - jobScheduler.cancelAll(); - Log.d(TAG, "Cancelled all jobs for " + getPackageName()); - break; - case ACTION_START_JOB: - final int jobId = intent.getIntExtra(EXTRA_JOB_ID_KEY, hashCode()); - JobInfo.Builder jobBuilder = new JobInfo.Builder(jobId, jobServiceComponent) - .setBackoffCriteria(JOB_INITIAL_BACKOFF, JobInfo.BACKOFF_POLICY_LINEAR) - .setMinimumLatency(JOB_MINIMUM_LATENCY) - .setOverrideDeadline(JOB_MINIMUM_LATENCY); - final int result = jobScheduler.schedule(jobBuilder.build()); - if (result != JobScheduler.RESULT_SUCCESS) { - Log.e(TAG, "Could not schedule job " + jobId); - } else { - Log.d(TAG, "Successfully scheduled job with id " + jobId); - } - break; - default: - Log.e(TAG, "Unknown action " + intent.getAction()); - } - finish(); - } -}
\ No newline at end of file diff --git a/services/tests/servicestests/test-apps/JobTestApp/src/com/android/servicestests/apps/jobtestapp/TestJobService.java b/services/tests/servicestests/test-apps/JobTestApp/src/com/android/servicestests/apps/jobtestapp/TestJobService.java deleted file mode 100644 index b8585f26c185..000000000000 --- a/services/tests/servicestests/test-apps/JobTestApp/src/com/android/servicestests/apps/jobtestapp/TestJobService.java +++ /dev/null @@ -1,53 +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.servicestests.apps.jobtestapp; - -import android.annotation.TargetApi; -import android.app.job.JobParameters; -import android.app.job.JobService; -import android.content.Intent; -import android.util.Log; - -@TargetApi(24) -public class TestJobService extends JobService { - private static final String TAG = TestJobService.class.getSimpleName(); - private static final String PACKAGE_NAME = "com.android.servicestests.apps.jobtestapp"; - public static final String ACTION_JOB_STARTED = PACKAGE_NAME + ".action.JOB_STARTED"; - public static final String ACTION_JOB_STOPPED = PACKAGE_NAME + ".action.JOB_STOPPED"; - public static final String JOB_PARAMS_EXTRA_KEY = PACKAGE_NAME + ".extra.JOB_PARAMETERS"; - - @Override - public boolean onStartJob(JobParameters params) { - Log.i(TAG, "Test job executing: " + params.getJobId()); - Intent reportJobStartIntent = new Intent(ACTION_JOB_STARTED); - reportJobStartIntent.putExtra(JOB_PARAMS_EXTRA_KEY, params) - .addFlags(Intent.FLAG_RECEIVER_FOREGROUND); - sendBroadcast(reportJobStartIntent); - return true; - } - - @Override - public boolean onStopJob(JobParameters params) { - Log.i(TAG, "Test job stopped executing: " + params.getJobId()); - Intent reportJobStopIntent = new Intent(ACTION_JOB_STOPPED); - reportJobStopIntent.putExtra(JOB_PARAMS_EXTRA_KEY, params) - .addFlags(Intent.FLAG_RECEIVER_FOREGROUND); - sendBroadcast(reportJobStopIntent); - // Deadline constraint is dropped on reschedule, so it's more reliable to use a new job. - return false; - } -} diff --git a/services/tests/uiservicestests/Android.bp b/services/tests/uiservicestests/Android.bp index 4e1c72af2727..2f29d10ec2f9 100644 --- a/services/tests/uiservicestests/Android.bp +++ b/services/tests/uiservicestests/Android.bp @@ -47,6 +47,7 @@ android_test { "flag-junit", "notification_flags_lib", "platform-test-rules", + "SettingsLib", ], libs: [ diff --git a/services/tests/uiservicestests/src/com/android/server/UiModeManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/UiModeManagerServiceTest.java index db4653208f62..839cf7ce4926 100644 --- a/services/tests/uiservicestests/src/com/android/server/UiModeManagerServiceTest.java +++ b/services/tests/uiservicestests/src/com/android/server/UiModeManagerServiceTest.java @@ -17,6 +17,9 @@ package com.android.server; import static android.Manifest.permission.MODIFY_DAY_NIGHT_MODE; +import static android.app.UiModeManager.MODE_ATTENTION_THEME_OVERLAY_DAY; +import static android.app.UiModeManager.MODE_ATTENTION_THEME_OVERLAY_NIGHT; +import static android.app.UiModeManager.MODE_ATTENTION_THEME_OVERLAY_OFF; import static android.app.UiModeManager.MODE_NIGHT_AUTO; import static android.app.UiModeManager.MODE_NIGHT_CUSTOM; import static android.app.UiModeManager.MODE_NIGHT_CUSTOM_TYPE_BEDTIME; @@ -32,6 +35,7 @@ import static com.android.server.UiModeManagerService.SUPPORTED_NIGHT_MODE_CUSTO import static com.google.common.truth.Truth.assertThat; +import static junit.framework.Assert.fail; import static junit.framework.TestCase.assertFalse; import static junit.framework.TestCase.assertTrue; @@ -65,6 +69,7 @@ import static org.testng.Assert.assertThrows; import android.Manifest; import android.app.Activity; import android.app.AlarmManager; +import android.app.Flags; import android.app.IOnProjectionStateChangedListener; import android.app.IUiModeManager; import android.content.BroadcastReceiver; @@ -84,6 +89,8 @@ import android.os.Process; import android.os.RemoteException; import android.os.UserHandle; import android.os.test.FakePermissionEnforcer; +import android.platform.test.annotations.EnableFlags; +import android.platform.test.flag.junit.SetFlagsRule; import android.provider.Settings; import android.service.dreams.DreamManagerInternal; import android.test.mock.MockContentResolver; @@ -98,6 +105,7 @@ import com.android.server.wm.WindowManagerInternal; import org.junit.Before; import org.junit.Ignore; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; @@ -109,6 +117,7 @@ import java.time.LocalDateTime; import java.time.LocalTime; import java.time.ZoneId; import java.util.List; +import java.util.Map; import java.util.function.Consumer; @RunWith(AndroidTestingRunner.class) @@ -159,6 +168,11 @@ public class UiModeManagerServiceTest extends UiServiceTestCase { private TwilightListener mTwilightListener; private FakePermissionEnforcer mPermissionEnforcer; + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule( + SetFlagsRule.DefaultInitValueType.DEVICE_DEFAULT); + + @Before public void setUp() { // The AIDL stub will use PermissionEnforcer to check permission from the caller. @@ -1437,6 +1451,51 @@ public class UiModeManagerServiceTest extends UiServiceTestCase { verify(mInjector).startDreamWhenDockedIfAppropriate(mContext); } + private void testAttentionModeThemeOverlay(boolean modeNight) throws RemoteException { + //setup + if (modeNight) { + mService.setNightMode(MODE_NIGHT_YES); + assertTrue(mUiManagerService.getConfiguration().isNightModeActive()); + } else { + mService.setNightMode(MODE_NIGHT_NO); + assertFalse(mUiManagerService.getConfiguration().isNightModeActive()); + } + + // attention modes with expected night modes + Map<Integer, Boolean> modes = Map.of( + MODE_ATTENTION_THEME_OVERLAY_OFF, modeNight, + MODE_ATTENTION_THEME_OVERLAY_DAY, false, + MODE_ATTENTION_THEME_OVERLAY_NIGHT, true + ); + + // test + for (int aMode : modes.keySet()) { + try { + mService.setAttentionModeThemeOverlay(aMode); + + int appliedAMode = mService.getAttentionModeThemeOverlay(); + boolean nMode = modes.get(aMode); + + assertEquals(aMode, appliedAMode); + assertEquals(isNightModeActivated(), nMode); + } catch (RemoteException e) { + fail("Error communicating with server: " + e.getMessage()); + } + } + } + + @Test + @EnableFlags(Flags.FLAG_MODES_API) + public void testAttentionModeThemeOverlay_nightModeDisabled() throws RemoteException { + testAttentionModeThemeOverlay(false); + } + + @Test + @EnableFlags(Flags.FLAG_MODES_API) + public void testAttentionModeThemeOverlay_nightModeEnabled() throws RemoteException { + testAttentionModeThemeOverlay(true); + } + private void triggerDockIntent() { final Intent dockedIntent = new Intent(Intent.ACTION_DOCK_EVENT) diff --git a/services/tests/uiservicestests/src/com/android/server/notification/DefaultDeviceEffectsApplierTest.java b/services/tests/uiservicestests/src/com/android/server/notification/DefaultDeviceEffectsApplierTest.java index 30843d222742..3797dbb97213 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/DefaultDeviceEffectsApplierTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/DefaultDeviceEffectsApplierTest.java @@ -16,7 +16,8 @@ package com.android.server.notification; -import static android.app.UiModeManager.MODE_NIGHT_CUSTOM_TYPE_BEDTIME; +import static android.app.UiModeManager.MODE_ATTENTION_THEME_OVERLAY_NIGHT; +import static android.app.UiModeManager.MODE_ATTENTION_THEME_OVERLAY_OFF; import static android.service.notification.ZenModeConfig.UPDATE_ORIGIN_APP; import static android.service.notification.ZenModeConfig.UPDATE_ORIGIN_USER; @@ -121,28 +122,49 @@ public class DefaultDeviceEffectsApplierTest { verify(mPowerManager).suppressAmbientDisplay(anyString(), eq(true)); verify(mColorDisplayManager).setSaturationLevel(eq(0)); verify(mWallpaperManager).setWallpaperDimAmount(eq(0.6f)); - verify(mUiModeManager).setNightModeActivatedForCustomMode( - eq(MODE_NIGHT_CUSTOM_TYPE_BEDTIME), eq(true)); + verify(mUiModeManager).setAttentionModeThemeOverlay(eq(MODE_ATTENTION_THEME_OVERLAY_NIGHT)); } @Test - public void apply_removesPreviouslyAppliedEffects() { + public void apply_removesEffects() { mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API); ZenDeviceEffects previousEffects = new ZenDeviceEffects.Builder() .setShouldSuppressAmbientDisplay(true) .setShouldDimWallpaper(true) + .setShouldDisplayGrayscale(true) + .setShouldUseNightMode(true) .build(); mApplier.apply(previousEffects, UPDATE_ORIGIN_USER); verify(mPowerManager).suppressAmbientDisplay(anyString(), eq(true)); + verify(mColorDisplayManager).setSaturationLevel(eq(0)); verify(mWallpaperManager).setWallpaperDimAmount(eq(0.6f)); + verify(mUiModeManager).setAttentionModeThemeOverlay(eq(MODE_ATTENTION_THEME_OVERLAY_NIGHT)); ZenDeviceEffects noEffects = new ZenDeviceEffects.Builder().build(); mApplier.apply(noEffects, UPDATE_ORIGIN_USER); verify(mPowerManager).suppressAmbientDisplay(anyString(), eq(false)); + verify(mColorDisplayManager).setSaturationLevel(eq(100)); verify(mWallpaperManager).setWallpaperDimAmount(eq(0.0f)); - verifyZeroInteractions(mColorDisplayManager, mUiModeManager); + verify(mUiModeManager).setAttentionModeThemeOverlay(eq(MODE_ATTENTION_THEME_OVERLAY_OFF)); + } + + @Test + public void apply_removesOnlyPreviouslyAppliedEffects() { + mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API); + + ZenDeviceEffects previousEffects = new ZenDeviceEffects.Builder() + .setShouldSuppressAmbientDisplay(true) + .build(); + mApplier.apply(previousEffects, UPDATE_ORIGIN_USER); + verify(mPowerManager).suppressAmbientDisplay(anyString(), eq(true)); + + ZenDeviceEffects noEffects = new ZenDeviceEffects.Builder().build(); + mApplier.apply(noEffects, UPDATE_ORIGIN_USER); + + verify(mPowerManager).suppressAmbientDisplay(anyString(), eq(false)); + verifyZeroInteractions(mColorDisplayManager, mWallpaperManager, mUiModeManager); } @Test @@ -150,6 +172,7 @@ public class DefaultDeviceEffectsApplierTest { mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API); mContext.addMockSystemService(ColorDisplayManager.class, null); mContext.addMockSystemService(WallpaperManager.class, null); + mApplier = new DefaultDeviceEffectsApplier(mContext); ZenDeviceEffects effects = new ZenDeviceEffects.Builder() .setShouldSuppressAmbientDisplay(true) @@ -177,7 +200,7 @@ public class DefaultDeviceEffectsApplierTest { verify(mWallpaperManager).setWallpaperDimAmount(eq(0.6f)); verify(mPowerManager, never()).suppressAmbientDisplay(anyString(), anyBoolean()); - verify(mUiModeManager, never()).setNightModeActivatedForCustomMode(anyInt(), anyBoolean()); + verify(mUiModeManager, never()).setAttentionModeThemeOverlay(anyInt()); } @Test @@ -223,8 +246,7 @@ public class DefaultDeviceEffectsApplierTest { screenOffReceiver.onReceive(mContext, new Intent(Intent.ACTION_SCREEN_OFF)); // So the effect is applied, and we stopped listening for this event. - verify(mUiModeManager).setNightModeActivatedForCustomMode( - eq(MODE_NIGHT_CUSTOM_TYPE_BEDTIME), eq(true)); + verify(mUiModeManager).setAttentionModeThemeOverlay(eq(MODE_ATTENTION_THEME_OVERLAY_NIGHT)); verify(mContext).unregisterReceiver(eq(screenOffReceiver)); } @@ -239,8 +261,7 @@ public class DefaultDeviceEffectsApplierTest { origin.value()); // Effect was applied, and no broadcast receiver was registered. - verify(mUiModeManager).setNightModeActivatedForCustomMode( - eq(MODE_NIGHT_CUSTOM_TYPE_BEDTIME), eq(true)); + verify(mUiModeManager).setAttentionModeThemeOverlay(eq(MODE_ATTENTION_THEME_OVERLAY_NIGHT)); verify(mContext, never()).registerReceiver(any(), any(), anyInt()); } @@ -256,8 +277,7 @@ public class DefaultDeviceEffectsApplierTest { origin.value()); // Effect was applied, and no broadcast receiver was registered. - verify(mUiModeManager).setNightModeActivatedForCustomMode( - eq(MODE_NIGHT_CUSTOM_TYPE_BEDTIME), eq(true)); + verify(mUiModeManager).setAttentionModeThemeOverlay(eq(MODE_ATTENTION_THEME_OVERLAY_NIGHT)); verify(mContext, never()).registerReceiver(any(), any(), anyInt()); } diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java index c1f35ccb69e0..723ac15fb50f 100755 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java @@ -13792,8 +13792,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { NotificationManager.Policy policy = new NotificationManager.Policy(0, 0, 0); mBinderService.setNotificationPolicy("package", policy, false); - verify(zenHelper).applyGlobalPolicyAsImplicitZenRule(eq("package"), anyInt(), eq(policy), - eq(ZenModeConfig.UPDATE_ORIGIN_APP)); + verify(zenHelper).applyGlobalPolicyAsImplicitZenRule(eq("package"), anyInt(), eq(policy)); } @Test @@ -13859,7 +13858,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { verify(zenModeHelper).setNotificationPolicy(eq(policy), anyInt(), anyInt()); } else { verify(zenModeHelper).applyGlobalPolicyAsImplicitZenRule(anyString(), anyInt(), - eq(policy), anyInt()); + eq(policy)); } } 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 08af09c20de5..0e20daf2c0f1 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,12 @@ public class ZenAdaptersTest extends UiServiceTestCase { Policy.policyState(false, true), 0); ZenPolicy zenPolicy = notificationPolicyToZenPolicy(policy); - assertThat(zenPolicy.getAllowedChannels()).isEqualTo(ZenPolicy.CHANNEL_TYPE_PRIORITY); + assertThat(zenPolicy.getPriorityChannels()).isEqualTo(ZenPolicy.STATE_ALLOW); Policy notAllowed = new Policy(0, 0, 0, 0, Policy.policyState(false, false), 0); ZenPolicy zenPolicyNotAllowed = notificationPolicyToZenPolicy(notAllowed); - assertThat(zenPolicyNotAllowed.getAllowedChannels()).isEqualTo(ZenPolicy.CHANNEL_TYPE_NONE); + assertThat(zenPolicyNotAllowed.getPriorityChannels()).isEqualTo(ZenPolicy.STATE_DISALLOW); } @Test @@ -158,12 +158,11 @@ public class ZenAdaptersTest extends UiServiceTestCase { Policy.policyState(false, true), 0); ZenPolicy zenPolicy = notificationPolicyToZenPolicy(policy); - assertThat(zenPolicy.getAllowedChannels()).isEqualTo(ZenPolicy.CHANNEL_TYPE_UNSET); + assertThat(zenPolicy.getPriorityChannels()).isEqualTo(ZenPolicy.STATE_UNSET); Policy notAllowed = new Policy(0, 0, 0, 0, Policy.policyState(false, false), 0); ZenPolicy zenPolicyNotAllowed = notificationPolicyToZenPolicy(notAllowed); - assertThat(zenPolicyNotAllowed.getAllowedChannels()) - .isEqualTo(ZenPolicy.CHANNEL_TYPE_UNSET); + assertThat(zenPolicyNotAllowed.getPriorityChannels()).isEqualTo(ZenPolicy.STATE_UNSET); } } diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenDeviceEffectsTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenDeviceEffectsTest.java index 3d8ec2ec9277..f604f1e77cf4 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/ZenDeviceEffectsTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenDeviceEffectsTest.java @@ -52,7 +52,6 @@ public class ZenDeviceEffectsTest extends UiServiceTestCase { .setShouldMaximizeDoze(true) .setShouldUseNightMode(false) .setShouldSuppressAmbientDisplay(false).setShouldSuppressAmbientDisplay(true) - .setUserModifiedFields(8) .build(); assertThat(deviceEffects.shouldDimWallpaper()).isTrue(); @@ -65,7 +64,6 @@ public class ZenDeviceEffectsTest extends UiServiceTestCase { assertThat(deviceEffects.shouldMinimizeRadioUsage()).isFalse(); assertThat(deviceEffects.shouldUseNightMode()).isFalse(); assertThat(deviceEffects.shouldSuppressAmbientDisplay()).isTrue(); - assertThat(deviceEffects.getUserModifiedFields()).isEqualTo(8); } @Test @@ -97,7 +95,6 @@ public class ZenDeviceEffectsTest extends UiServiceTestCase { .setShouldMinimizeRadioUsage(true) .setShouldUseNightMode(true) .setShouldSuppressAmbientDisplay(true) - .setUserModifiedFields(6) .build(); Parcel parcel = Parcel.obtain(); @@ -116,7 +113,6 @@ public class ZenDeviceEffectsTest extends UiServiceTestCase { assertThat(copy.shouldUseNightMode()).isTrue(); assertThat(copy.shouldSuppressAmbientDisplay()).isTrue(); assertThat(copy.shouldDisplayGrayscale()).isFalse(); - assertThat(copy.getUserModifiedFields()).isEqualTo(6); } @Test 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 dd252f3ffd20..e523e79f6370 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java @@ -164,7 +164,7 @@ public class ZenModeConfigTest extends UiServiceTestCase { .allowConversations(CONVERSATION_SENDERS_IMPORTANT) .showLights(false) .showInAmbientDisplay(false) - .allowChannels(ZenPolicy.CHANNEL_TYPE_NONE) + .allowPriorityChannels(false) .build(); Policy originalPolicy = config.toNotificationPolicy(); @@ -255,7 +255,7 @@ public class ZenModeConfigTest extends UiServiceTestCase { .allowCalls(ZenPolicy.PEOPLE_TYPE_CONTACTS) .allowMessages(ZenPolicy.PEOPLE_TYPE_STARRED) .allowConversations(ZenPolicy.CONVERSATION_SENDERS_NONE) - .allowChannels(ZenPolicy.CHANNEL_TYPE_NONE) + .allowPriorityChannels(false) .build(); ZenModeConfig config = getMutedAllConfig(); @@ -284,8 +284,7 @@ public class ZenModeConfigTest extends UiServiceTestCase { actual.getPriorityConversationSenders()); assertEquals(expected.getPriorityCallSenders(), actual.getPriorityCallSenders()); assertEquals(expected.getPriorityMessageSenders(), actual.getPriorityMessageSenders()); - assertEquals(expected.getAllowedChannels(), actual.getAllowedChannels()); - assertEquals(expected.getUserModifiedFields(), actual.getUserModifiedFields()); + assertEquals(expected.getPriorityChannels(), actual.getPriorityChannels()); } @Test @@ -342,45 +341,32 @@ public class ZenModeConfigTest extends UiServiceTestCase { ZenModeConfig.ZenRule rule = new ZenModeConfig.ZenRule(); rule.zenPolicy = null; rule.zenDeviceEffects = null; - assertThat(rule.canBeUpdatedByApp()).isTrue(); rule.userModifiedFields = 1; + assertThat(rule.canBeUpdatedByApp()).isFalse(); } @Test public void testCanBeUpdatedByApp_policyModified() throws Exception { - ZenPolicy.Builder policyBuilder = new ZenPolicy.Builder().setUserModifiedFields(0); - ZenPolicy policy = policyBuilder.build(); - ZenModeConfig.ZenRule rule = new ZenModeConfig.ZenRule(); - rule.zenPolicy = policy; - - assertThat(rule.userModifiedFields).isEqualTo(0); + rule.zenPolicy = new ZenPolicy(); assertThat(rule.canBeUpdatedByApp()).isTrue(); - policy = policyBuilder.setUserModifiedFields(1).build(); - assertThat(policy.getUserModifiedFields()).isEqualTo(1); - rule.zenPolicy = policy; + rule.zenPolicyUserModifiedFields = 1; + assertThat(rule.canBeUpdatedByApp()).isFalse(); } @Test public void testCanBeUpdatedByApp_deviceEffectsModified() throws Exception { - ZenDeviceEffects.Builder deviceEffectsBuilder = - new ZenDeviceEffects.Builder().setUserModifiedFields(0); - ZenDeviceEffects deviceEffects = deviceEffectsBuilder.build(); - ZenModeConfig.ZenRule rule = new ZenModeConfig.ZenRule(); - rule.zenDeviceEffects = deviceEffects; - - assertThat(rule.userModifiedFields).isEqualTo(0); + rule.zenDeviceEffects = new ZenDeviceEffects.Builder().build(); assertThat(rule.canBeUpdatedByApp()).isTrue(); - deviceEffects = deviceEffectsBuilder.setUserModifiedFields(1).build(); - assertThat(deviceEffects.getUserModifiedFields()).isEqualTo(1); - rule.zenDeviceEffects = deviceEffects; + rule.zenDeviceEffectsUserModifiedFields = 1; + assertThat(rule.canBeUpdatedByApp()).isFalse(); } @@ -406,6 +392,8 @@ public class ZenModeConfigTest extends UiServiceTestCase { rule.allowManualInvocation = ALLOW_MANUAL; rule.type = TYPE; rule.userModifiedFields = 16; + rule.zenPolicyUserModifiedFields = 5; + rule.zenDeviceEffectsUserModifiedFields = 2; rule.iconResName = ICON_RES_NAME; rule.triggerDescription = TRIGGER_DESC; rule.deletionInstant = Instant.ofEpochMilli(1701790147000L); @@ -432,6 +420,9 @@ public class ZenModeConfigTest extends UiServiceTestCase { assertEquals(rule.iconResName, parceled.iconResName); assertEquals(rule.type, parceled.type); assertEquals(rule.userModifiedFields, parceled.userModifiedFields); + assertEquals(rule.zenPolicyUserModifiedFields, parceled.zenPolicyUserModifiedFields); + assertEquals(rule.zenDeviceEffectsUserModifiedFields, + parceled.zenDeviceEffectsUserModifiedFields); assertEquals(rule.triggerDescription, parceled.triggerDescription); assertEquals(rule.zenPolicy, parceled.zenPolicy); assertEquals(rule.deletionInstant, parceled.deletionInstant); @@ -511,6 +502,8 @@ public class ZenModeConfigTest extends UiServiceTestCase { rule.allowManualInvocation = ALLOW_MANUAL; rule.type = TYPE; rule.userModifiedFields = 4; + rule.zenPolicyUserModifiedFields = 5; + rule.zenDeviceEffectsUserModifiedFields = 2; rule.iconResName = ICON_RES_NAME; rule.triggerDescription = TRIGGER_DESC; rule.deletionInstant = Instant.ofEpochMilli(1701790147000L); @@ -541,6 +534,9 @@ public class ZenModeConfigTest extends UiServiceTestCase { assertEquals(rule.allowManualInvocation, fromXml.allowManualInvocation); assertEquals(rule.type, fromXml.type); assertEquals(rule.userModifiedFields, fromXml.userModifiedFields); + assertEquals(rule.zenPolicyUserModifiedFields, fromXml.zenPolicyUserModifiedFields); + assertEquals(rule.zenDeviceEffectsUserModifiedFields, + fromXml.zenDeviceEffectsUserModifiedFields); assertEquals(rule.triggerDescription, fromXml.triggerDescription); assertEquals(rule.iconResName, fromXml.iconResName); assertEquals(rule.deletionInstant, fromXml.deletionInstant); @@ -694,10 +690,9 @@ public class ZenModeConfigTest extends UiServiceTestCase { .allowSystem(true) .allowReminders(false) .allowEvents(true) - .allowChannels(ZenPolicy.CHANNEL_TYPE_NONE) + .allowPriorityChannels(false) .hideAllVisualEffects() .showVisualEffect(ZenPolicy.VISUAL_EFFECT_AMBIENT, true) - .setUserModifiedFields(4) .build(); ByteArrayOutputStream baos = new ByteArrayOutputStream(); @@ -721,7 +716,7 @@ public class ZenModeConfigTest extends UiServiceTestCase { assertEquals(policy.getPriorityCategorySystem(), fromXml.getPriorityCategorySystem()); assertEquals(policy.getPriorityCategoryReminders(), fromXml.getPriorityCategoryReminders()); assertEquals(policy.getPriorityCategoryEvents(), fromXml.getPriorityCategoryEvents()); - assertEquals(policy.getAllowedChannels(), fromXml.getAllowedChannels()); + assertEquals(policy.getPriorityChannels(), fromXml.getPriorityChannels()); assertEquals(policy.getVisualEffectFullScreenIntent(), fromXml.getVisualEffectFullScreenIntent()); @@ -732,7 +727,6 @@ public class ZenModeConfigTest extends UiServiceTestCase { assertEquals(policy.getVisualEffectAmbient(), fromXml.getVisualEffectAmbient()); assertEquals(policy.getVisualEffectNotificationList(), fromXml.getVisualEffectNotificationList()); - assertEquals(policy.getUserModifiedFields(), fromXml.getUserModifiedFields()); } private ZenModeConfig getMutedRingerConfig() { diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeDiffTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeDiffTest.java index 9d7cf53e62db..2e64645ecade 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeDiffTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeDiffTest.java @@ -73,13 +73,15 @@ public class ZenModeDiffTest extends UiServiceTestCase { : Set.of("version", "manualRule", "automaticRules"); // Differences for flagged fields are only generated if the flag is enabled. - // "Metadata" fields (userModifiedFields, deletionInstant) are not compared. + // "Metadata" fields (userModifiedFields & co, deletionInstant) are not compared. private static final Set<String> ZEN_RULE_EXEMPT_FIELDS = android.app.Flags.modesApi() - ? Set.of("userModifiedFields", "deletionInstant") + ? Set.of("userModifiedFields", "zenPolicyUserModifiedFields", + "zenDeviceEffectsUserModifiedFields", "deletionInstant") : Set.of(RuleDiff.FIELD_TYPE, RuleDiff.FIELD_TRIGGER_DESCRIPTION, RuleDiff.FIELD_ICON_RES, RuleDiff.FIELD_ALLOW_MANUAL, RuleDiff.FIELD_ZEN_DEVICE_EFFECTS, "userModifiedFields", + "zenPolicyUserModifiedFields", "zenDeviceEffectsUserModifiedFields", "deletionInstant"); // allowPriorityChannels is flagged by android.app.modes_api diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeFilteringTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeFilteringTest.java index 29208f44c64f..7d6e12cf7cb4 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeFilteringTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeFilteringTest.java @@ -546,13 +546,13 @@ public class ZenModeFilteringTest extends UiServiceTestCase { // Create a policy to allow channels through, which means shouldIntercept is false ZenModeConfig config = new ZenModeConfig(); Policy policy = config.toNotificationPolicy(new ZenPolicy.Builder() - .allowChannels(ZenPolicy.CHANNEL_TYPE_PRIORITY) + .allowPriorityChannels(true) .build()); assertFalse(mZenModeFiltering.shouldIntercept(ZEN_MODE_IMPORTANT_INTERRUPTIONS, policy, r)); // Now create a policy which does not allow priority channels: policy = config.toNotificationPolicy(new ZenPolicy.Builder() - .allowChannels(ZenPolicy.CHANNEL_TYPE_NONE) + .allowPriorityChannels(false) .build()); assertTrue(mZenModeFiltering.shouldIntercept(ZEN_MODE_IMPORTANT_INTERRUPTIONS, policy, r)); } 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 9e3e336fa12f..edc876aab388 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java @@ -46,6 +46,7 @@ import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_PEEK; import static android.content.pm.PackageManager.PERMISSION_GRANTED; import static android.provider.Settings.Global.ZEN_MODE_ALARMS; import static android.provider.Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS; +import static android.provider.Settings.Global.ZEN_MODE_NO_INTERRUPTIONS; import static android.provider.Settings.Global.ZEN_MODE_OFF; import static android.service.notification.Condition.SOURCE_SCHEDULE; import static android.service.notification.Condition.SOURCE_USER_ACTION; @@ -67,6 +68,7 @@ import static com.android.os.dnd.DNDProtoEnums.STATE_ALLOW; import static com.android.os.dnd.DNDProtoEnums.STATE_DISALLOW; import static com.android.server.notification.ZenModeHelper.RULE_LIMIT_PER_PACKAGE; +import static com.google.common.collect.Iterables.getOnlyElement; import static com.google.common.truth.Truth.assertThat; import static junit.framework.Assert.assertEquals; @@ -295,6 +297,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { when(appInfoSpy.loadLabel(any())).thenReturn(CUSTOM_APP_LABEL); when(mPackageManager.getApplicationInfo(eq(CUSTOM_PKG_NAME), anyInt())) .thenReturn(appInfoSpy); + when(mPackageManager.getApplicationInfo(eq(mContext.getPackageName()), anyInt())) + .thenReturn(appInfoSpy); mZenModeHelper.mPm = mPackageManager; mZenModeEventLogger.reset(); @@ -1151,7 +1155,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { .allowAlarms(true) .allowRepeatCallers(false) .allowCalls(PEOPLE_TYPE_STARRED) - .allowChannels(ZenPolicy.CHANNEL_TYPE_NONE) + .allowPriorityChannels(false) .build(); mZenModeHelper.mConfig.automaticRules.put(rule.id, rule); List<StatsEvent> events = new LinkedList<>(); @@ -1174,7 +1178,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { assertThat(policy.getAllowCallsFrom().getNumber()) .isEqualTo(DNDProtoEnums.PEOPLE_STARRED); assertThat(policy.getAllowChannels().getNumber()) - .isEqualTo(DNDProtoEnums.CHANNEL_TYPE_NONE); + .isEqualTo(DNDProtoEnums.CHANNEL_POLICY_NONE); } } assertTrue("couldn't find custom rule", foundCustomEvent); @@ -2236,12 +2240,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { AutomaticZenRule savedRule = mZenModeHelper.getAutomaticZenRule(ruleId); - // savedRule.getDeviceEffects() is equal to zde, except for the userModifiedFields. - // So we clear before comparing. - ZenDeviceEffects savedEffects = new ZenDeviceEffects.Builder(savedRule.getDeviceEffects()) - .setUserModifiedFields(0).build(); - - assertThat(savedEffects).isEqualTo(zde); + assertThat(savedRule.getDeviceEffects()).isEqualTo(zde); } @Test @@ -2331,12 +2330,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { AutomaticZenRule savedRule = mZenModeHelper.getAutomaticZenRule(ruleId); - // savedRule.getDeviceEffects() is equal to updateFromUser, except for the - // userModifiedFields, so we clear before comparing. - ZenDeviceEffects savedEffects = new ZenDeviceEffects.Builder(savedRule.getDeviceEffects()) - .setUserModifiedFields(0).build(); - - assertThat(savedEffects).isEqualTo(updateFromUser); + assertThat(savedRule.getDeviceEffects()).isEqualTo(updateFromUser); } @Test @@ -3098,7 +3092,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { DNDPolicyProto origDndProto = mZenModeEventLogger.getPolicyProto(0); checkDndProtoMatchesSetupZenConfig(origDndProto); assertThat(origDndProto.getAllowChannels().getNumber()) - .isEqualTo(DNDProtoEnums.CHANNEL_TYPE_PRIORITY); + .isEqualTo(DNDProtoEnums.CHANNEL_POLICY_PRIORITY); // Second message where we change the policy: // - DND_POLICY_CHANGED (indicates only the policy changed and nothing else) @@ -3110,7 +3104,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { .isEqualTo(DNDProtoEnums.UNKNOWN_RULE); DNDPolicyProto dndProto = mZenModeEventLogger.getPolicyProto(1); assertThat(dndProto.getAllowChannels().getNumber()) - .isEqualTo(DNDProtoEnums.CHANNEL_TYPE_NONE); + .isEqualTo(DNDProtoEnums.CHANNEL_POLICY_NONE); } @Test @@ -3299,7 +3293,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { // one rule, custom policy, allows channels ZenPolicy customPolicy = new ZenPolicy.Builder() - .allowChannels(ZenPolicy.CHANNEL_TYPE_PRIORITY) + .allowPriorityChannels(true) .build(); AutomaticZenRule zenRule = new AutomaticZenRule("name", @@ -3321,7 +3315,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { // add new rule with policy that disallows channels ZenPolicy strictPolicy = new ZenPolicy.Builder() - .allowChannels(ZenPolicy.CHANNEL_TYPE_NONE) + .allowPriorityChannels(false) .build(); AutomaticZenRule zenRule2 = new AutomaticZenRule("name2", @@ -3411,7 +3405,6 @@ public class ZenModeHelperTest extends UiServiceTestCase { rule.allowManualInvocation = ALLOW_MANUAL; rule.type = TYPE; - rule.userModifiedFields = AutomaticZenRule.FIELD_NAME; rule.iconResName = ICON_RES_NAME; rule.triggerDescription = TRIGGER_DESC; @@ -3426,7 +3419,6 @@ public class ZenModeHelperTest extends UiServiceTestCase { assertEquals(POLICY, actual.getZenPolicy()); assertEquals(CONFIG_ACTIVITY, actual.getConfigurationActivity()); assertEquals(TYPE, actual.getType()); - assertEquals(AutomaticZenRule.FIELD_NAME, actual.getUserModifiedFields()); assertEquals(ALLOW_MANUAL, actual.isManualInvocationAllowed()); assertEquals(CREATION_TIME, actual.getCreationTime()); assertEquals(OWNER.getPackageName(), actual.getPackageName()); @@ -3453,29 +3445,31 @@ public class ZenModeHelperTest extends UiServiceTestCase { .setManualInvocationAllowed(ALLOW_MANUAL) .build(); - ZenModeConfig.ZenRule rule = new ZenModeConfig.ZenRule(); - - mZenModeHelper.populateZenRule(OWNER.getPackageName(), azr, rule, UPDATE_ORIGIN_APP, true); - - assertEquals(NAME, rule.name); - assertEquals(OWNER, rule.component); - assertEquals(CONDITION_ID, rule.conditionId); - assertEquals(INTERRUPTION_FILTER_ZR, rule.zenMode); - assertEquals(ENABLED, rule.enabled); - assertEquals(POLICY, rule.zenPolicy); - assertEquals(CONFIG_ACTIVITY, rule.configurationActivity); - assertEquals(TYPE, rule.type); - assertEquals(ALLOW_MANUAL, rule.allowManualInvocation); - assertEquals(OWNER.getPackageName(), rule.getPkg()); - assertEquals(ICON_RES_NAME, rule.iconResName); + String ruleId = mZenModeHelper.addAutomaticZenRule(OWNER.getPackageName(), azr, + UPDATE_ORIGIN_APP, "add", CUSTOM_PKG_UID); + + ZenModeConfig.ZenRule storedRule = mZenModeHelper.mConfig.automaticRules.get(ruleId); + + assertThat(storedRule).isNotNull(); + assertEquals(NAME, storedRule.name); + assertEquals(OWNER, storedRule.component); + assertEquals(CONDITION_ID, storedRule.conditionId); + assertEquals(INTERRUPTION_FILTER_ZR, storedRule.zenMode); + assertEquals(ENABLED, storedRule.enabled); + assertEquals(POLICY, storedRule.zenPolicy); + assertEquals(CONFIG_ACTIVITY, storedRule.configurationActivity); + assertEquals(TYPE, storedRule.type); + assertEquals(ALLOW_MANUAL, storedRule.allowManualInvocation); + assertEquals(OWNER.getPackageName(), storedRule.getPkg()); + assertEquals(ICON_RES_NAME, storedRule.iconResName); // Because the origin of the update is the app, we don't expect the bitmask to change. - assertEquals(0, rule.userModifiedFields); - assertEquals(TRIGGER_DESC, rule.triggerDescription); + assertEquals(0, storedRule.userModifiedFields); + assertEquals(TRIGGER_DESC, storedRule.triggerDescription); } @Test @EnableFlags(Flags.FLAG_MODES_API) - public void automaticZenRuleToZenRule_updatesNameUnlessUserModified() { + public void updateAutomaticZenRule_fromApp_updatesNameUnlessUserModified() { // Add a starting rule with the name OriginalName. AutomaticZenRule azrBase = new AutomaticZenRule.Builder("OriginalName", CONDITION_ID) .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY) @@ -3492,7 +3486,6 @@ public class ZenModeHelperTest extends UiServiceTestCase { Process.SYSTEM_UID); rule = mZenModeHelper.getAutomaticZenRule(ruleId); assertThat(rule.getName()).isEqualTo("NewName"); - assertThat(rule.canUpdate()).isTrue(); // The user modifies some other field in the rule, which makes the rule as a whole not // app modifiable. @@ -3501,10 +3494,6 @@ public class ZenModeHelperTest extends UiServiceTestCase { .build(); mZenModeHelper.updateAutomaticZenRule(ruleId, azrUpdate, UPDATE_ORIGIN_USER, "reason", Process.SYSTEM_UID); - rule = mZenModeHelper.getAutomaticZenRule(ruleId); - assertThat(rule.getUserModifiedFields()) - .isEqualTo(AutomaticZenRule.FIELD_INTERRUPTION_FILTER); - assertThat(rule.canUpdate()).isFalse(); // ...but the app can still modify the name, because the name itself hasn't been modified // by the user. @@ -3524,8 +3513,6 @@ public class ZenModeHelperTest extends UiServiceTestCase { Process.SYSTEM_UID); rule = mZenModeHelper.getAutomaticZenRule(ruleId); assertThat(rule.getName()).isEqualTo("UserProvidedName"); - assertThat(rule.getUserModifiedFields()).isEqualTo(AutomaticZenRule.FIELD_NAME - | AutomaticZenRule.FIELD_INTERRUPTION_FILTER); // The app is no longer able to modify the name. azrUpdate = new AutomaticZenRule.Builder(rule) @@ -3539,7 +3526,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { @Test @EnableFlags(Flags.FLAG_MODES_API) - public void automaticZenRuleToZenRule_updatesBitmaskAndValueForUserOrigin() { + public void updateAutomaticZenRule_fromUser_updatesBitmaskAndValue() { // Adds a starting rule with empty zen policies and device effects AutomaticZenRule azrBase = new AutomaticZenRule.Builder(NAME, CONDITION_ID) .setZenPolicy(new ZenPolicy.Builder().build()) @@ -3552,7 +3539,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { // Modifies the zen policy and device effects ZenPolicy policy = new ZenPolicy.Builder(rule.getZenPolicy()) - .allowChannels(ZenPolicy.CHANNEL_TYPE_PRIORITY) + .allowPriorityChannels(true) .build(); ZenDeviceEffects deviceEffects = new ZenDeviceEffects.Builder(rule.getDeviceEffects()) @@ -3571,85 +3558,21 @@ public class ZenModeHelperTest extends UiServiceTestCase { // UPDATE_ORIGIN_USER should change the bitmask and change the values. assertThat(rule.getInterruptionFilter()).isEqualTo(INTERRUPTION_FILTER_PRIORITY); - assertThat(rule.getUserModifiedFields()) + assertThat(rule.getZenPolicy().getPriorityChannels()).isEqualTo(ZenPolicy.STATE_ALLOW); + assertThat(rule.getDeviceEffects().shouldDisplayGrayscale()).isTrue(); + + ZenRule storedRule = mZenModeHelper.mConfig.automaticRules.get(ruleId); + assertThat(storedRule.userModifiedFields) .isEqualTo(AutomaticZenRule.FIELD_INTERRUPTION_FILTER); - assertThat(rule.getZenPolicy().getUserModifiedFields()) + assertThat(storedRule.zenPolicyUserModifiedFields) .isEqualTo(ZenPolicy.FIELD_ALLOW_CHANNELS); - assertThat(rule.getZenPolicy().getAllowedChannels()) - .isEqualTo(ZenPolicy.CHANNEL_TYPE_PRIORITY); - assertThat(rule.getDeviceEffects().getUserModifiedFields()) + assertThat(storedRule.zenDeviceEffectsUserModifiedFields) .isEqualTo(ZenDeviceEffects.FIELD_GRAYSCALE); - assertThat(rule.getDeviceEffects().shouldDisplayGrayscale()).isTrue(); } @Test @EnableFlags(Flags.FLAG_MODES_API) - public void automaticZenRuleToZenRule_doesNotUpdateValuesForInitUserOrigin() { - // Adds a starting rule with empty zen policies and device effects - AutomaticZenRule azrBase = new AutomaticZenRule.Builder(NAME, CONDITION_ID) - .setInterruptionFilter(INTERRUPTION_FILTER_ALL) // Already the default, no change - .setZenPolicy(new ZenPolicy.Builder() - .allowReminders(false) - .build()) - .setDeviceEffects(new ZenDeviceEffects.Builder() - .setShouldDisplayGrayscale(false) - .build()) - .build(); - // Adds the rule using the user, to set user-modified bits. - String ruleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), - azrBase, UPDATE_ORIGIN_USER, "reason", Process.SYSTEM_UID); - AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(ruleId); - assertThat(rule.canUpdate()).isFalse(); - assertThat(rule.getUserModifiedFields()).isEqualTo(AutomaticZenRule.FIELD_NAME); - - ZenPolicy policy = new ZenPolicy.Builder(rule.getZenPolicy()) - .allowReminders(true) - .build(); - ZenDeviceEffects deviceEffects = new ZenDeviceEffects.Builder(rule.getDeviceEffects()) - .setShouldDisplayGrayscale(true) - .build(); - AutomaticZenRule azrUpdate = new AutomaticZenRule.Builder(rule) - .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY) - .setZenPolicy(policy) - .setDeviceEffects(deviceEffects) - .build(); - - // Attempts to update the rule with the AZR from origin init user. - mZenModeHelper.updateAutomaticZenRule(ruleId, azrUpdate, UPDATE_ORIGIN_INIT_USER, "reason", - Process.SYSTEM_UID); - AutomaticZenRule unchangedRule = mZenModeHelper.getAutomaticZenRule(ruleId); - - // UPDATE_ORIGIN_INIT_USER does not change the bitmask or values if rule is user modified. - // TODO: b/318506692 - Remove once we check that INIT origins can't call add/updateAZR. - assertThat(unchangedRule.getUserModifiedFields()).isEqualTo(rule.getUserModifiedFields()); - assertThat(unchangedRule.getInterruptionFilter()).isEqualTo(INTERRUPTION_FILTER_ALL); - assertThat(unchangedRule.getZenPolicy().getUserModifiedFields()).isEqualTo( - rule.getZenPolicy().getUserModifiedFields()); - assertThat(unchangedRule.getZenPolicy().getPriorityCategoryReminders()).isEqualTo( - ZenPolicy.STATE_DISALLOW); - assertThat(unchangedRule.getDeviceEffects().getUserModifiedFields()).isEqualTo( - rule.getDeviceEffects().getUserModifiedFields()); - assertThat(unchangedRule.getDeviceEffects().shouldDisplayGrayscale()).isFalse(); - - // Creates a new rule with the AZR from origin init user. - String newRuleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), - azrUpdate, UPDATE_ORIGIN_INIT_USER, "reason", Process.SYSTEM_UID); - AutomaticZenRule newRule = mZenModeHelper.getAutomaticZenRule(newRuleId); - - // UPDATE_ORIGIN_INIT_USER does change the values if the rule is new, - // but does not update the bitmask. - assertThat(newRule.getUserModifiedFields()).isEqualTo(0); - assertThat(newRule.getInterruptionFilter()).isEqualTo(INTERRUPTION_FILTER_PRIORITY); - assertThat(newRule.getZenPolicy().getUserModifiedFields()).isEqualTo(0); - assertThat(newRule.getZenPolicy().getPriorityCategoryReminders()) - .isEqualTo(ZenPolicy.STATE_ALLOW); - assertThat(newRule.getDeviceEffects().getUserModifiedFields()).isEqualTo(0); - assertThat(newRule.getDeviceEffects().shouldDisplayGrayscale()).isTrue(); - } - - @Test - @EnableFlags(Flags.FLAG_MODES_API) - public void automaticZenRuleToZenRule_updatesValuesForSystemUiOrigin() { + public void updateAutomaticZenRule_fromSystemUi_updatesValues() { // Adds a starting rule with empty zen policies and device effects AutomaticZenRule azrBase = new AutomaticZenRule.Builder(NAME, CONDITION_ID) .setInterruptionFilter(INTERRUPTION_FILTER_ALL) @@ -3685,17 +3608,19 @@ public class ZenModeHelperTest extends UiServiceTestCase { rule = mZenModeHelper.getAutomaticZenRule(ruleId); // UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI should change the value but NOT update the bitmask. - assertThat(rule.getUserModifiedFields()).isEqualTo(0); - assertThat(rule.getZenPolicy().getUserModifiedFields()).isEqualTo(0); assertThat(rule.getZenPolicy().getPriorityCategoryReminders()) .isEqualTo(ZenPolicy.STATE_ALLOW); - assertThat(rule.getDeviceEffects().getUserModifiedFields()).isEqualTo(0); assertThat(rule.getDeviceEffects().shouldDisplayGrayscale()).isTrue(); + + ZenRule storedRule = mZenModeHelper.mConfig.automaticRules.get(ruleId); + assertThat(storedRule.userModifiedFields).isEqualTo(0); + assertThat(storedRule.zenPolicyUserModifiedFields).isEqualTo(0); + assertThat(storedRule.zenDeviceEffectsUserModifiedFields).isEqualTo(0); } @Test @EnableFlags(Flags.FLAG_MODES_API) - public void automaticZenRuleToZenRule_updatesValuesIfRuleNotUserModified() { + public void updateAutomaticZenRule_fromApp_updatesValuesIfRuleNotUserModified() { // Adds a starting rule with empty zen policies and device effects AutomaticZenRule azrBase = new AutomaticZenRule.Builder(NAME, CONDITION_ID) .setInterruptionFilter(INTERRUPTION_FILTER_ALL) @@ -3710,7 +3635,6 @@ public class ZenModeHelperTest extends UiServiceTestCase { String ruleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), azrBase, UPDATE_ORIGIN_APP, "reason", Process.SYSTEM_UID); AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(ruleId); - assertThat(rule.canUpdate()).isTrue(); ZenPolicy policy = new ZenPolicy.Builder() .allowReminders(true) @@ -3718,57 +3642,59 @@ public class ZenModeHelperTest extends UiServiceTestCase { ZenDeviceEffects deviceEffects = new ZenDeviceEffects.Builder() .setShouldDisplayGrayscale(true) .build(); - AutomaticZenRule azrUpdate = new AutomaticZenRule.Builder(rule) + AutomaticZenRule azrUpdate = new AutomaticZenRule.Builder(rule) .setInterruptionFilter(INTERRUPTION_FILTER_ALARMS) .setZenPolicy(policy) .setDeviceEffects(deviceEffects) .build(); - // Since the rule is not already user modified, UPDATE_ORIGIN_UNKNOWN can modify the rule. + // Since the rule is not already user modified, UPDATE_ORIGIN_APP can modify the rule. // The bitmask is not modified. - mZenModeHelper.updateAutomaticZenRule(ruleId, azrUpdate, UPDATE_ORIGIN_UNKNOWN, "reason", + mZenModeHelper.updateAutomaticZenRule(ruleId, azrUpdate, UPDATE_ORIGIN_APP, "reason", Process.SYSTEM_UID); - AutomaticZenRule unchangedRule = mZenModeHelper.getAutomaticZenRule(ruleId); - assertThat(unchangedRule.getUserModifiedFields()).isEqualTo(rule.getUserModifiedFields()); - assertThat(unchangedRule.getInterruptionFilter()).isEqualTo(INTERRUPTION_FILTER_ALARMS); - assertThat(unchangedRule.getZenPolicy().getUserModifiedFields()).isEqualTo( - rule.getZenPolicy().getUserModifiedFields()); - assertThat(unchangedRule.getZenPolicy().getPriorityCategoryReminders()) + ZenRule storedRule = mZenModeHelper.mConfig.automaticRules.get(ruleId); + assertThat(storedRule.userModifiedFields).isEqualTo(0); + + assertThat(storedRule.zenMode).isEqualTo(ZEN_MODE_ALARMS); + assertThat(storedRule.zenPolicy.getPriorityCategoryReminders()) .isEqualTo(ZenPolicy.STATE_ALLOW); - assertThat(unchangedRule.getDeviceEffects().getUserModifiedFields()).isEqualTo( - rule.getDeviceEffects().getUserModifiedFields()); - assertThat(unchangedRule.getDeviceEffects().shouldDisplayGrayscale()).isTrue(); + assertThat(storedRule.zenDeviceEffects.shouldDisplayGrayscale()).isTrue(); + assertThat(storedRule.userModifiedFields).isEqualTo(0); + assertThat(storedRule.zenPolicyUserModifiedFields).isEqualTo(0); + assertThat(storedRule.zenDeviceEffectsUserModifiedFields).isEqualTo(0); // Creates another rule, this time from user. This will have user modified bits set. String ruleIdUser = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), azrBase, UPDATE_ORIGIN_USER, "reason", Process.SYSTEM_UID); - AutomaticZenRule ruleUser = mZenModeHelper.getAutomaticZenRule(ruleIdUser); - assertThat(ruleUser.canUpdate()).isFalse(); + storedRule = mZenModeHelper.mConfig.automaticRules.get(ruleIdUser); + int ruleModifiedFields = storedRule.userModifiedFields; + int rulePolicyModifiedFields = storedRule.zenPolicyUserModifiedFields; + int ruleDeviceEffectsModifiedFields = storedRule.zenDeviceEffectsUserModifiedFields; - // Zen rule update coming from unknown origin. This cannot fully update the rule, because + // Zen rule update coming from the app again. This cannot fully update the rule, because // the rule is already considered user modified. - mZenModeHelper.updateAutomaticZenRule(ruleIdUser, azrUpdate, UPDATE_ORIGIN_UNKNOWN, + mZenModeHelper.updateAutomaticZenRule(ruleIdUser, azrUpdate, UPDATE_ORIGIN_APP, "reason", Process.SYSTEM_UID); - ruleUser = mZenModeHelper.getAutomaticZenRule(ruleIdUser); + AutomaticZenRule ruleUser = mZenModeHelper.getAutomaticZenRule(ruleIdUser); - // UPDATE_ORIGIN_UNKNOWN can only change the value if the rule is not already user modified, + // The app can only change the value if the rule is not already user modified, // so the rule is not changed, and neither is the bitmask. assertThat(ruleUser.getInterruptionFilter()).isEqualTo(INTERRUPTION_FILTER_ALL); - // Interruption Filter All is the default value, so it's not included as a modified field. - assertThat(ruleUser.getUserModifiedFields() | AutomaticZenRule.FIELD_NAME).isGreaterThan(0); - assertThat(ruleUser.getZenPolicy().getUserModifiedFields() - | ZenPolicy.FIELD_PRIORITY_CATEGORY_REMINDERS).isGreaterThan(0); assertThat(ruleUser.getZenPolicy().getPriorityCategoryReminders()) .isEqualTo(ZenPolicy.STATE_DISALLOW); - assertThat(ruleUser.getDeviceEffects().getUserModifiedFields() - | ZenDeviceEffects.FIELD_GRAYSCALE).isGreaterThan(0); assertThat(ruleUser.getDeviceEffects().shouldDisplayGrayscale()).isFalse(); + + storedRule = mZenModeHelper.mConfig.automaticRules.get(ruleIdUser); + assertThat(storedRule.userModifiedFields).isEqualTo(ruleModifiedFields); + assertThat(storedRule.zenPolicyUserModifiedFields).isEqualTo(rulePolicyModifiedFields); + assertThat(storedRule.zenDeviceEffectsUserModifiedFields).isEqualTo( + ruleDeviceEffectsModifiedFields); } @Test @EnableFlags(Flags.FLAG_MODES_API) - public void automaticZenRuleToZenRule_updatesValuesIfRuleNew() { + public void addAutomaticZenRule_updatesValues() { // Adds a starting rule with empty zen policies and device effects AutomaticZenRule azrBase = new AutomaticZenRule.Builder(NAME, CONDITION_ID) .setInterruptionFilter(INTERRUPTION_FILTER_ALARMS) @@ -3779,21 +3705,22 @@ public class ZenModeHelperTest extends UiServiceTestCase { .setShouldDisplayGrayscale(true) .build()) .build(); - // Adds the rule using origin unknown, to show that a new rule is always allowed. String ruleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), - azrBase, UPDATE_ORIGIN_UNKNOWN, "reason", Process.SYSTEM_UID); + azrBase, UPDATE_ORIGIN_APP, "reason", Process.SYSTEM_UID); AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(ruleId); // The values are modified but the bitmask is not. - assertThat(rule.canUpdate()).isTrue(); assertThat(rule.getZenPolicy().getPriorityCategoryReminders()) .isEqualTo(ZenPolicy.STATE_ALLOW); assertThat(rule.getDeviceEffects().shouldDisplayGrayscale()).isTrue(); + + ZenRule storedRule = mZenModeHelper.mConfig.automaticRules.get(ruleId); + assertThat(storedRule.canBeUpdatedByApp()).isTrue(); } @Test @EnableFlags(Flags.FLAG_MODES_API) - public void automaticZenRuleToZenRule_nullDeviceEffectsUpdate() { + public void updateAutomaticZenRule_nullDeviceEffectsUpdate() { // Adds a starting rule with empty zen policies and device effects AutomaticZenRule azrBase = new AutomaticZenRule.Builder(NAME, CONDITION_ID) .setDeviceEffects(new ZenDeviceEffects.Builder().build()) @@ -3808,9 +3735,9 @@ public class ZenModeHelperTest extends UiServiceTestCase { .setDeviceEffects(null) .build(); - // Zen rule update coming from unknown origin, but since the rule isn't already + // Zen rule update coming from app, but since the rule isn't already // user modified, it can be updated. - mZenModeHelper.updateAutomaticZenRule(ruleId, azr, UPDATE_ORIGIN_UNKNOWN, "reason", + mZenModeHelper.updateAutomaticZenRule(ruleId, azr, UPDATE_ORIGIN_APP, "reason", Process.SYSTEM_UID); rule = mZenModeHelper.getAutomaticZenRule(ruleId); @@ -3820,7 +3747,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { @Test @EnableFlags(Flags.FLAG_MODES_API) - public void automaticZenRuleToZenRule_nullPolicyUpdate() { + public void updateAutomaticZenRule_nullPolicyUpdate() { // Adds a starting rule with empty zen policies and device effects AutomaticZenRule azrBase = new AutomaticZenRule.Builder(NAME, CONDITION_ID) .setZenPolicy(new ZenPolicy.Builder().build()) @@ -3829,16 +3756,15 @@ public class ZenModeHelperTest extends UiServiceTestCase { String ruleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), azrBase, UPDATE_ORIGIN_APP, "reason", Process.SYSTEM_UID); AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(ruleId); - assertThat(rule.canUpdate()).isTrue(); AutomaticZenRule azr = new AutomaticZenRule.Builder(azrBase) // Set zen policy to null .setZenPolicy(null) .build(); - // Zen rule update coming from unknown origin, but since the rule isn't already + // Zen rule update coming from app, but since the rule isn't already // user modified, it can be updated. - mZenModeHelper.updateAutomaticZenRule(ruleId, azr, UPDATE_ORIGIN_UNKNOWN, "reason", + mZenModeHelper.updateAutomaticZenRule(ruleId, azr, UPDATE_ORIGIN_APP, "reason", Process.SYSTEM_UID); rule = mZenModeHelper.getAutomaticZenRule(ruleId); @@ -3860,11 +3786,10 @@ public class ZenModeHelperTest extends UiServiceTestCase { String ruleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), azrBase, UPDATE_ORIGIN_APP, "reason", Process.SYSTEM_UID); AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(ruleId); - assertThat(rule.canUpdate()).isTrue(); // Create a fully populated ZenPolicy. ZenPolicy policy = new ZenPolicy.Builder() - .allowChannels(ZenPolicy.CHANNEL_TYPE_NONE) // Differs from the default + .allowPriorityChannels(false) // Differs from the default .allowReminders(true) // Differs from the default .allowEvents(true) // Differs from the default .allowConversations(ZenPolicy.CONVERSATION_SENDERS_IMPORTANT) @@ -3894,9 +3819,11 @@ public class ZenModeHelperTest extends UiServiceTestCase { // New ZenPolicy differs from the default config assertThat(rule.getZenPolicy()).isNotNull(); - assertThat(rule.getZenPolicy().getAllowedChannels()).isEqualTo(ZenPolicy.CHANNEL_TYPE_NONE); - assertThat(rule.canUpdate()).isFalse(); - assertThat(rule.getZenPolicy().getUserModifiedFields()).isEqualTo( + assertThat(rule.getZenPolicy().getPriorityChannels()).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 @@ -3919,7 +3846,6 @@ public class ZenModeHelperTest extends UiServiceTestCase { String ruleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), azrBase, UPDATE_ORIGIN_APP, "reason", Process.SYSTEM_UID); AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(ruleId); - assertThat(rule.canUpdate()).isTrue(); ZenDeviceEffects deviceEffects = new ZenDeviceEffects.Builder() .setShouldDisplayGrayscale(true) @@ -3936,8 +3862,10 @@ public class ZenModeHelperTest extends UiServiceTestCase { // New ZenDeviceEffects is used; all fields considered set, since previously were null. assertThat(rule.getDeviceEffects()).isNotNull(); assertThat(rule.getDeviceEffects().shouldDisplayGrayscale()).isTrue(); - assertThat(rule.canUpdate()).isFalse(); - assertThat(rule.getDeviceEffects().getUserModifiedFields()).isEqualTo( + + ZenRule storedRule = mZenModeHelper.mConfig.automaticRules.get(ruleId); + assertThat(storedRule.canBeUpdatedByApp()).isFalse(); + assertThat(storedRule.zenDeviceEffectsUserModifiedFields).isEqualTo( ZenDeviceEffects.FIELD_GRAYSCALE); } @@ -4340,7 +4268,6 @@ public class ZenModeHelperTest extends UiServiceTestCase { String ruleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), rule, UPDATE_ORIGIN_APP, "add it", CUSTOM_PKG_UID); assertThat(mZenModeHelper.getAutomaticZenRule(ruleId).getCreationTime()).isEqualTo(1000); - assertThat(mZenModeHelper.getAutomaticZenRule(ruleId).canUpdate()).isTrue(); // User customizes it. AutomaticZenRule userUpdate = new AutomaticZenRule.Builder(rule) @@ -4372,9 +4299,11 @@ public class ZenModeHelperTest extends UiServiceTestCase { assertThat(finalRule.getInterruptionFilter()).isEqualTo(INTERRUPTION_FILTER_ALARMS); assertThat(finalRule.getZenPolicy().getPriorityCategoryRepeatCallers()).isEqualTo( ZenPolicy.STATE_ALLOW); - assertThat(finalRule.getUserModifiedFields()).isEqualTo( + + ZenRule storedRule = mZenModeHelper.mConfig.automaticRules.get(ruleId); + assertThat(storedRule.userModifiedFields).isEqualTo( AutomaticZenRule.FIELD_INTERRUPTION_FILTER); - assertThat(finalRule.getZenPolicy().getUserModifiedFields()).isEqualTo( + assertThat(storedRule.zenPolicyUserModifiedFields).isEqualTo( ZenPolicy.FIELD_PRIORITY_CATEGORY_REPEAT_CALLERS); // Also, we discarded the "deleted rule" since we already used it for restoration. @@ -4653,7 +4582,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { ZEN_MODE_IMPORTANT_INTERRUPTIONS); assertThat(mZenModeHelper.mConfig.automaticRules.values()) - .comparingElementsUsing(IGNORE_TIMESTAMPS) + .comparingElementsUsing(IGNORE_METADATA) .containsExactly( expectedImplicitRule(CUSTOM_PKG_NAME, ZEN_MODE_IMPORTANT_INTERRUPTIONS, null, true)); @@ -4673,12 +4602,75 @@ public class ZenModeHelperTest extends UiServiceTestCase { ZEN_MODE_ALARMS); assertThat(mZenModeHelper.mConfig.automaticRules.values()) - .comparingElementsUsing(IGNORE_TIMESTAMPS) + .comparingElementsUsing(IGNORE_METADATA) .containsExactly( expectedImplicitRule(CUSTOM_PKG_NAME, ZEN_MODE_ALARMS, null, true)); } @Test + @EnableFlags(android.app.Flags.FLAG_MODES_API) + public void applyGlobalZenModeAsImplicitZenRule_ruleCustomized_doesNotUpdateRule() { + mZenModeHelper.mConfig.automaticRules.clear(); + String pkg = mContext.getPackageName(); + + // From app, call "setInterruptionFilter" and create and implicit rule. + mZenModeHelper.applyGlobalZenModeAsImplicitZenRule(pkg, CUSTOM_PKG_UID, + ZEN_MODE_IMPORTANT_INTERRUPTIONS); + String ruleId = getOnlyElement(mZenModeHelper.mConfig.automaticRules.keySet()); + assertThat(getOnlyElement(mZenModeHelper.mConfig.automaticRules.values()).zenMode) + .isEqualTo(ZEN_MODE_IMPORTANT_INTERRUPTIONS); + + // From user, update that rule's interruption filter. + AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(ruleId); + AutomaticZenRule userUpdateRule = new AutomaticZenRule.Builder(rule) + .setInterruptionFilter(INTERRUPTION_FILTER_ALARMS) + .build(); + mZenModeHelper.updateAutomaticZenRule(ruleId, userUpdateRule, UPDATE_ORIGIN_USER, "reason", + Process.SYSTEM_UID); + + // From app, call "setInterruptionFilter" again. + mZenModeHelper.applyGlobalZenModeAsImplicitZenRule(pkg, CUSTOM_PKG_UID, + ZEN_MODE_NO_INTERRUPTIONS); + + // The app's update was ignored, and the user's update is still current, and the current + // mode is the one they chose. + assertThat(getOnlyElement(mZenModeHelper.mConfig.automaticRules.values()).zenMode) + .isEqualTo(ZEN_MODE_ALARMS); + assertThat(mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_ALARMS); + } + + @Test + @EnableFlags(android.app.Flags.FLAG_MODES_API) + public void applyGlobalZenModeAsImplicitZenRule_ruleCustomizedButNotFilter_updatesRule() { + mZenModeHelper.mConfig.automaticRules.clear(); + String pkg = mContext.getPackageName(); + + // From app, call "setInterruptionFilter" and create and implicit rule. + mZenModeHelper.applyGlobalZenModeAsImplicitZenRule(pkg, CUSTOM_PKG_UID, + ZEN_MODE_IMPORTANT_INTERRUPTIONS); + String ruleId = getOnlyElement(mZenModeHelper.mConfig.automaticRules.keySet()); + assertThat(getOnlyElement(mZenModeHelper.mConfig.automaticRules.values()).zenMode) + .isEqualTo(ZEN_MODE_IMPORTANT_INTERRUPTIONS); + + // From user, update something in that rule, but not the interruption filter. + AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(ruleId); + AutomaticZenRule userUpdateRule = new AutomaticZenRule.Builder(rule) + .setName("Renamed") + .build(); + mZenModeHelper.updateAutomaticZenRule(ruleId, userUpdateRule, UPDATE_ORIGIN_USER, "reason", + Process.SYSTEM_UID); + + // From app, call "setInterruptionFilter" again. + mZenModeHelper.applyGlobalZenModeAsImplicitZenRule(pkg, CUSTOM_PKG_UID, + ZEN_MODE_NO_INTERRUPTIONS); + + // The app's update was accepted, and the current mode is the one that they wanted. + assertThat(getOnlyElement(mZenModeHelper.mConfig.automaticRules.values()).zenMode) + .isEqualTo(ZEN_MODE_NO_INTERRUPTIONS); + assertThat(mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_NO_INTERRUPTIONS); + } + + @Test public void applyGlobalZenModeAsImplicitZenRule_modeOff_deactivatesImplicitRule() { mSetFlagsRule.enableFlags(android.app.Flags.FLAG_MODES_API); mZenModeHelper.mConfig.automaticRules.clear(); @@ -4748,18 +4740,17 @@ public class ZenModeHelperTest extends UiServiceTestCase { Policy policy = new Policy(PRIORITY_CATEGORY_CALLS | PRIORITY_CATEGORY_CONVERSATIONS, PRIORITY_SENDERS_CONTACTS, PRIORITY_SENDERS_STARRED, Policy.getAllSuppressedVisualEffects(), CONVERSATION_SENDERS_IMPORTANT); - mZenModeHelper.applyGlobalPolicyAsImplicitZenRule(CUSTOM_PKG_NAME, CUSTOM_PKG_UID, policy, - UPDATE_ORIGIN_APP); + mZenModeHelper.applyGlobalPolicyAsImplicitZenRule(CUSTOM_PKG_NAME, CUSTOM_PKG_UID, policy); ZenPolicy expectedZenPolicy = new ZenPolicy.Builder() .disallowAllSounds() .allowCalls(PEOPLE_TYPE_CONTACTS) .allowConversations(CONVERSATION_SENDERS_IMPORTANT) .hideAllVisualEffects() - .allowChannels(ZenPolicy.CHANNEL_TYPE_PRIORITY) + .allowPriorityChannels(true) .build(); assertThat(mZenModeHelper.mConfig.automaticRules.values()) - .comparingElementsUsing(IGNORE_TIMESTAMPS) + .comparingElementsUsing(IGNORE_METADATA) .containsExactly( expectedImplicitRule(CUSTOM_PKG_NAME, ZEN_MODE_IMPORTANT_INTERRUPTIONS, expectedZenPolicy, /* conditionActive= */ null)); @@ -4774,37 +4765,103 @@ public class ZenModeHelperTest extends UiServiceTestCase { PRIORITY_SENDERS_CONTACTS, PRIORITY_SENDERS_STARRED, Policy.getAllSuppressedVisualEffects(), CONVERSATION_SENDERS_IMPORTANT); mZenModeHelper.applyGlobalPolicyAsImplicitZenRule(CUSTOM_PKG_NAME, CUSTOM_PKG_UID, - original, UPDATE_ORIGIN_APP); + original); // Change priorityCallSenders: contacts -> starred. Policy updated = new Policy(PRIORITY_CATEGORY_CALLS | PRIORITY_CATEGORY_CONVERSATIONS, PRIORITY_SENDERS_STARRED, PRIORITY_SENDERS_STARRED, Policy.getAllSuppressedVisualEffects(), CONVERSATION_SENDERS_IMPORTANT); - mZenModeHelper.applyGlobalPolicyAsImplicitZenRule(CUSTOM_PKG_NAME, CUSTOM_PKG_UID, updated, - UPDATE_ORIGIN_APP); + mZenModeHelper.applyGlobalPolicyAsImplicitZenRule(CUSTOM_PKG_NAME, CUSTOM_PKG_UID, updated); ZenPolicy expectedZenPolicy = new ZenPolicy.Builder() .disallowAllSounds() .allowCalls(PEOPLE_TYPE_STARRED) .allowConversations(CONVERSATION_SENDERS_IMPORTANT) .hideAllVisualEffects() - .allowChannels(ZenPolicy.CHANNEL_TYPE_PRIORITY) + .allowPriorityChannels(true) .build(); assertThat(mZenModeHelper.mConfig.automaticRules.values()) - .comparingElementsUsing(IGNORE_TIMESTAMPS) + .comparingElementsUsing(IGNORE_METADATA) .containsExactly( expectedImplicitRule(CUSTOM_PKG_NAME, ZEN_MODE_IMPORTANT_INTERRUPTIONS, expectedZenPolicy, /* conditionActive= */ null)); } @Test + @EnableFlags(android.app.Flags.FLAG_MODES_API) + public void applyGlobalPolicyAsImplicitZenRule_ruleCustomized_doesNotUpdateRule() { + mZenModeHelper.mConfig.automaticRules.clear(); + String pkg = mContext.getPackageName(); + + // From app, call "setNotificationPolicy" and create and implicit rule. + Policy originalPolicy = new Policy(PRIORITY_CATEGORY_MEDIA, 0, 0); + mZenModeHelper.applyGlobalPolicyAsImplicitZenRule(pkg, CUSTOM_PKG_UID, originalPolicy); + String ruleId = getOnlyElement(mZenModeHelper.mConfig.automaticRules.keySet()); + + // From user, update that rule's policy. + AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(ruleId); + ZenPolicy userUpdateZenPolicy = new ZenPolicy.Builder().disallowAllSounds() + .allowAlarms(true).build(); + AutomaticZenRule userUpdateRule = new AutomaticZenRule.Builder(rule) + .setZenPolicy(userUpdateZenPolicy) + .build(); + mZenModeHelper.updateAutomaticZenRule(ruleId, userUpdateRule, UPDATE_ORIGIN_USER, "reason", + Process.SYSTEM_UID); + + // From app, call "setNotificationPolicy" again. + Policy appUpdatePolicy = new Policy(PRIORITY_CATEGORY_SYSTEM, 0, 0); + mZenModeHelper.applyGlobalPolicyAsImplicitZenRule(pkg, CUSTOM_PKG_UID, appUpdatePolicy); + + // The app's update was ignored, and the user's update is still current. + assertThat(mZenModeHelper.mConfig.automaticRules.values()) + .comparingElementsUsing(IGNORE_METADATA) + .containsExactly( + expectedImplicitRule(pkg, ZEN_MODE_IMPORTANT_INTERRUPTIONS, + userUpdateZenPolicy, + /* conditionActive= */ null)); + } + + @Test + @EnableFlags(android.app.Flags.FLAG_MODES_API) + public void applyGlobalPolicyAsImplicitZenRule_ruleCustomizedButNotZenPolicy_updatesRule() { + mZenModeHelper.mConfig.automaticRules.clear(); + String pkg = mContext.getPackageName(); + + // From app, call "setNotificationPolicy" and create and implicit rule. + Policy originalPolicy = new Policy(PRIORITY_CATEGORY_MEDIA, 0, 0); + mZenModeHelper.applyGlobalPolicyAsImplicitZenRule(pkg, CUSTOM_PKG_UID, originalPolicy); + String ruleId = getOnlyElement(mZenModeHelper.mConfig.automaticRules.keySet()); + + // From user, update something in that rule, but not the ZenPolicy. + AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(ruleId); + AutomaticZenRule userUpdateRule = new AutomaticZenRule.Builder(rule) + .setName("Rule renamed, not touching policy") + .build(); + mZenModeHelper.updateAutomaticZenRule(ruleId, userUpdateRule, UPDATE_ORIGIN_USER, "reason", + Process.SYSTEM_UID); + + // From app, call "setNotificationPolicy" again. + Policy appUpdatePolicy = new Policy(PRIORITY_CATEGORY_SYSTEM, 0, 0); + mZenModeHelper.applyGlobalPolicyAsImplicitZenRule(pkg, CUSTOM_PKG_UID, appUpdatePolicy); + + // The app's update was applied. + ZenPolicy appsSecondZenPolicy = new ZenPolicy.Builder() + .disallowAllSounds() + .allowSystem(true) + .allowPriorityChannels(true) + .build(); + assertThat(getOnlyElement(mZenModeHelper.mConfig.automaticRules.values()).zenPolicy) + .isEqualTo(appsSecondZenPolicy); + } + + @Test public void applyGlobalPolicyAsImplicitZenRule_flagOff_ignored() { mSetFlagsRule.disableFlags(android.app.Flags.FLAG_MODES_API); mZenModeHelper.mConfig.automaticRules.clear(); withoutWtfCrash( () -> mZenModeHelper.applyGlobalPolicyAsImplicitZenRule(CUSTOM_PKG_NAME, - CUSTOM_PKG_UID, new Policy(0, 0, 0), UPDATE_ORIGIN_APP)); + CUSTOM_PKG_UID, new Policy(0, 0, 0))); assertThat(mZenModeHelper.mConfig.automaticRules).isEmpty(); } @@ -4817,7 +4874,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { Policy.getAllSuppressedVisualEffects(), STATE_FALSE, CONVERSATION_SENDERS_IMPORTANT); mZenModeHelper.applyGlobalPolicyAsImplicitZenRule(CUSTOM_PKG_NAME, CUSTOM_PKG_UID, - writtenPolicy, UPDATE_ORIGIN_APP); + writtenPolicy); Policy readPolicy = mZenModeHelper.getNotificationPolicyFromImplicitZenRule( CUSTOM_PKG_NAME); @@ -4857,7 +4914,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { assertThat(readPolicy.allowConversations()).isFalse(); } - private static final Correspondence<ZenRule, ZenRule> IGNORE_TIMESTAMPS = + private static final Correspondence<ZenRule, ZenRule> IGNORE_METADATA = Correspondence.transforming(zr -> { Parcel p = Parcel.obtain(); try { @@ -4865,12 +4922,15 @@ public class ZenModeHelperTest extends UiServiceTestCase { p.setDataPosition(0); ZenRule copy = new ZenRule(p); copy.creationTime = 0; + copy.userModifiedFields = 0; + copy.zenPolicyUserModifiedFields = 0; + copy.zenDeviceEffectsUserModifiedFields = 0; return copy; } finally { p.recycle(); } }, - "Ignoring timestamps"); + "Ignoring timestamp and userModifiedFields"); private ZenRule expectedImplicitRule(String ownerPkg, int zenMode, ZenPolicy policy, @Nullable Boolean conditionActive) { 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 21c96d6adc7e..4ed55df7775c 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/ZenPolicyTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenPolicyTest.java @@ -213,13 +213,12 @@ public class ZenPolicyTest extends UiServiceTestCase { ZenPolicy unset = builder.build(); // priority channels allowed - builder.allowChannels(ZenPolicy.CHANNEL_TYPE_PRIORITY); + builder.allowPriorityChannels(true); ZenPolicy channelsPriority = builder.build(); // unset applied, channels setting keeps its state channelsPriority.apply(unset); - assertThat(channelsPriority.getAllowedChannels()) - .isEqualTo(ZenPolicy.CHANNEL_TYPE_PRIORITY); + assertThat(channelsPriority.getPriorityChannels()).isEqualTo(ZenPolicy.STATE_ALLOW); } @Test @@ -227,15 +226,15 @@ public class ZenPolicyTest extends UiServiceTestCase { mSetFlagsRule.enableFlags(Flags.FLAG_MODES_API); ZenPolicy.Builder builder = new ZenPolicy.Builder(); - builder.allowChannels(ZenPolicy.CHANNEL_TYPE_NONE); + builder.allowPriorityChannels(false); ZenPolicy none = builder.build(); - builder.allowChannels(ZenPolicy.CHANNEL_TYPE_PRIORITY); + builder.allowPriorityChannels(true); ZenPolicy priority = builder.build(); // priority channels (less strict state) cannot override a setting that sets it to none none.apply(priority); - assertThat(none.getAllowedChannels()).isEqualTo(ZenPolicy.CHANNEL_TYPE_NONE); + assertThat(none.getPriorityChannels()).isEqualTo(ZenPolicy.STATE_DISALLOW); } @Test @@ -243,15 +242,15 @@ public class ZenPolicyTest extends UiServiceTestCase { mSetFlagsRule.enableFlags(Flags.FLAG_MODES_API); ZenPolicy.Builder builder = new ZenPolicy.Builder(); - builder.allowChannels(ZenPolicy.CHANNEL_TYPE_NONE); + builder.allowPriorityChannels(false); ZenPolicy none = builder.build(); - builder.allowChannels(ZenPolicy.CHANNEL_TYPE_PRIORITY); + builder.allowPriorityChannels(true); ZenPolicy priority = builder.build(); // applying a policy with channelType=none overrides priority setting priority.apply(none); - assertThat(priority.getAllowedChannels()).isEqualTo(ZenPolicy.CHANNEL_TYPE_NONE); + assertThat(priority.getPriorityChannels()).isEqualTo(ZenPolicy.STATE_DISALLOW); } @Test @@ -261,12 +260,12 @@ public class ZenPolicyTest extends UiServiceTestCase { ZenPolicy.Builder builder = new ZenPolicy.Builder(); ZenPolicy unset = builder.build(); - builder.allowChannels(ZenPolicy.CHANNEL_TYPE_PRIORITY); + builder.allowPriorityChannels(true); ZenPolicy priority = builder.build(); // applying a policy with a set channel type actually goes through unset.apply(priority); - assertThat(unset.getAllowedChannels()).isEqualTo(ZenPolicy.CHANNEL_TYPE_PRIORITY); + assertThat(unset.getPriorityChannels()).isEqualTo(ZenPolicy.STATE_ALLOW); } @Test @@ -308,7 +307,7 @@ public class ZenPolicyTest extends UiServiceTestCase { ZenPolicy.Builder builder = new ZenPolicy.Builder(); ZenPolicy policy = builder.build(); - assertThat(policy.getAllowedChannels()).isEqualTo(ZenPolicy.CHANNEL_TYPE_UNSET); + assertThat(policy.getPriorityChannels()).isEqualTo(ZenPolicy.STATE_UNSET); } @Test @@ -622,10 +621,10 @@ public class ZenPolicyTest extends UiServiceTestCase { // allowChannels should be unset, not be modifiable, and not show up in any output ZenPolicy.Builder builder = new ZenPolicy.Builder(); - builder.allowChannels(ZenPolicy.CHANNEL_TYPE_PRIORITY); + builder.allowPriorityChannels(true); ZenPolicy policy = builder.build(); - assertThat(policy.getAllowedChannels()).isEqualTo(ZenPolicy.CHANNEL_TYPE_UNSET); + assertThat(policy.getPriorityChannels()).isEqualTo(ZenPolicy.STATE_UNSET); assertThat(policy.toString().contains("allowChannels")).isFalse(); } @@ -635,40 +634,14 @@ public class ZenPolicyTest extends UiServiceTestCase { // allow priority channels ZenPolicy.Builder builder = new ZenPolicy.Builder(); - builder.allowChannels(ZenPolicy.CHANNEL_TYPE_PRIORITY); + builder.allowPriorityChannels(true); ZenPolicy policy = builder.build(); - assertThat(policy.getAllowedChannels()).isEqualTo(ZenPolicy.CHANNEL_TYPE_PRIORITY); + assertThat(policy.getPriorityChannels()).isEqualTo(ZenPolicy.STATE_ALLOW); // disallow priority channels - builder.allowChannels(ZenPolicy.CHANNEL_TYPE_NONE); + builder.allowPriorityChannels(false); policy = builder.build(); - assertThat(policy.getAllowedChannels()).isEqualTo(ZenPolicy.CHANNEL_TYPE_NONE); - } - - @Test - public void testFromParcel() { - ZenPolicy.Builder builder = new ZenPolicy.Builder(); - builder.setUserModifiedFields(10); - - ZenPolicy policy = builder.build(); - assertThat(policy.getUserModifiedFields()).isEqualTo(10); - - Parcel parcel = Parcel.obtain(); - policy.writeToParcel(parcel, 0); - parcel.setDataPosition(0); - - ZenPolicy fromParcel = ZenPolicy.CREATOR.createFromParcel(parcel); - assertThat(fromParcel.getUserModifiedFields()).isEqualTo(10); - } - - @Test - public void testPolicy_userModifiedFields() { - ZenPolicy.Builder builder = new ZenPolicy.Builder(); - builder.setUserModifiedFields(10); - assertThat(builder.build().getUserModifiedFields()).isEqualTo(10); - - builder.setUserModifiedFields(0); - assertThat(builder.build().getUserModifiedFields()).isEqualTo(0); + assertThat(policy.getPriorityChannels()).isEqualTo(ZenPolicy.STATE_DISALLOW); } @Test @@ -676,8 +649,8 @@ public class ZenPolicyTest extends UiServiceTestCase { ZenPolicy.Builder builder = new ZenPolicy.Builder(); ZenPolicy policy = builder.allowRepeatCallers(true).allowAlarms(false) .showLights(true).showBadges(false) - .allowChannels(ZenPolicy.CHANNEL_TYPE_PRIORITY) - .setUserModifiedFields(20).build(); + .allowPriorityChannels(true) + .build(); ZenPolicy newPolicy = new ZenPolicy.Builder(policy).build(); @@ -689,8 +662,7 @@ public class ZenPolicyTest extends UiServiceTestCase { assertThat(newPolicy.getVisualEffectBadge()).isEqualTo(ZenPolicy.STATE_DISALLOW); assertThat(newPolicy.getVisualEffectPeek()).isEqualTo(ZenPolicy.STATE_UNSET); - assertThat(newPolicy.getAllowedChannels()).isEqualTo(ZenPolicy.CHANNEL_TYPE_PRIORITY); - assertThat(newPolicy.getUserModifiedFields()).isEqualTo(20); + assertThat(newPolicy.getPriorityChannels()).isEqualTo(ZenPolicy.STATE_ALLOW); } @Test 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 9bb2da0ff70c..ba7b52e368f3 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java @@ -3365,7 +3365,7 @@ public class ActivityRecordTests extends WindowTestsBase { assertFalse(app2.mActivityRecord.mImeInsetsFrozenUntilStartInput); if (Flags.bundleClientTransactionFlag()) { - verify(app2.getProcess()).scheduleClientTransactionItem( + verify(app2.getProcess(), atLeastOnce()).scheduleClientTransactionItem( isA(WindowStateResizeItem.class)); } else { verify(app2.mClient, atLeastOnce()).resized(any(), anyBoolean(), any(), diff --git a/services/tests/wmtests/src/com/android/server/wm/SensitiveContentPackagesTest.java b/services/tests/wmtests/src/com/android/server/wm/SensitiveContentPackagesTest.java index 71dbc57e5065..298637266cc3 100644 --- a/services/tests/wmtests/src/com/android/server/wm/SensitiveContentPackagesTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/SensitiveContentPackagesTest.java @@ -20,6 +20,7 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import android.platform.test.annotations.Presubmit; +import android.util.ArraySet; import androidx.test.filters.SmallTest; @@ -28,7 +29,6 @@ import com.android.server.wm.SensitiveContentPackages.PackageInfo; import org.junit.After; import org.junit.Test; -import java.util.Collections; import java.util.Set; /** @@ -52,18 +52,21 @@ public class SensitiveContentPackagesTest { @After public void tearDown() { - mSensitiveContentPackages.setShouldBlockScreenCaptureForApp(Collections.emptySet()); + mSensitiveContentPackages.clearBlockedApps(); } @Test - public void setShouldBlockScreenCaptureForApp() { - Set<PackageInfo> blockedApps = + public void addBlockScreenCaptureForApps() { + ArraySet<PackageInfo> blockedApps = new ArraySet( Set.of(new PackageInfo(APP_PKG_1, APP_UID_1), new PackageInfo(APP_PKG_1, APP_UID_2), new PackageInfo(APP_PKG_2, APP_UID_1), - new PackageInfo(APP_PKG_2, APP_UID_2)); + new PackageInfo(APP_PKG_2, APP_UID_2) + )); - mSensitiveContentPackages.setShouldBlockScreenCaptureForApp(blockedApps); + boolean modified = mSensitiveContentPackages.addBlockScreenCaptureForApps(blockedApps); + + assertTrue(modified); assertTrue(mSensitiveContentPackages.shouldBlockScreenCaptureForApp(APP_PKG_1, APP_UID_1)); assertTrue(mSensitiveContentPackages.shouldBlockScreenCaptureForApp(APP_PKG_1, APP_UID_2)); @@ -79,15 +82,93 @@ public class SensitiveContentPackagesTest { } @Test - public void setShouldBlockScreenCaptureForApp_empty() { - Set<PackageInfo> blockedApps = + public void addBlockScreenCaptureForApps_addedTwice() { + ArraySet<PackageInfo> blockedApps = new ArraySet( Set.of(new PackageInfo(APP_PKG_1, APP_UID_1), new PackageInfo(APP_PKG_1, APP_UID_2), new PackageInfo(APP_PKG_2, APP_UID_1), - new PackageInfo(APP_PKG_2, APP_UID_2)); + new PackageInfo(APP_PKG_2, APP_UID_2) + )); + + mSensitiveContentPackages.addBlockScreenCaptureForApps(blockedApps); + boolean modified = mSensitiveContentPackages.addBlockScreenCaptureForApps(blockedApps); + + assertFalse(modified); + + assertTrue(mSensitiveContentPackages.shouldBlockScreenCaptureForApp(APP_PKG_1, APP_UID_1)); + assertTrue(mSensitiveContentPackages.shouldBlockScreenCaptureForApp(APP_PKG_1, APP_UID_2)); + assertFalse(mSensitiveContentPackages.shouldBlockScreenCaptureForApp(APP_PKG_1, APP_UID_3)); + + assertTrue(mSensitiveContentPackages.shouldBlockScreenCaptureForApp(APP_PKG_2, APP_UID_1)); + assertTrue(mSensitiveContentPackages.shouldBlockScreenCaptureForApp(APP_PKG_2, APP_UID_2)); + assertFalse(mSensitiveContentPackages.shouldBlockScreenCaptureForApp(APP_PKG_2, APP_UID_3)); + + assertFalse(mSensitiveContentPackages.shouldBlockScreenCaptureForApp(APP_PKG_3, APP_UID_1)); + assertFalse(mSensitiveContentPackages.shouldBlockScreenCaptureForApp(APP_PKG_3, APP_UID_2)); + assertFalse(mSensitiveContentPackages.shouldBlockScreenCaptureForApp(APP_PKG_3, APP_UID_3)); + } + + @Test + public void addBlockScreenCaptureForApps_withPartialPreviousPackages() { + ArraySet<PackageInfo> blockedApps = new ArraySet( + Set.of(new PackageInfo(APP_PKG_1, APP_UID_1), + new PackageInfo(APP_PKG_1, APP_UID_2), + new PackageInfo(APP_PKG_2, APP_UID_1), + new PackageInfo(APP_PKG_2, APP_UID_2) + )); + + mSensitiveContentPackages.addBlockScreenCaptureForApps(blockedApps); + boolean modified = mSensitiveContentPackages + .addBlockScreenCaptureForApps( + new ArraySet(Set.of(new PackageInfo(APP_PKG_3, APP_UID_1)))); + + assertTrue(modified); + + assertTrue(mSensitiveContentPackages.shouldBlockScreenCaptureForApp(APP_PKG_1, APP_UID_1)); + assertTrue(mSensitiveContentPackages.shouldBlockScreenCaptureForApp(APP_PKG_1, APP_UID_2)); + assertFalse(mSensitiveContentPackages.shouldBlockScreenCaptureForApp(APP_PKG_1, APP_UID_3)); + + assertTrue(mSensitiveContentPackages.shouldBlockScreenCaptureForApp(APP_PKG_2, APP_UID_1)); + assertTrue(mSensitiveContentPackages.shouldBlockScreenCaptureForApp(APP_PKG_2, APP_UID_2)); + assertFalse(mSensitiveContentPackages.shouldBlockScreenCaptureForApp(APP_PKG_2, APP_UID_3)); + + assertTrue(mSensitiveContentPackages.shouldBlockScreenCaptureForApp(APP_PKG_3, APP_UID_1)); + assertFalse(mSensitiveContentPackages.shouldBlockScreenCaptureForApp(APP_PKG_3, APP_UID_2)); + assertFalse(mSensitiveContentPackages.shouldBlockScreenCaptureForApp(APP_PKG_3, APP_UID_3)); + } + + @Test + public void clearBlockedApps() { + ArraySet<PackageInfo> blockedApps = new ArraySet( + Set.of(new PackageInfo(APP_PKG_1, APP_UID_1), + new PackageInfo(APP_PKG_1, APP_UID_2), + new PackageInfo(APP_PKG_2, APP_UID_1), + new PackageInfo(APP_PKG_2, APP_UID_2) + )); + + mSensitiveContentPackages.addBlockScreenCaptureForApps(blockedApps); + boolean modified = mSensitiveContentPackages.clearBlockedApps(); + + assertTrue(modified); + + assertFalse(mSensitiveContentPackages.shouldBlockScreenCaptureForApp(APP_PKG_1, APP_UID_1)); + assertFalse(mSensitiveContentPackages.shouldBlockScreenCaptureForApp(APP_PKG_1, APP_UID_2)); + assertFalse(mSensitiveContentPackages.shouldBlockScreenCaptureForApp(APP_PKG_1, APP_UID_3)); + + assertFalse(mSensitiveContentPackages.shouldBlockScreenCaptureForApp(APP_PKG_2, APP_UID_1)); + assertFalse(mSensitiveContentPackages.shouldBlockScreenCaptureForApp(APP_PKG_2, APP_UID_2)); + assertFalse(mSensitiveContentPackages.shouldBlockScreenCaptureForApp(APP_PKG_2, APP_UID_3)); + + assertFalse(mSensitiveContentPackages.shouldBlockScreenCaptureForApp(APP_PKG_3, APP_UID_1)); + assertFalse(mSensitiveContentPackages.shouldBlockScreenCaptureForApp(APP_PKG_3, APP_UID_2)); + assertFalse(mSensitiveContentPackages.shouldBlockScreenCaptureForApp(APP_PKG_3, APP_UID_3)); + } + + @Test + public void clearBlockedApps_alreadyEmpty() { + boolean modified = mSensitiveContentPackages.clearBlockedApps(); - mSensitiveContentPackages.setShouldBlockScreenCaptureForApp(blockedApps); - mSensitiveContentPackages.setShouldBlockScreenCaptureForApp(Collections.emptySet()); + assertFalse(modified); assertFalse(mSensitiveContentPackages.shouldBlockScreenCaptureForApp(APP_PKG_1, APP_UID_1)); assertFalse(mSensitiveContentPackages.shouldBlockScreenCaptureForApp(APP_PKG_1, APP_UID_2)); diff --git a/services/tests/wmtests/src/com/android/server/wm/SplashScreenExceptionListTest.java b/services/tests/wmtests/src/com/android/server/wm/SplashScreenExceptionListTest.java index 2d3c4bbe8bdc..2ea5dc4d7700 100644 --- a/services/tests/wmtests/src/com/android/server/wm/SplashScreenExceptionListTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/SplashScreenExceptionListTest.java @@ -157,8 +157,11 @@ public class SplashScreenExceptionListTest { DeviceConfig.setProperty(DeviceConfig.NAMESPACE_WINDOW_MANAGER, KEY_SPLASH_SCREEN_EXCEPTION_LIST, commaSeparatedList, false); try { - assertTrue("Timed out waiting for DeviceConfig to be updated.", - latch.await(5, TimeUnit.SECONDS)); + if (!latch.await(1, TimeUnit.SECONDS)) { + Log.w(getClass().getSimpleName(), + "Timed out waiting for DeviceConfig to be updated. Force update."); + mList.updateDeviceConfig(commaSeparatedList); + } } catch (InterruptedException e) { Assert.fail(e.getMessage()); } diff --git a/services/tests/wmtests/src/com/android/server/wm/SurfaceAnimationRunnerTest.java b/services/tests/wmtests/src/com/android/server/wm/SurfaceAnimationRunnerTest.java index 339162a02301..dfea2fc4b22a 100644 --- a/services/tests/wmtests/src/com/android/server/wm/SurfaceAnimationRunnerTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/SurfaceAnimationRunnerTest.java @@ -44,7 +44,6 @@ import android.view.SurfaceControl.Transaction; import android.view.animation.Animation; import android.view.animation.TranslateAnimation; -import androidx.test.filters.FlakyTest; import androidx.test.filters.SmallTest; import com.android.server.AnimationThread; @@ -147,9 +146,10 @@ public class SurfaceAnimationRunnerTest { assertFinishCallbackNotCalled(); } - @FlakyTest(bugId = 71719744) @Test public void testCancel_sneakyCancelBeforeUpdate() throws Exception { + final CountDownLatch animationCancelled = new CountDownLatch(1); + mSurfaceAnimationRunner = new SurfaceAnimationRunner(null, () -> new ValueAnimator() { { setFloatValues(0f, 1f); @@ -162,6 +162,7 @@ public class SurfaceAnimationRunnerTest { // interleaving of multiple threads. Muahahaha if (animation.getCurrentPlayTime() > 0) { mSurfaceAnimationRunner.onAnimationCancelled(mMockSurface); + animationCancelled.countDown(); } listener.onAnimationUpdate(animation); }); @@ -170,11 +171,7 @@ public class SurfaceAnimationRunnerTest { when(mMockAnimationSpec.getDuration()).thenReturn(200L); mSurfaceAnimationRunner.startAnimation(mMockAnimationSpec, mMockSurface, mMockTransaction, this::finishedCallback); - - // We need to wait for two frames: The first frame starts the animation, the second frame - // actually cancels the animation. - waitUntilNextFrame(); - waitUntilNextFrame(); + assertTrue(animationCancelled.await(1, SECONDS)); assertTrue(mSurfaceAnimationRunner.mRunningAnimations.isEmpty()); verify(mMockAnimationSpec, atLeastOnce()).apply(any(), any(), eq(0L)); } diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskTests.java b/services/tests/wmtests/src/com/android/server/wm/TaskTests.java index b36080023ef2..961fdfb14bf3 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TaskTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/TaskTests.java @@ -1822,6 +1822,35 @@ public class TaskTests extends WindowTestsBase { verify(fragment2).assignLayer(t, 2); } + @Test + public void testMoveTaskFragmentsToBottomIfNeeded() { + final TaskFragmentOrganizer organizer = new TaskFragmentOrganizer(Runnable::run); + final Task task = new TaskBuilder(mSupervisor).setCreateActivity(true).build(); + final ActivityRecord unembeddedActivity = task.getTopMostActivity(); + + final TaskFragment fragment1 = createTaskFragmentWithEmbeddedActivity(task, organizer); + final TaskFragment fragment2 = createTaskFragmentWithEmbeddedActivity(task, organizer); + final TaskFragment fragment3 = createTaskFragmentWithEmbeddedActivity(task, organizer); + doReturn(true).when(fragment1).isMoveToBottomIfClearWhenLaunch(); + doReturn(false).when(fragment2).isMoveToBottomIfClearWhenLaunch(); + doReturn(true).when(fragment3).isMoveToBottomIfClearWhenLaunch(); + + assertEquals(unembeddedActivity, task.mChildren.get(0)); + assertEquals(fragment1, task.mChildren.get(1)); + assertEquals(fragment2, task.mChildren.get(2)); + assertEquals(fragment3, task.mChildren.get(3)); + + final int[] finishCount = {0}; + task.moveTaskFragmentsToBottomIfNeeded(unembeddedActivity, finishCount); + + // fragment1 and fragment3 should be moved to the bottom of the task + assertEquals(fragment1, task.mChildren.get(0)); + assertEquals(fragment3, task.mChildren.get(1)); + assertEquals(unembeddedActivity, task.mChildren.get(2)); + assertEquals(fragment2, task.mChildren.get(3)); + assertEquals(2, finishCount[0]); + } + private Task getTestTask() { return new TaskBuilder(mSupervisor).setCreateActivity(true).build(); } 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 a1cc8d5d9188..fe9d83776ad9 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java @@ -115,7 +115,6 @@ import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import java.util.ArrayList; -import java.util.Collections; /** * Build/Install/Run: @@ -139,7 +138,7 @@ public class WindowManagerServiceTests extends WindowTestsBase { @After public void tearDown() { - mWm.mSensitiveContentPackages.setShouldBlockScreenCaptureForApp(Collections.emptySet()); + mWm.mSensitiveContentPackages.clearBlockedApps(); } @Test @@ -824,7 +823,7 @@ public class WindowManagerServiceTests extends WindowTestsBase { } @Test - public void setShouldBlockScreenCaptureForApp() { + public void addBlockScreenCaptureForApps() { String testPackage = "test"; int ownerId1 = 20; int ownerId2 = 21; @@ -833,7 +832,7 @@ public class WindowManagerServiceTests extends WindowTestsBase { blockedPackages.add(blockedPackage); WindowManagerInternal wmInternal = LocalServices.getService(WindowManagerInternal.class); - wmInternal.setShouldBlockScreenCaptureForApp(blockedPackages); + wmInternal.addBlockScreenCaptureForApps(blockedPackages); assertTrue(mWm.mSensitiveContentPackages .shouldBlockScreenCaptureForApp(testPackage, ownerId1)); @@ -843,7 +842,47 @@ public class WindowManagerServiceTests extends WindowTestsBase { } @Test - public void setShouldBlockScreenCaptureForApp_emptySet_clearsCache() { + public void addBlockScreenCaptureForApps_duplicate_verifyNoRefresh() { + String testPackage = "test"; + int ownerId1 = 20; + int ownerId2 = 21; + PackageInfo blockedPackage = new PackageInfo(testPackage, ownerId1); + ArraySet<PackageInfo> blockedPackages = new ArraySet(); + blockedPackages.add(blockedPackage); + + WindowManagerInternal wmInternal = LocalServices.getService(WindowManagerInternal.class); + wmInternal.addBlockScreenCaptureForApps(blockedPackages); + wmInternal.addBlockScreenCaptureForApps(blockedPackages); + + verify(mWm, times(1)).refreshScreenCaptureDisabled(); + } + + @Test + public void addBlockScreenCaptureForApps_notDuplicate_verifyRefresh() { + String testPackage = "test"; + int ownerId1 = 20; + int ownerId2 = 21; + PackageInfo blockedPackage = new PackageInfo(testPackage, ownerId1); + PackageInfo blockedPackage2 = new PackageInfo(testPackage, ownerId2); + ArraySet<PackageInfo> blockedPackages = new ArraySet(); + blockedPackages.add(blockedPackage); + ArraySet<PackageInfo> blockedPackages2 = new ArraySet(); + blockedPackages2.add(blockedPackage); + blockedPackages2.add(blockedPackage2); + + WindowManagerInternal wmInternal = LocalServices.getService(WindowManagerInternal.class); + wmInternal.addBlockScreenCaptureForApps(blockedPackages); + wmInternal.addBlockScreenCaptureForApps(blockedPackages2); + + assertTrue(mWm.mSensitiveContentPackages + .shouldBlockScreenCaptureForApp(testPackage, ownerId1)); + assertTrue(mWm.mSensitiveContentPackages + .shouldBlockScreenCaptureForApp(testPackage, ownerId2)); + verify(mWm, times(2)).refreshScreenCaptureDisabled(); + } + + @Test + public void clearBlockedApps_clearsCache() { String testPackage = "test"; int ownerId1 = 20; PackageInfo blockedPackage = new PackageInfo(testPackage, ownerId1); @@ -851,8 +890,8 @@ public class WindowManagerServiceTests extends WindowTestsBase { blockedPackages.add(blockedPackage); WindowManagerInternal wmInternal = LocalServices.getService(WindowManagerInternal.class); - wmInternal.setShouldBlockScreenCaptureForApp(blockedPackages); - wmInternal.setShouldBlockScreenCaptureForApp(Collections.emptySet()); + wmInternal.addBlockScreenCaptureForApps(blockedPackages); + wmInternal.clearBlockedApps(); assertFalse(mWm.mSensitiveContentPackages .shouldBlockScreenCaptureForApp(testPackage, ownerId1)); @@ -860,6 +899,14 @@ public class WindowManagerServiceTests extends WindowTestsBase { } @Test + public void clearBlockedApps_alreadyEmpty_verifyNoRefresh() { + WindowManagerInternal wmInternal = LocalServices.getService(WindowManagerInternal.class); + wmInternal.clearBlockedApps(); + + verify(mWm, never()).refreshScreenCaptureDisabled(); + } + + @Test public void testisLetterboxBackgroundMultiColored() { assertThat(setupLetterboxConfigurationWithBackgroundType( LETTERBOX_BACKGROUND_APP_COLOR_BACKGROUND_FLOATING)).isTrue(); diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java index fb4edfacb8e3..a0562aa2f710 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java @@ -136,7 +136,7 @@ public class WindowStateTests extends WindowTestsBase { @After public void tearDown() { - mWm.mSensitiveContentPackages.setShouldBlockScreenCaptureForApp(Collections.emptySet()); + mWm.mSensitiveContentPackages.clearBlockedApps(); } @Test @@ -1398,7 +1398,7 @@ public class WindowStateTests extends WindowTestsBase { PackageInfo blockedPackage = new PackageInfo(testPackage, ownerId1); ArraySet<PackageInfo> blockedPackages = new ArraySet(); blockedPackages.add(blockedPackage); - mWm.mSensitiveContentPackages.setShouldBlockScreenCaptureForApp(blockedPackages); + mWm.mSensitiveContentPackages.addBlockScreenCaptureForApps(blockedPackages); assertTrue(window1.isSecureLocked()); assertFalse(window2.isSecureLocked()); 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 9c421ba29796..7ae5a1156d07 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java @@ -1062,6 +1062,8 @@ class WindowTestsBase extends SystemServiceTestsBase { mWm.mAnimator.ready(); if (!mWm.mWindowPlacerLocked.isTraversalScheduled()) { mRootWindowContainer.performSurfacePlacement(); + } else { + waitHandlerIdle(mWm.mAnimationHandler); } waitUntilWindowAnimatorIdle(); } diff --git a/services/usage/java/com/android/server/usage/BroadcastResponseStatsLogger.java b/services/usage/java/com/android/server/usage/BroadcastResponseStatsLogger.java index 336bfdd0fb14..a8fd6f29f862 100644 --- a/services/usage/java/com/android/server/usage/BroadcastResponseStatsLogger.java +++ b/services/usage/java/com/android/server/usage/BroadcastResponseStatsLogger.java @@ -35,11 +35,13 @@ import android.util.Slog; import android.util.TimeUtils; import com.android.internal.annotations.GuardedBy; -import com.android.internal.annotations.Keep; import com.android.internal.util.IndentingPrintWriter; import com.android.internal.util.RingBuffer; import com.android.server.usage.BroadcastResponseStatsTracker.NotificationEventType; +import java.util.function.IntFunction; +import java.util.function.Supplier; + public class BroadcastResponseStatsLogger { private static final int MAX_LOG_SIZE = @@ -49,10 +51,10 @@ public class BroadcastResponseStatsLogger { @GuardedBy("mLock") private final LogBuffer mBroadcastEventsBuffer = new LogBuffer( - BroadcastEvent.class, MAX_LOG_SIZE); + BroadcastEvent::new, BroadcastEvent[]::new, MAX_LOG_SIZE); @GuardedBy("mLock") private final LogBuffer mNotificationEventsBuffer = new LogBuffer( - NotificationEvent.class, MAX_LOG_SIZE); + NotificationEvent::new, NotificationEvent[]::new, MAX_LOG_SIZE); void logBroadcastDispatchEvent(int sourceUid, @NonNull String targetPackage, UserHandle targetUser, long idForResponseEvent, @@ -96,8 +98,8 @@ public class BroadcastResponseStatsLogger { private static final class LogBuffer<T extends Data> extends RingBuffer<T> { - LogBuffer(Class<T> classType, int capacity) { - super(classType, capacity); + LogBuffer(Supplier<T> newItem, IntFunction<T[]> newBacking, int capacity) { + super(newItem, newBacking, capacity); } void logBroadcastDispatchEvent(int sourceUid, @NonNull String targetPackage, @@ -179,8 +181,7 @@ public class BroadcastResponseStatsLogger { } } - @Keep - public static final class BroadcastEvent implements Data { + private static final class BroadcastEvent implements Data { public int sourceUid; public int targetUserId; public int targetUidProcessState; @@ -200,8 +201,7 @@ public class BroadcastResponseStatsLogger { } } - @Keep - public static final class NotificationEvent implements Data { + private static final class NotificationEvent implements Data { public int type; public String packageName; public int userId; @@ -218,7 +218,7 @@ public class BroadcastResponseStatsLogger { } } - public interface Data { + private interface Data { void reset(); } } diff --git a/services/voiceinteraction/java/com/android/server/soundtrigger/PhoneCallStateHandler.java b/services/voiceinteraction/java/com/android/server/soundtrigger/PhoneCallStateHandler.java index 1df7012c44f8..49ad46131b0d 100644 --- a/services/voiceinteraction/java/com/android/server/soundtrigger/PhoneCallStateHandler.java +++ b/services/voiceinteraction/java/com/android/server/soundtrigger/PhoneCallStateHandler.java @@ -62,7 +62,8 @@ public class PhoneCallStateHandler { SubscriptionManager subscriptionManager, TelephonyManager telephonyManager, Callback callback) { - mSubscriptionManager = Objects.requireNonNull(subscriptionManager); + mSubscriptionManager = Objects.requireNonNull(subscriptionManager) + .createForAllUserProfiles(); mTelephonyManager = Objects.requireNonNull(telephonyManager); mCallback = Objects.requireNonNull(callback); mSubscriptionManager.addOnSubscriptionsChangedListener( diff --git a/telecomm/java/android/telecom/CallControl.java b/telecomm/java/android/telecom/CallControl.java index 24d39182add6..fe699af86f1d 100644 --- a/telecomm/java/android/telecom/CallControl.java +++ b/telecomm/java/android/telecom/CallControl.java @@ -19,6 +19,7 @@ package android.telecom; import static android.telecom.CallException.TRANSACTION_EXCEPTION_KEY; import android.annotation.CallbackExecutor; +import android.annotation.FlaggedApi; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SuppressLint; @@ -32,6 +33,7 @@ import android.text.TextUtils; import com.android.internal.telecom.ClientTransactionalServiceRepository; import com.android.internal.telecom.ICallControl; +import com.android.server.telecom.flags.Flags; import java.util.List; import java.util.Objects; @@ -292,6 +294,43 @@ public final class CallControl { } /** + * Request a new mute state. Note: {@link CallEventCallback#onMuteStateChanged(boolean)} + * will be called every time the mute state is changed and can be used to track the current + * mute state. + * + * @param isMuted The new mute state. Passing in a {@link Boolean#TRUE} for the isMuted + * parameter will mute the call. {@link Boolean#FALSE} will unmute the call. + * @param executor The {@link Executor} on which the {@link OutcomeReceiver} callback + * will be called on. + * @param callback The {@link OutcomeReceiver} that will be completed on the Telecom side + * that details success or failure of the requested operation. + * + * {@link OutcomeReceiver#onResult} will be called if Telecom has + * successfully changed the mute state. + * + * {@link OutcomeReceiver#onError} will be called if Telecom has failed to + * switch to the mute state. A {@link CallException} will be + * passed that details why the operation failed. + */ + @FlaggedApi(Flags.FLAG_SET_MUTE_STATE) + public void setMuteState(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)); + + } catch (RemoteException e) { + throw e.rethrowAsRuntimeException(); + } + } else { + throw new IllegalStateException(INTERFACE_ERROR_MSG); + } + } + + /** * Raises an event to the {@link android.telecom.InCallService} implementations tracking this * call via {@link android.telecom.Call.Callback#onConnectionEvent(Call, String, Bundle)}. * These events and the associated extra keys for the {@code Bundle} parameter are mutually diff --git a/telecomm/java/android/telecom/TelecomManager.java b/telecomm/java/android/telecom/TelecomManager.java index 57b13e960093..e81f48280e46 100644 --- a/telecomm/java/android/telecom/TelecomManager.java +++ b/telecomm/java/android/telecom/TelecomManager.java @@ -1049,8 +1049,17 @@ public class TelecomManager { public static final int PRESENTATION_UNAVAILABLE = 5; + /** + * Controls audio route for video calls. + * 0 - Use the default audio routing strategy. + * 1 - Disable the speaker. Route the audio to Headset or Bluetooth + * or Earpiece, based on the default audio routing strategy. + * @hide + */ + public static final String PROPERTY_VIDEOCALL_AUDIO_OUTPUT = "persist.radio.call.audio.output"; + /* - * Values for the adb property "persist.radio.videocall.audio.output" + * Values for the adb property "persist.radio.call.audio.output" */ /** @hide */ public static final int AUDIO_OUTPUT_ENABLE_SPEAKER = 0; diff --git a/telecomm/java/com/android/internal/telecom/ICallControl.aidl b/telecomm/java/com/android/internal/telecom/ICallControl.aidl index 5e2c923e4c9c..372e4a12ff70 100644 --- a/telecomm/java/com/android/internal/telecom/ICallControl.aidl +++ b/telecomm/java/com/android/internal/telecom/ICallControl.aidl @@ -32,5 +32,6 @@ oneway interface ICallControl { void disconnect(String callId, in DisconnectCause disconnectCause, in ResultReceiver callback); void startCallStreaming(String callId, in ResultReceiver callback); void requestCallEndpointChange(in CallEndpoint callEndpoint, in ResultReceiver callback); + void setMuteState(boolean isMuted, in ResultReceiver callback); void sendEvent(String callId, String event, in Bundle extras); }
\ No newline at end of file diff --git a/telephony/java/android/telephony/CarrierConfigManager.java b/telephony/java/android/telephony/CarrierConfigManager.java index 73c26a3e5fc9..1badf674c8ce 100644 --- a/telephony/java/android/telephony/CarrierConfigManager.java +++ b/telephony/java/android/telephony/CarrierConfigManager.java @@ -9694,6 +9694,27 @@ public class CarrierConfigManager { "remove_satellite_plmn_in_manual_network_scan_bool"; /** + * An integer key holds the time interval for refreshing or re-querying the satellite + * entitlement status from the entitlement server to ensure it is the latest. + * + * The default value is 30 days (1 month). + */ + @FlaggedApi(Flags.FLAG_CARRIER_ENABLED_SATELLITE_FLAG) + public static final String KEY_SATELLITE_ENTITLEMENT_STATUS_REFRESH_DAYS_INT = + "satellite_entitlement_status_refresh_days_int"; + + /** + * This configuration enables device to query the entitlement server to get the satellite + * configuration. + * This will need agreement the carrier before enabling this flag. + * + * The default value is false. + */ + @FlaggedApi(Flags.FLAG_CARRIER_ENABLED_SATELLITE_FLAG) + public static final String KEY_SATELLITE_ENTITLEMENT_SUPPORTED_BOOL = + "satellite_entitlement_supported_bool"; + + /** * Indicating whether DUN APN should be disabled when the device is roaming. In that case, * the default APN (i.e. internet) will be used for tethering. * @@ -10799,6 +10820,8 @@ public class CarrierConfigManager { sDefaults.putInt(KEY_PARAMETERS_USED_FOR_NTN_LTE_SIGNAL_BAR_INT, CellSignalStrengthLte.USE_RSRP); sDefaults.putBoolean(KEY_REMOVE_SATELLITE_PLMN_IN_MANUAL_NETWORK_SCAN_BOOL, true); + sDefaults.putInt(KEY_SATELLITE_ENTITLEMENT_STATUS_REFRESH_DAYS_INT, 30); + sDefaults.putBoolean(KEY_SATELLITE_ENTITLEMENT_SUPPORTED_BOOL, false); sDefaults.putBoolean(KEY_DISABLE_DUN_APN_WHILE_ROAMING_WITH_PRESET_APN_BOOL, false); sDefaults.putString(KEY_DEFAULT_PREFERRED_APN_NAME_STRING, ""); sDefaults.putBoolean(KEY_SUPPORTS_CALL_COMPOSER_BOOL, false); diff --git a/telephony/java/android/telephony/ims/ImsService.java b/telephony/java/android/telephony/ims/ImsService.java index 4c37f7d3184c..b84ff2977b34 100644 --- a/telephony/java/android/telephony/ims/ImsService.java +++ b/telephony/java/android/telephony/ims/ImsService.java @@ -16,6 +16,7 @@ package android.telephony.ims; +import android.annotation.FlaggedApi; import android.annotation.LongDef; import android.annotation.NonNull; import android.annotation.Nullable; @@ -46,6 +47,7 @@ import android.util.SparseBooleanArray; import com.android.ims.internal.IImsFeatureStatusCallback; import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.telephony.flags.Flags; import com.android.internal.telephony.util.TelephonyUtils; import java.lang.annotation.Retention; @@ -152,12 +154,36 @@ public class ImsService extends Service { public static final long CAPABILITY_TERMINAL_BASED_CALL_WAITING = 1 << 2; /** + * This ImsService supports the capability to manage calls on multiple subscriptions at the same + * time. + * <p> + * When set, this ImsService supports managing calls on multiple subscriptions at the same time + * for all WLAN network configurations. Telephony will allow new outgoing/incoming IMS calls to + * be set up on other subscriptions while there is an ongoing call. The ImsService must also + * support managing calls on WWAN + WWAN configurations whenever the modem also reports + * simultaneous calling availability, which can be listened to using the + * {@link android.telephony.TelephonyCallback.SimultaneousCellularCallingSupportListener} API. + * Telephony will only allow additional ongoing/incoming IMS calls on another subscription to be + * set up on WWAN + WWAN configurations when the modem reports that simultaneous cellular + * calling is allowed at the current time on both subscriptions where there are ongoing calls. + * <p> + * When unset (default), this ImsService can not support calls on multiple subscriptions at the + * same time for any WLAN or WWAN configurations, so pending outgoing call placed on another + * cellular subscription while there is an ongoing call will be cancelled by Telephony. + * Similarly, any incoming call notification on another cellular subscription while there is an + * ongoing call will be rejected. + * @hide TODO: move this to system API when we have a backing implementation + CTS testing + */ + @FlaggedApi(Flags.FLAG_SIMULTANEOUS_CALLING_INDICATIONS) + public static final long CAPABILITY_SUPPORTS_SIMULTANEOUS_CALLING = 1 << 3; + + /** * Used for internal correctness checks of capabilities set by the ImsService implementation and * tracks the index of the largest defined flag in the capabilities long. * @hide */ public static final long CAPABILITY_MAX_INDEX = - Long.numberOfTrailingZeros(CAPABILITY_TERMINAL_BASED_CALL_WAITING); + Long.numberOfTrailingZeros(CAPABILITY_SUPPORTS_SIMULTANEOUS_CALLING); /** * @hide diff --git a/telephony/java/android/telephony/satellite/SatelliteManager.java b/telephony/java/android/telephony/satellite/SatelliteManager.java index 3b0397b6e480..70047a6feb9c 100644 --- a/telephony/java/android/telephony/satellite/SatelliteManager.java +++ b/telephony/java/android/telephony/satellite/SatelliteManager.java @@ -928,10 +928,19 @@ public final class SatelliteManager { @FlaggedApi(Flags.FLAG_CARRIER_ENABLED_SATELLITE_FLAG) public static final int SATELLITE_COMMUNICATION_RESTRICTION_REASON_GEOLOCATION = 1; + /** + * Satellite communication restricted by entitlement server. This can be triggered based on + * the EntitlementStatus value received from the entitlement server to enable or disable + * satellite. + */ + @FlaggedApi(Flags.FLAG_CARRIER_ENABLED_SATELLITE_FLAG) + public static final int SATELLITE_COMMUNICATION_RESTRICTION_REASON_ENTITLEMENT = 2; + /** @hide */ @IntDef(prefix = "SATELLITE_COMMUNICATION_RESTRICTION_REASON_", value = { SATELLITE_COMMUNICATION_RESTRICTION_REASON_USER, - SATELLITE_COMMUNICATION_RESTRICTION_REASON_GEOLOCATION + SATELLITE_COMMUNICATION_RESTRICTION_REASON_GEOLOCATION, + SATELLITE_COMMUNICATION_RESTRICTION_REASON_ENTITLEMENT }) @Retention(RetentionPolicy.SOURCE) public @interface SatelliteCommunicationRestrictionReason {} diff --git a/test-base/Android.bp b/test-base/Android.bp index 527159a78ebf..70a95400bd9e 100644 --- a/test-base/Android.bp +++ b/test-base/Android.bp @@ -120,12 +120,13 @@ filegroup { path: "src", } -// Make the current.txt available for use by the cts/tests/signature tests. +// Make the current.txt available for use by the cts/tests/signature and /vendor tests. // ======================================================================== filegroup { name: "android-test-base-current.txt", visibility: [ "//cts/tests/signature/api", + "//vendor:__subpackages__", ], srcs: [ "api/current.txt", diff --git a/test-mock/Android.bp b/test-mock/Android.bp index 2ff74132ffbb..f37d2d17973e 100644 --- a/test-mock/Android.bp +++ b/test-mock/Android.bp @@ -77,12 +77,13 @@ android_ravenwood_test { auto_gen_config: true, } -// Make the current.txt available for use by the cts/tests/signature tests. +// Make the current.txt available for use by the cts/tests/signature and /vendor tests. // ======================================================================== filegroup { name: "android-test-mock-current.txt", visibility: [ "//cts/tests/signature/api", + "//vendor:__subpackages__", ], srcs: [ "api/current.txt", diff --git a/test-mock/src/android/test/mock/MockContext.java b/test-mock/src/android/test/mock/MockContext.java index f5f9d97787ae..cf38bea55f2c 100644 --- a/test-mock/src/android/test/mock/MockContext.java +++ b/test-mock/src/android/test/mock/MockContext.java @@ -822,12 +822,6 @@ public class MockContext extends Context { throw new UnsupportedOperationException(); } - /** {@hide} */ - @Override - public int getUserId() { - throw new UnsupportedOperationException(); - } - @Override public Context createConfigurationContext(Configuration overrideConfiguration) { throw new UnsupportedOperationException(); diff --git a/test-runner/Android.bp b/test-runner/Android.bp index 13a5dac9eb38..21e09d3221ce 100644 --- a/test-runner/Android.bp +++ b/test-runner/Android.bp @@ -79,12 +79,13 @@ java_library { ], } -// Make the current.txt available for use by the cts/tests/signature tests. +// Make the current.txt available for use by the cts/tests/signature and /vendor tests. // ======================================================================== filegroup { name: "android-test-runner-current.txt", visibility: [ "//cts/tests/signature/api", + "//vendor:__subpackages__", ], srcs: [ "api/current.txt", diff --git a/services/tests/servicestests/test-apps/JobTestApp/Android.bp b/tests/Camera2Tests/CameraToo/tests/Android.bp index 6458bcd4fc8f..8339a2c8ab25 100644 --- a/services/tests/servicestests/test-apps/JobTestApp/Android.bp +++ b/tests/Camera2Tests/CameraToo/tests/Android.bp @@ -1,4 +1,4 @@ -// Copyright (C) 2017 The Android Open Source Project +// Copyright (C) 2014 The Android Open 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,24 +14,21 @@ package { // See: http://go/android-license-faq - // A large-scale-change added 'default_applicable_licenses' to import - // all of the 'license_kinds' from "frameworks_base_license" - // to get the below license kinds: - // SPDX-license-identifier-Apache-2.0 - default_applicable_licenses: ["frameworks_base_license"], + default_applicable_licenses: [ + "frameworks_base_license", + ], } -android_test_helper_app { - name: "JobTestApp", - +android_test { + name: "CameraTooTests", + instrumentation_for: "CameraToo", + srcs: ["src/**/*.java"], sdk_version: "current", - - srcs: ["**/*.java"], - - dex_preopt: { - enabled: false, - }, - optimize: { - enabled: false, - }, + static_libs: [ + "androidx.test.rules", + "mockito-target-minus-junit4", + ], + data: [ + ":CameraToo", + ], } diff --git a/tests/Camera2Tests/CameraToo/tests/Android.mk b/tests/Camera2Tests/CameraToo/tests/Android.mk deleted file mode 100644 index dfa64f1feade..000000000000 --- a/tests/Camera2Tests/CameraToo/tests/Android.mk +++ /dev/null @@ -1,28 +0,0 @@ -# Copyright (C) 2014 The Android Open Source Project -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT 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) -include $(CLEAR_VARS) - -LOCAL_MODULE_TAGS := tests -LOCAL_PACKAGE_NAME := CameraTooTests -LOCAL_LICENSE_KINDS := SPDX-license-identifier-Apache-2.0 -LOCAL_LICENSE_CONDITIONS := notice -LOCAL_NOTICE_FILE := $(LOCAL_PATH)/../../../../NOTICE -LOCAL_INSTRUMENTATION_FOR := CameraToo -LOCAL_SDK_VERSION := current -LOCAL_SRC_FILES := $(call all-java-files-under,src) -LOCAL_STATIC_JAVA_LIBRARIES := androidx.test.rules mockito-target-minus-junit4 - -include $(BUILD_PACKAGE) diff --git a/tests/Camera2Tests/CameraToo/tests/AndroidTest.xml b/tests/Camera2Tests/CameraToo/tests/AndroidTest.xml new file mode 100644 index 000000000000..884c095fef49 --- /dev/null +++ b/tests/Camera2Tests/CameraToo/tests/AndroidTest.xml @@ -0,0 +1,30 @@ +<?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. +--> +<configuration description="Runs CameraToo tests."> + <option name="test-suite-tag" value="apct" /> + <option name="test-suite-tag" value="apct-instrumentation" /> + + <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller"> + <option name="cleanup-apks" value="true" /> + <option name="test-file-name" value="CameraTooTests.apk" /> + <option name="test-file-name" value="CameraToo.apk" /> + </target_preparer> + + <test class="com.android.tradefed.testtype.AndroidJUnitTest" > + <option name="package" value="com.example.android.camera2.cameratoo.tests" /> + <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" /> + </test> +</configuration> diff --git a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityEmbeddingSecondaryActivity.java b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityEmbeddingSecondaryActivity.java index e3988cd20199..c44d9435d203 100644 --- a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityEmbeddingSecondaryActivity.java +++ b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityEmbeddingSecondaryActivity.java @@ -68,7 +68,7 @@ public class ActivityEmbeddingSecondaryActivity extends Activity { // This triggers a recalcuation of splitatributes. ActivityEmbeddingController .getInstance(ActivityEmbeddingSecondaryActivity.this) - .invalidateTopVisibleActivityStacks(); + .invalidateVisibleActivityStacks(); } }); findViewById(R.id.secondary_enter_pip_button).setOnClickListener( diff --git a/tests/FsVerityTest/AndroidTest.xml b/tests/FsVerityTest/AndroidTest.xml index d2537f6410e8..f2d7990436e4 100644 --- a/tests/FsVerityTest/AndroidTest.xml +++ b/tests/FsVerityTest/AndroidTest.xml @@ -15,6 +15,7 @@ --> <configuration description="fs-verity end-to-end test"> <option name="test-suite-tag" value="apct" /> + <option name="config-descriptor:metadata" key="parameter" value="secondary_user"/> <object type="module_controller" class="com.android.tradefed.testtype.suite.module.ShippingApiLevelModuleController"> <!-- fs-verity is required since R/30 --> diff --git a/tests/Input/src/com/android/server/input/KeyboardLayoutManagerTests.kt b/tests/Input/src/com/android/server/input/KeyboardLayoutManagerTests.kt index bbd4567a4454..7343ba1c1ce7 100644 --- a/tests/Input/src/com/android/server/input/KeyboardLayoutManagerTests.kt +++ b/tests/Input/src/com/android/server/input/KeyboardLayoutManagerTests.kt @@ -16,6 +16,7 @@ package com.android.server.input +import android.app.NotificationManager import android.content.Context import android.content.ContextWrapper import android.content.pm.ActivityInfo @@ -129,6 +130,8 @@ class KeyboardLayoutManagerTests { @Mock private lateinit var packageManager: PackageManager + @Mock + private lateinit var notificationManager: NotificationManager private lateinit var keyboardLayoutManager: KeyboardLayoutManager private lateinit var imeInfo: InputMethodInfo @@ -163,6 +166,8 @@ class KeyboardLayoutManagerTests { keyboardLayoutManager = Mockito.spy( KeyboardLayoutManager(context, native, dataStore, testLooper.looper) ) + Mockito.`when`(context.getSystemService(Mockito.eq(Context.NOTIFICATION_SERVICE))) + .thenReturn(notificationManager) setupInputDevices() setupBroadcastReceiver() setupIme() @@ -946,6 +951,48 @@ class KeyboardLayoutManagerTests { } } + @Test + fun testNotificationShown_onInputDeviceChanged() { + val imeInfos = listOf(KeyboardLayoutManager.ImeInfo(0, imeInfo, createImeSubtype())) + Mockito.doReturn(imeInfos).`when`(keyboardLayoutManager).imeInfoListForLayoutMapping + Mockito.doReturn(false).`when`(keyboardLayoutManager).isVirtualDevice( + ArgumentMatchers.eq(keyboardDevice.id) + ) + NewSettingsApiFlag(true).use { + keyboardLayoutManager.onInputDeviceChanged(keyboardDevice.id) + ExtendedMockito.verify( + notificationManager, + Mockito.times(1) + ).notifyAsUser( + ArgumentMatchers.isNull(), + ArgumentMatchers.anyInt(), + ArgumentMatchers.any(), + ArgumentMatchers.any() + ) + } + } + + @Test + fun testNotificationNotShown_onInputDeviceChanged_forVirtualDevice() { + val imeInfos = listOf(KeyboardLayoutManager.ImeInfo(0, imeInfo, createImeSubtype())) + Mockito.doReturn(imeInfos).`when`(keyboardLayoutManager).imeInfoListForLayoutMapping + Mockito.doReturn(true).`when`(keyboardLayoutManager).isVirtualDevice( + ArgumentMatchers.eq(keyboardDevice.id) + ) + NewSettingsApiFlag(true).use { + keyboardLayoutManager.onInputDeviceChanged(keyboardDevice.id) + ExtendedMockito.verify( + notificationManager, + Mockito.never() + ).notifyAsUser( + ArgumentMatchers.isNull(), + ArgumentMatchers.anyInt(), + ArgumentMatchers.any(), + ArgumentMatchers.any() + ) + } + } + private fun assertCorrectLayout( device: InputDevice, imeSubtype: InputMethodSubtype, diff --git a/tools/aapt2/format/binary/TableFlattener.cpp b/tools/aapt2/format/binary/TableFlattener.cpp index f05611048caa..1a82021bce71 100644 --- a/tools/aapt2/format/binary/TableFlattener.cpp +++ b/tools/aapt2/format/binary/TableFlattener.cpp @@ -381,9 +381,9 @@ class PackageFlattener { return true; } - bool FlattenTypeSpec(const ResourceTableTypeView& type, - const std::vector<ResourceTableEntryView>& sorted_entries, - BigBuffer* buffer) { + ResTable_typeSpec* FlattenTypeSpec(const ResourceTableTypeView& type, + const std::vector<ResourceTableEntryView>& sorted_entries, + BigBuffer* buffer) { ChunkWriter type_spec_writer(buffer); ResTable_typeSpec* spec_header = type_spec_writer.StartChunk<ResTable_typeSpec>(RES_TABLE_TYPE_SPEC_TYPE); @@ -391,7 +391,7 @@ class PackageFlattener { if (sorted_entries.empty()) { type_spec_writer.Finish(); - return true; + return spec_header; } // We can't just take the size of the vector. There may be holes in the @@ -427,7 +427,7 @@ class PackageFlattener { } } type_spec_writer.Finish(); - return true; + return spec_header; } bool FlattenTypes(BigBuffer* buffer) { @@ -450,7 +450,8 @@ class PackageFlattener { expected_type_id++; type_pool_.MakeRef(type.named_type.to_string()); - if (!FlattenTypeSpec(type, type.entries, buffer)) { + const auto type_spec_header = FlattenTypeSpec(type, type.entries, buffer); + if (!type_spec_header) { return false; } @@ -511,6 +512,10 @@ class PackageFlattener { return false; } } + + // And now we can update the type entries count in the typeSpec header. + type_spec_header->typesCount = android::util::HostToDevice16(uint16_t(std::min<uint32_t>( + config_to_entry_list_map.size(), std::numeric_limits<uint16_t>::max()))); } return true; } diff --git a/tools/aapt2/integration-tests/MergeOnlyTest/Android.mk b/tools/aapt2/integration-tests/MergeOnlyTest/Android.mk deleted file mode 100644 index 6361f9b8ae7d..000000000000 --- a/tools/aapt2/integration-tests/MergeOnlyTest/Android.mk +++ /dev/null @@ -1,2 +0,0 @@ -LOCAL_PATH := $(call my-dir) -include $(call all-makefiles-under,$(LOCAL_PATH)) diff --git a/tools/aapt2/integration-tests/MergeOnlyTest/App/Android.mk b/tools/aapt2/integration-tests/MergeOnlyTest/App/Android.mk deleted file mode 100644 index 27b6068632f3..000000000000 --- a/tools/aapt2/integration-tests/MergeOnlyTest/App/Android.mk +++ /dev/null @@ -1,32 +0,0 @@ -// -// Copyright (C) 2019 The Android Open Source Project -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT 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) - -include $(CLEAR_VARS) -LOCAL_USE_AAPT2 := true -LOCAL_AAPT_NAMESPACES := true -LOCAL_PACKAGE_NAME := AaptTestMergeOnly_App -LOCAL_LICENSE_KINDS := SPDX-license-identifier-Apache-2.0 -LOCAL_LICENSE_CONDITIONS := notice -LOCAL_NOTICE_FILE := $(LOCAL_PATH)/../../../../../NOTICE -LOCAL_SDK_VERSION := current -LOCAL_EXPORT_PACKAGE_RESOURCES := true -LOCAL_MODULE_TAGS := tests -LOCAL_STATIC_ANDROID_LIBRARIES := \ - AaptTestMergeOnly_LeafLib \ - AaptTestMergeOnly_LocalLib -include $(BUILD_PACKAGE) diff --git a/tools/aapt2/integration-tests/MergeOnlyTest/LeafLib/Android.mk b/tools/aapt2/integration-tests/MergeOnlyTest/LeafLib/Android.mk deleted file mode 100644 index c084849a9d28..000000000000 --- a/tools/aapt2/integration-tests/MergeOnlyTest/LeafLib/Android.mk +++ /dev/null @@ -1,31 +0,0 @@ -// -// Copyright (C) 2019 The Android Open Source Project -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT 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) - -include $(CLEAR_VARS) -LOCAL_USE_AAPT2 := true -LOCAL_AAPT_NAMESPACES := true -LOCAL_MODULE := AaptTestMergeOnly_LeafLib -LOCAL_LICENSE_KINDS := SPDX-license-identifier-Apache-2.0 -LOCAL_LICENSE_CONDITIONS := notice -LOCAL_NOTICE_FILE := $(LOCAL_PATH)/../../../../../NOTICE -LOCAL_SDK_VERSION := current -LOCAL_MODULE_TAGS := tests -LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res -LOCAL_MIN_SDK_VERSION := 21 -LOCAL_AAPT_FLAGS := --merge-only -include $(BUILD_STATIC_JAVA_LIBRARY) diff --git a/tools/aapt2/integration-tests/MergeOnlyTest/LocalLib/Android.mk b/tools/aapt2/integration-tests/MergeOnlyTest/LocalLib/Android.mk deleted file mode 100644 index 699ad79ecf1a..000000000000 --- a/tools/aapt2/integration-tests/MergeOnlyTest/LocalLib/Android.mk +++ /dev/null @@ -1,31 +0,0 @@ -// -// Copyright (C) 2019 The Android Open Source Project -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT 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) - -include $(CLEAR_VARS) -LOCAL_USE_AAPT2 := true -LOCAL_AAPT_NAMESPACES := true -LOCAL_MODULE := AaptTestMergeOnly_LocalLib -LOCAL_LICENSE_KINDS := SPDX-license-identifier-Apache-2.0 -LOCAL_LICENSE_CONDITIONS := notice -LOCAL_NOTICE_FILE := $(LOCAL_PATH)/../../../../../NOTICE -LOCAL_SDK_VERSION := current -LOCAL_MODULE_TAGS := tests -LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res -LOCAL_MIN_SDK_VERSION := 21 -LOCAL_AAPT_FLAGS := --merge-only -include $(BUILD_STATIC_JAVA_LIBRARY) diff --git a/tools/aapt2/integration-tests/NamespaceTest/Android.mk b/tools/aapt2/integration-tests/NamespaceTest/Android.mk deleted file mode 100644 index 6361f9b8ae7d..000000000000 --- a/tools/aapt2/integration-tests/NamespaceTest/Android.mk +++ /dev/null @@ -1,2 +0,0 @@ -LOCAL_PATH := $(call my-dir) -include $(call all-makefiles-under,$(LOCAL_PATH)) diff --git a/tools/aapt2/integration-tests/NamespaceTest/App/Android.mk b/tools/aapt2/integration-tests/NamespaceTest/App/Android.mk deleted file mode 100644 index 98b74403a7ff..000000000000 --- a/tools/aapt2/integration-tests/NamespaceTest/App/Android.mk +++ /dev/null @@ -1,33 +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. -# - -LOCAL_PATH := $(call my-dir) - -include $(CLEAR_VARS) -LOCAL_USE_AAPT2 := true -LOCAL_AAPT_NAMESPACES := true -LOCAL_PACKAGE_NAME := AaptTestNamespace_App -LOCAL_LICENSE_KINDS := SPDX-license-identifier-Apache-2.0 -LOCAL_LICENSE_CONDITIONS := notice -LOCAL_NOTICE_FILE := $(LOCAL_PATH)/../../../../../NOTICE -LOCAL_SDK_VERSION := current -LOCAL_EXPORT_PACKAGE_RESOURCES := true -LOCAL_MODULE_TAGS := tests -LOCAL_SRC_FILES := $(call all-java-files-under,src) -LOCAL_STATIC_ANDROID_LIBRARIES := \ - AaptTestNamespace_LibOne \ - AaptTestNamespace_LibTwo -include $(BUILD_PACKAGE) diff --git a/tools/aapt2/integration-tests/NamespaceTest/LibOne/Android.mk b/tools/aapt2/integration-tests/NamespaceTest/LibOne/Android.mk deleted file mode 100644 index dd4170234258..000000000000 --- a/tools/aapt2/integration-tests/NamespaceTest/LibOne/Android.mk +++ /dev/null @@ -1,33 +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. -# - -LOCAL_PATH := $(call my-dir) - -include $(CLEAR_VARS) -LOCAL_USE_AAPT2 := true -LOCAL_AAPT_NAMESPACES := true -LOCAL_MODULE := AaptTestNamespace_LibOne -LOCAL_LICENSE_KINDS := SPDX-license-identifier-Apache-2.0 -LOCAL_LICENSE_CONDITIONS := notice -LOCAL_NOTICE_FILE := $(LOCAL_PATH)/../../../../../NOTICE -LOCAL_SDK_VERSION := current -LOCAL_MODULE_TAGS := tests -LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res -LOCAL_MIN_SDK_VERSION := 21 - -# We need this to retain the R.java generated for this library. -LOCAL_JAR_EXCLUDE_FILES := none -include $(BUILD_STATIC_JAVA_LIBRARY) diff --git a/tools/aapt2/integration-tests/NamespaceTest/LibTwo/Android.mk b/tools/aapt2/integration-tests/NamespaceTest/LibTwo/Android.mk deleted file mode 100644 index 0d11bcbda64d..000000000000 --- a/tools/aapt2/integration-tests/NamespaceTest/LibTwo/Android.mk +++ /dev/null @@ -1,34 +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. -# - -LOCAL_PATH := $(call my-dir) - -include $(CLEAR_VARS) -LOCAL_USE_AAPT2 := true -LOCAL_AAPT_NAMESPACES := true -LOCAL_MODULE := AaptTestNamespace_LibTwo -LOCAL_LICENSE_KINDS := SPDX-license-identifier-Apache-2.0 -LOCAL_LICENSE_CONDITIONS := notice -LOCAL_NOTICE_FILE := $(LOCAL_PATH)/../../../../../NOTICE -LOCAL_SDK_VERSION := current -LOCAL_MODULE_TAGS := tests -LOCAL_SRC_FILES := $(call all-java-files-under,src) -LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res -LOCAL_MIN_SDK_VERSION := 21 - -# We need this to retain the R.java generated for this library. -LOCAL_JAR_EXCLUDE_FILES := none -include $(BUILD_STATIC_JAVA_LIBRARY) diff --git a/tools/aapt2/integration-tests/NamespaceTest/Split/Android.mk b/tools/aapt2/integration-tests/NamespaceTest/Split/Android.mk deleted file mode 100644 index 30375728c9e0..000000000000 --- a/tools/aapt2/integration-tests/NamespaceTest/Split/Android.mk +++ /dev/null @@ -1,32 +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. -# - -LOCAL_PATH := $(call my-dir) - -include $(CLEAR_VARS) -LOCAL_USE_AAPT2 := true -LOCAL_AAPT_NAMESPACES := true -LOCAL_PACKAGE_NAME := AaptTestNamespace_Split -LOCAL_LICENSE_KINDS := SPDX-license-identifier-Apache-2.0 -LOCAL_LICENSE_CONDITIONS := notice -LOCAL_NOTICE_FILE := $(LOCAL_PATH)/../../../../../NOTICE -LOCAL_SDK_VERSION := current -LOCAL_MODULE_TAGS := tests -LOCAL_SRC_FILES := $(call all-java-files-under,src) -LOCAL_APK_LIBRARIES := AaptTestNamespace_App -LOCAL_RES_LIBRARIES := AaptTestNamespace_App -LOCAL_AAPT_FLAGS := --package-id 0x80 --rename-manifest-package com.android.aapt.namespace.app -include $(BUILD_PACKAGE) diff --git a/tools/hoststubgen/hoststubgen/helper-framework-runtime-src/libcore-fake/com/android/okhttp/internalandroidapi/Dns.java b/tools/hoststubgen/hoststubgen/helper-framework-runtime-src/libcore-fake/com/android/okhttp/internalandroidapi/Dns.java new file mode 100644 index 000000000000..379c4ae8a059 --- /dev/null +++ b/tools/hoststubgen/hoststubgen/helper-framework-runtime-src/libcore-fake/com/android/okhttp/internalandroidapi/Dns.java @@ -0,0 +1,36 @@ +/* + * 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.okhttp.internalandroidapi; + +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.List; + +/** + * A domain name service that resolves IP addresses for host names. + * @hide + * @hide This class is not part of the Android public SDK API + */ +public interface Dns { + /** + * Returns the IP addresses of {@code hostname}, in the order they should + * be attempted. + * + * @hide + */ + List<InetAddress> lookup(String hostname) throws UnknownHostException; +} |