diff options
156 files changed, 3642 insertions, 2167 deletions
diff --git a/AconfigFlags.bp b/AconfigFlags.bp index 605ce22e1aae..6d74a840525b 100644 --- a/AconfigFlags.bp +++ b/AconfigFlags.bp @@ -16,6 +16,7 @@ aconfig_srcjars = [ ":android.app.usage.flags-aconfig-java{.generated_srcjars}", ":android.companion.flags-aconfig-java{.generated_srcjars}", ":android.content.pm.flags-aconfig-java{.generated_srcjars}", + ":android.content.res.flags-aconfig-java{.generated_srcjars}", ":android.hardware.radio.flags-aconfig-java{.generated_srcjars}", ":android.nfc.flags-aconfig-java{.generated_srcjars}", ":android.os.flags-aconfig-java{.generated_srcjars}", @@ -306,6 +307,19 @@ java_aconfig_library { defaults: ["framework-minus-apex-aconfig-java-defaults"], } +// Resources +aconfig_declarations { + name: "android.content.res.flags-aconfig", + package: "android.content.res", + srcs: ["core/java/android/content/res/*.aconfig"], +} + +java_aconfig_library { + name: "android.content.res.flags-aconfig-java", + aconfig_declarations: "android.content.res.flags-aconfig", + defaults: ["framework-minus-apex-aconfig-java-defaults"], +} + // Media BetterTogether aconfig_declarations { name: "com.android.media.flags.bettertogether-aconfig", diff --git a/api/ApiDocs.bp b/api/ApiDocs.bp index 7142eb5bef53..e1621008cc33 100644 --- a/api/ApiDocs.bp +++ b/api/ApiDocs.bp @@ -131,13 +131,7 @@ droidstubs { defaults: ["framework-doc-stubs-sources-default"], args: metalava_framework_docs_args + " --show-annotation android.annotation.SystemApi\\(client=android.annotation.SystemApi.Client.PRIVILEGED_APPS\\) ", - api_levels_annotations_enabled: true, - api_levels_annotations_dirs: [ - "sdk-dir", - "api-versions-jars-dir", - ], - api_levels_sdk_type: "system", - extensions_info_file: ":sdk-extensions-info", + api_levels_module: "api_versions_system", } ///////////////////////////////////////////////////////////////////// diff --git a/core/api/current.txt b/core/api/current.txt index f6564ecdbec0..66aeb0f7cbaf 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -10571,7 +10571,7 @@ package android.content { public final class ContextParams { method @Nullable public String getAttributionTag(); method @Nullable public android.content.AttributionSource getNextAttributionSource(); - method @NonNull public boolean shouldRegisterAttributionSource(); + method @FlaggedApi("android.permission.flags.should_register_attribution_source") @NonNull public boolean shouldRegisterAttributionSource(); } public static final class ContextParams.Builder { @@ -10580,7 +10580,7 @@ package android.content { method @NonNull public android.content.ContextParams build(); method @NonNull public android.content.ContextParams.Builder setAttributionTag(@Nullable String); method @NonNull public android.content.ContextParams.Builder setNextAttributionSource(@Nullable android.content.AttributionSource); - method @NonNull public android.content.ContextParams.Builder setShouldRegisterAttributionSource(boolean); + method @FlaggedApi("android.permission.flags.should_register_attribution_source") @NonNull public android.content.ContextParams.Builder setShouldRegisterAttributionSource(boolean); } public class ContextWrapper extends android.content.Context { diff --git a/core/api/system-current.txt b/core/api/system-current.txt index 4ff0028e7c83..5d1f6dc3e675 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -312,7 +312,7 @@ package android { field public static final String REMOVE_DRM_CERTIFICATES = "android.permission.REMOVE_DRM_CERTIFICATES"; field public static final String REMOVE_TASKS = "android.permission.REMOVE_TASKS"; field public static final String RENOUNCE_PERMISSIONS = "android.permission.RENOUNCE_PERMISSIONS"; - field @FlaggedApi("backstage_power.report_usage_stats_permission") public static final String REPORT_USAGE_STATS = "android.permission.REPORT_USAGE_STATS"; + field @FlaggedApi("android.app.usage.report_usage_stats_permission") public static final String REPORT_USAGE_STATS = "android.permission.REPORT_USAGE_STATS"; field @Deprecated public static final String REQUEST_NETWORK_SCORES = "android.permission.REQUEST_NETWORK_SCORES"; field public static final String REQUEST_NOTIFICATION_ASSISTANT_SERVICE = "android.permission.REQUEST_NOTIFICATION_ASSISTANT_SERVICE"; field public static final String RESET_PASSWORD = "android.permission.RESET_PASSWORD"; diff --git a/core/java/android/app/usage/flags.aconfig b/core/java/android/app/usage/flags.aconfig index d1f90676a15f..f401a7607364 100644 --- a/core/java/android/app/usage/flags.aconfig +++ b/core/java/android/app/usage/flags.aconfig @@ -2,7 +2,7 @@ package: "android.app.usage" flag { name: "user_interaction_type_api" - namespace: "power_optimization" + namespace: "backstage_power" description: "Feature flag for user interaction event report/query API" bug: "296061232" } diff --git a/core/java/android/content/ContextParams.java b/core/java/android/content/ContextParams.java index 988a9c0ab9b3..b844d358a523 100644 --- a/core/java/android/content/ContextParams.java +++ b/core/java/android/content/ContextParams.java @@ -16,7 +16,10 @@ package android.content; +import static android.permission.flags.Flags.FLAG_SHOULD_REGISTER_ATTRIBUTION_SOURCE; + import android.Manifest; +import android.annotation.FlaggedApi; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.RequiresPermission; @@ -102,6 +105,7 @@ public final class ContextParams { * registered. */ @NonNull + @FlaggedApi(FLAG_SHOULD_REGISTER_ATTRIBUTION_SOURCE) public boolean shouldRegisterAttributionSource() { return mShouldRegisterAttributionSource; } @@ -179,6 +183,7 @@ public final class ContextParams { * created should be registered. */ @NonNull + @FlaggedApi(FLAG_SHOULD_REGISTER_ATTRIBUTION_SOURCE) public Builder setShouldRegisterAttributionSource(boolean shouldRegister) { mShouldRegisterAttributionSource = shouldRegister; return this; diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java index d2b23083ab64..b765562ab587 100644 --- a/core/java/android/content/Intent.java +++ b/core/java/android/content/Intent.java @@ -3929,6 +3929,8 @@ public class Intent implements Parcelable, Cloneable { * {@link #ACTION_BOOT_COMPLETED} is sent. This is sent as a foreground * broadcast, since it is part of a visible user interaction; be as quick * as possible when handling it. + * + * <p><b>Note:</b> This broadcast is not sent to the system user. */ public static final String ACTION_USER_INITIALIZE = "android.intent.action.USER_INITIALIZE"; diff --git a/core/java/android/content/pm/ArchivedActivityParcel.aidl b/core/java/android/content/pm/ArchivedActivityParcel.aidl index 7ab7ed1cc5df..74953fff40d8 100644 --- a/core/java/android/content/pm/ArchivedActivityParcel.aidl +++ b/core/java/android/content/pm/ArchivedActivityParcel.aidl @@ -16,9 +16,12 @@ package android.content.pm; +import android.content.ComponentName; + /** @hide */ parcelable ArchivedActivityParcel { String title; + ComponentName originalComponentName; // PNG compressed bitmaps. byte[] iconBitmap; byte[] monochromeIconBitmap; diff --git a/core/java/android/content/pm/flags.aconfig b/core/java/android/content/pm/flags.aconfig index b2cc070228b7..db12728cfb98 100644 --- a/core/java/android/content/pm/flags.aconfig +++ b/core/java/android/content/pm/flags.aconfig @@ -50,3 +50,11 @@ flag { description: "Feature flag to enable the features that rely on new ART Service APIs that are in the VIC version of the ART module." bug: "304741685" } + +flag { + name: "sdk_lib_independence" + namespace: "package_manager_service" + description: "Feature flag to keep app working even if its declared sdk-library dependency is unavailable." + bug: "295827951" + is_fixed_read_only: true +} diff --git a/core/java/android/content/res/flags.aconfig b/core/java/android/content/res/flags.aconfig new file mode 100644 index 000000000000..0c2c0f494257 --- /dev/null +++ b/core/java/android/content/res/flags.aconfig @@ -0,0 +1,10 @@ +package: "android.content.res" + +flag { + name: "default_locale" + namespace: "resource_manager" + description: "Feature flag for default locale in LocaleConfig" + bug: "117306409" + # fixed_read_only or device wont boot because of permission issues accessing flags during boot + is_fixed_read_only: true +} diff --git a/core/java/android/hardware/SensorManager.java b/core/java/android/hardware/SensorManager.java index f033f9740b3d..bcf447b4267d 100644 --- a/core/java/android/hardware/SensorManager.java +++ b/core/java/android/hardware/SensorManager.java @@ -16,6 +16,7 @@ package android.hardware; +import android.annotation.IntDef; import android.annotation.Nullable; import android.annotation.SystemApi; import android.annotation.SystemService; @@ -27,6 +28,8 @@ import android.os.MemoryFile; import android.util.Log; import android.util.SparseArray; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -1809,6 +1812,41 @@ public abstract class SensorManager { protected abstract boolean cancelTriggerSensorImpl(TriggerEventListener listener, Sensor sensor, boolean disable); + /** + * @hide + */ + @Retention(RetentionPolicy.SOURCE) + @IntDef({DATA_INJECTION, REPLAY_DATA_INJECTION, HAL_BYPASS_REPLAY_DATA_INJECTION}) + public @interface DataInjectionMode {} + /** + * This mode is only used for testing purposes. Not all HALs support this mode. In this mode, + * the HAL ignores the sensor data provided by physical sensors and accepts the data that is + * injected from the SensorService as if it were the real sensor data. This mode is primarily + * used for testing various algorithms like vendor provided SensorFusion, Step Counter and + * Step Detector etc. Typically, in this mode, there is a client app which injects + * sensor data into the HAL. Normal apps can register and unregister for any sensor + * that supports injection. Registering to sensors that do not support injection will + * give an error. + * This is the default data injection mode. + * @hide + */ + public static final int DATA_INJECTION = 1; + /** + * Mostly equivalent to DATA_INJECTION with the difference being that the injected data is + * delivered to all requesting apps rather than just the package allowed to inject data. + * This mode is only allowed to be used on development builds. + * @hide + */ + public static final int REPLAY_DATA_INJECTION = 3; + /** + * Like REPLAY_DATA_INJECTION but injected data is not sent into the HAL. It is stored in a + * buffer in the platform and played back to all requesting apps. + * This is useful for playing back sensor data to test platform components without + * relying on the HAL to support data injection. + * @hide + */ + public static final int HAL_BYPASS_REPLAY_DATA_INJECTION = 4; + /** * For testing purposes only. Not for third party applications. @@ -1833,13 +1871,47 @@ public abstract class SensorManager { */ @SystemApi public boolean initDataInjection(boolean enable) { - return initDataInjectionImpl(enable); + return initDataInjectionImpl(enable, DATA_INJECTION); + } + + /** + * For testing purposes only. Not for third party applications. + * + * Initialize data injection mode and create a client for data injection. SensorService should + * already be operating in one of DATA_INJECTION, REPLAY_DATA_INJECTION or + * HAL_BYPASS_REPLAY_DATA_INJECTION modes for this call succeed. To set SensorService in + * a Data Injection mode, use one of: + * + * <ul> + * <li>adb shell dumpsys sensorservice data_injection</li> + * <li>adb shell dumpsys sensorservice replay_data_injection package_name</li> + * <li>adb shell dumpsys sensorservice hal_bypass_replay_data_injection package_name</li> + * </ul> + * + * Typically this is done using a host side test. This mode is expected to be used + * only for testing purposes. See {@link DataInjectionMode} for details of each data injection + * mode. Once this method succeeds, the test can call + * {@link #injectSensorData(Sensor, float[], int, long)} to inject sensor data into the HAL. + * To put SensorService back into normal mode, use "adb shell dumpsys sensorservice enable" + * + * @param enable True to initialize a client in a data injection mode. + * False to clean up the native resources. + * + * @param mode One of DATA_INJECTION, REPLAY_DATA_INJECTION or HAL_BYPASS_DATA_INJECTION. + * See {@link DataInjectionMode} for details. + * + * @return true if the HAL supports data injection and false + * otherwise. + * @hide + */ + public boolean initDataInjection(boolean enable, @DataInjectionMode int mode) { + return initDataInjectionImpl(enable, mode); } /** * @hide */ - protected abstract boolean initDataInjectionImpl(boolean enable); + protected abstract boolean initDataInjectionImpl(boolean enable, @DataInjectionMode int mode); /** * For testing purposes only. Not for third party applications. @@ -1871,9 +1943,6 @@ public abstract class SensorManager { if (sensor == null) { throw new IllegalArgumentException("sensor cannot be null"); } - if (!sensor.isDataInjectionSupported()) { - throw new IllegalArgumentException("sensor does not support data injection"); - } if (values == null) { throw new IllegalArgumentException("sensor data cannot be null"); } diff --git a/core/java/android/hardware/SystemSensorManager.java b/core/java/android/hardware/SystemSensorManager.java index dfd380233662..40e03dbb8b34 100644 --- a/core/java/android/hardware/SystemSensorManager.java +++ b/core/java/android/hardware/SystemSensorManager.java @@ -90,6 +90,8 @@ public class SystemSensorManager extends SensorManager { private static native void nativeGetRuntimeSensors( long nativeInstance, int deviceId, List<Sensor> list); private static native boolean nativeIsDataInjectionEnabled(long nativeInstance); + private static native boolean nativeIsReplayDataInjectionEnabled(long nativeInstance); + private static native boolean nativeIsHalBypassReplayDataInjectionEnabled(long nativeInstance); private static native int nativeCreateDirectChannel( long nativeInstance, int deviceId, long size, int channelType, int fd, @@ -384,20 +386,41 @@ public class SystemSensorManager extends SensorManager { } } - protected boolean initDataInjectionImpl(boolean enable) { + protected boolean initDataInjectionImpl(boolean enable, @DataInjectionMode int mode) { synchronized (sLock) { + boolean isDataInjectionModeEnabled = false; if (enable) { - boolean isDataInjectionModeEnabled = nativeIsDataInjectionEnabled(mNativeInstance); + switch (mode) { + case DATA_INJECTION: + isDataInjectionModeEnabled = nativeIsDataInjectionEnabled(mNativeInstance); + break; + case REPLAY_DATA_INJECTION: + isDataInjectionModeEnabled = nativeIsReplayDataInjectionEnabled( + mNativeInstance); + break; + case HAL_BYPASS_REPLAY_DATA_INJECTION: + isDataInjectionModeEnabled = nativeIsHalBypassReplayDataInjectionEnabled( + mNativeInstance); + break; + default: + break; + } // The HAL does not support injection OR SensorService hasn't been set in DI mode. if (!isDataInjectionModeEnabled) { - Log.e(TAG, "Data Injection mode not enabled"); + Log.e(TAG, "The correct Data Injection mode has not been enabled"); return false; } + if (sInjectEventQueue != null && sInjectEventQueue.getDataInjectionMode() != mode) { + // The inject event queue has been initialized for a different type of DI + // close it and create a new one + sInjectEventQueue.dispose(); + sInjectEventQueue = null; + } // Initialize a client for data_injection. if (sInjectEventQueue == null) { try { sInjectEventQueue = new InjectEventQueue( - mMainLooper, this, mContext.getPackageName()); + mMainLooper, this, mode, mContext.getPackageName()); } catch (RuntimeException e) { Log.e(TAG, "Cannot create InjectEventQueue: " + e); } @@ -421,6 +444,12 @@ public class SystemSensorManager extends SensorManager { Log.e(TAG, "Data injection mode not activated before calling injectSensorData"); return false; } + if (sInjectEventQueue.getDataInjectionMode() != HAL_BYPASS_REPLAY_DATA_INJECTION + && !sensor.isDataInjectionSupported()) { + // DI mode and Replay DI mode require support from the sensor HAL + // HAL Bypass mode doesn't require this. + throw new IllegalArgumentException("sensor does not support data injection"); + } int ret = sInjectEventQueue.injectSensorData(sensor.getHandle(), values, accuracy, timestamp); // If there are any errors in data injection clean up the native resources. @@ -825,6 +854,8 @@ public class SystemSensorManager extends SensorManager { protected static final int OPERATING_MODE_NORMAL = 0; protected static final int OPERATING_MODE_DATA_INJECTION = 1; + protected static final int OPERATING_MODE_REPLAY_DATA_INJECTION = 3; + protected static final int OPERATING_MODE_HAL_BYPASS_REPLAY_DATA_INJECTION = 4; BaseEventQueue(Looper looper, SystemSensorManager manager, int mode, String packageName) { if (packageName == null) packageName = ""; @@ -1134,8 +1165,12 @@ public class SystemSensorManager extends SensorManager { } final class InjectEventQueue extends BaseEventQueue { - public InjectEventQueue(Looper looper, SystemSensorManager manager, String packageName) { - super(looper, manager, OPERATING_MODE_DATA_INJECTION, packageName); + + private int mMode; + public InjectEventQueue(Looper looper, SystemSensorManager manager, + @DataInjectionMode int mode, String packageName) { + super(looper, manager, mode, packageName); + mMode = mode; } int injectSensorData(int handle, float[] values, int accuracy, long timestamp) { @@ -1161,6 +1196,10 @@ public class SystemSensorManager extends SensorManager { protected void removeSensorEvent(Sensor sensor) { } + + int getDataInjectionMode() { + return mMode; + } } protected boolean setOperationParameterImpl(SensorAdditionalInfo parameter) { diff --git a/core/java/android/hardware/face/FaceManager.java b/core/java/android/hardware/face/FaceManager.java index 02212968cdb0..02304b5ba4f3 100644 --- a/core/java/android/hardware/face/FaceManager.java +++ b/core/java/android/hardware/face/FaceManager.java @@ -1074,6 +1074,14 @@ public class FaceManager implements BiometricAuthenticator, BiometricFaceConstan */ public interface FaceDetectionCallback { void onFaceDetected(int sensorId, int userId, boolean isStrongBiometric); + + /** + * An error has occurred with face detection. + * + * This callback signifies that this operation has been completed, and + * no more callbacks should be expected. + */ + default void onDetectionError(int errorMsgId) {} } /** @@ -1373,6 +1381,9 @@ public class FaceManager implements BiometricAuthenticator, BiometricFaceConstan } else if (mRemovalCallback != null) { mRemovalCallback.onRemovalError(mRemovalFace, clientErrMsgId, getErrorString(mContext, errMsgId, vendorCode)); + } else if (mFaceDetectionCallback != null) { + mFaceDetectionCallback.onDetectionError(errMsgId); + mFaceDetectionCallback = null; } } diff --git a/core/java/android/hardware/fingerprint/FingerprintManager.java b/core/java/android/hardware/fingerprint/FingerprintManager.java index 5bfda70f03de..935157a48a45 100644 --- a/core/java/android/hardware/fingerprint/FingerprintManager.java +++ b/core/java/android/hardware/fingerprint/FingerprintManager.java @@ -462,6 +462,14 @@ public class FingerprintManager implements BiometricAuthenticator, BiometricFing * Invoked when a fingerprint has been detected. */ void onFingerprintDetected(int sensorId, int userId, boolean isStrongBiometric); + + /** + * An error has occurred with fingerprint detection. + * + * This callback signifies that this operation has been completed, and + * no more callbacks should be expected. + */ + default void onDetectionError(int errorMsgId) {} } /** @@ -1484,6 +1492,9 @@ public class FingerprintManager implements BiometricAuthenticator, BiometricFing ? mRemoveTracker.mSingleFingerprint : null; mRemovalCallback.onRemovalError(fp, clientErrMsgId, getErrorString(mContext, errMsgId, vendorCode)); + } else if (mFingerprintDetectionCallback != null) { + mFingerprintDetectionCallback.onDetectionError(errMsgId); + mFingerprintDetectionCallback = null; } } diff --git a/core/java/android/hardware/input/InputDeviceSensorManager.java b/core/java/android/hardware/input/InputDeviceSensorManager.java index aa55e543dd48..05024ea95eda 100644 --- a/core/java/android/hardware/input/InputDeviceSensorManager.java +++ b/core/java/android/hardware/input/InputDeviceSensorManager.java @@ -644,7 +644,7 @@ public class InputDeviceSensorManager implements InputManager.InputDeviceListene } @Override - protected boolean initDataInjectionImpl(boolean enable) { + protected boolean initDataInjectionImpl(boolean enable, int mode) { return false; } diff --git a/core/java/android/os/flags.aconfig b/core/java/android/os/flags.aconfig index 7fceda4a4393..b7f2e065bac8 100644 --- a/core/java/android/os/flags.aconfig +++ b/core/java/android/os/flags.aconfig @@ -16,7 +16,7 @@ flag { flag { name: "remove_app_profiler_pss_collection" - namespace: "power_optimization" + namespace: "backstage_power" description: "Replaces background PSS collection in AppProfiler with RSS" bug: "297542292" } diff --git a/core/java/android/permission/flags.aconfig b/core/java/android/permission/flags.aconfig index d60d4c6372fe..3f06a91f6e5b 100644 --- a/core/java/android/permission/flags.aconfig +++ b/core/java/android/permission/flags.aconfig @@ -28,3 +28,10 @@ flag { description: "enable AttributionSource.setNextAttributionSource" bug: "304478648" } + +flag { + name: "should_register_attribution_source" + namespace: "permissions" + description: "enable the shouldRegisterAttributionSource API" + bug: "305057691" +} diff --git a/core/java/android/service/voice/VoiceInteractionService.java b/core/java/android/service/voice/VoiceInteractionService.java index 3f41c56ac7f1..d2806217a276 100644 --- a/core/java/android/service/voice/VoiceInteractionService.java +++ b/core/java/android/service/voice/VoiceInteractionService.java @@ -520,7 +520,7 @@ public class VoiceInteractionService extends Service { @NonNull String keyphrase, @SuppressLint("UseIcu") @NonNull Locale locale, @NonNull @CallbackExecutor Executor executor, @NonNull AlwaysOnHotwordDetector.Callback callback) { - // TODO (b/269080850): Resolve AndroidFrameworkRequiresPermission lint warning + // TODO(b/269080850): Resolve AndroidFrameworkRequiresPermission lint warning Objects.requireNonNull(keyphrase); Objects.requireNonNull(locale); @@ -546,6 +546,10 @@ public class VoiceInteractionService extends Service { @NonNull SoundTrigger.ModuleProperties moduleProperties, @NonNull @CallbackExecutor Executor executor, @NonNull AlwaysOnHotwordDetector.Callback callback) { + // TODO(b/305787465): Remove the MANAGE_HOTWORD_DETECTION permission enforcement on the + // {@link #createAlwaysOnHotwordDetectorForTest(String, Locale, + // SoundTrigger.ModuleProperties, AlwaysOnHotwordDetector.Callback)} and replace with the + // permission RECEIVE_SANDBOX_TRIGGER_AUDIO when it is fully launched. Objects.requireNonNull(keyphrase); Objects.requireNonNull(locale); @@ -612,6 +616,11 @@ public class VoiceInteractionService extends Service { @Nullable PersistableBundle options, @Nullable SharedMemory sharedMemory, @SuppressLint("MissingNullability") AlwaysOnHotwordDetector.Callback callback) { + // TODO(b/305787465): Remove the MANAGE_HOTWORD_DETECTION permission enforcement on the + // {@link #createAlwaysOnHotwordDetector(String, Locale, PersistableBundle, SharedMemory, + // AlwaysOnHotwordDetector.Callback)} and replace with the permission + // RECEIVE_SANDBOX_TRIGGER_AUDIO when it is fully launched. + return createAlwaysOnHotwordDetectorInternal(keyphrase, locale, /* supportHotwordDetectionService= */ true, options, sharedMemory, /* modulProperties */ null, /* executor= */ null, callback); @@ -663,7 +672,11 @@ public class VoiceInteractionService extends Service { @Nullable PersistableBundle options, @Nullable SharedMemory sharedMemory, @NonNull @CallbackExecutor Executor executor, @NonNull AlwaysOnHotwordDetector.Callback callback) { - // TODO (b/269080850): Resolve AndroidFrameworkRequiresPermission lint warning + // TODO(b/269080850): Resolve AndroidFrameworkRequiresPermission lint warning + // TODO(b/305787465): Remove the MANAGE_HOTWORD_DETECTION permission enforcement on the + // {@link #createAlwaysOnHotwordDetector(String, Locale, PersistableBundle, SharedMemory, + // Executor, AlwaysOnHotwordDetector.Callback)} and replace with the permission + // RECEIVE_SANDBOX_TRIGGER_AUDIO when it is fully launched. Objects.requireNonNull(keyphrase); Objects.requireNonNull(locale); @@ -690,6 +703,10 @@ public class VoiceInteractionService extends Service { @NonNull SoundTrigger.ModuleProperties moduleProperties, @NonNull @CallbackExecutor Executor executor, @NonNull AlwaysOnHotwordDetector.Callback callback) { + // TODO(b/305787465): Remove the MANAGE_HOTWORD_DETECTION permission enforcement on the + // {@link #createAlwaysOnHotwordDetectorForTest(String, Locale, PersistableBundle, + // SharedMemory, SoundTrigger.ModuleProperties, Executor, AlwaysOnHotwordDetector.Callback)} + // and replace with the permission RECEIVE_SANDBOX_TRIGGER_AUDIO when it is fully launched. Objects.requireNonNull(keyphrase); Objects.requireNonNull(locale); diff --git a/core/java/android/util/FeatureFlagUtils.java b/core/java/android/util/FeatureFlagUtils.java index 2906d86f803d..766e924c994e 100644 --- a/core/java/android/util/FeatureFlagUtils.java +++ b/core/java/android/util/FeatureFlagUtils.java @@ -200,6 +200,12 @@ public class FeatureFlagUtils { public static final String SETTINGS_REMOTE_DEVICE_CREDENTIAL_VALIDATION = "settings_remote_device_credential_validation"; + /** + * Flag to enable/disable to start treating any calls to "suspend" an app as "quarantine". + * @hide + */ + public static final String SETTINGS_TREAT_PAUSE_AS_QUARANTINE = + "settings_treat_pause_as_quarantine"; private static final Map<String, String> DEFAULT_FLAGS; @@ -247,6 +253,7 @@ public class FeatureFlagUtils { DEFAULT_FLAGS.put(SETTINGS_BIOMETRICS2_FINGERPRINT_SETTINGS, "false"); // TODO: b/298454866 Replace with Trunk Stable Feature Flag DEFAULT_FLAGS.put(SETTINGS_REMOTEAUTH_ENROLLMENT_SETTINGS, "false"); + DEFAULT_FLAGS.put(SETTINGS_TREAT_PAUSE_AS_QUARANTINE, "false"); } private static final Set<String> PERSISTENT_FLAGS; @@ -264,6 +271,7 @@ public class FeatureFlagUtils { PERSISTENT_FLAGS.add(SETTINGS_ENABLE_SPA); PERSISTENT_FLAGS.add(SETTINGS_ENABLE_SPA_PHASE2); PERSISTENT_FLAGS.add(SETTINGS_PREFER_ACCESSIBILITY_MENU_IN_SYSTEM); + PERSISTENT_FLAGS.add(SETTINGS_TREAT_PAUSE_AS_QUARANTINE); } /** diff --git a/core/java/android/view/ContentRecordingSession.java b/core/java/android/view/ContentRecordingSession.java index a89f79540c65..dc41b70d1683 100644 --- a/core/java/android/view/ContentRecordingSession.java +++ b/core/java/android/view/ContentRecordingSession.java @@ -52,6 +52,12 @@ public final class ContentRecordingSession implements Parcelable { */ public static final int RECORD_CONTENT_TASK = 1; + /** Full screen sharing (app is not selected). */ + public static final int TARGET_UID_FULL_SCREEN = -1; + + /** Can't report (e.g. side loaded app). */ + public static final int TARGET_UID_UNKNOWN = -2; + /** * Unique logical identifier of the {@link android.hardware.display.VirtualDisplay} that has * recorded content rendered to its surface. @@ -89,27 +95,36 @@ public final class ContentRecordingSession implements Parcelable { */ private boolean mWaitingForConsent = false; + /** UID of the package that is captured if selected. */ + private int mTargetUid = TARGET_UID_UNKNOWN; + /** * Default instance, with recording the display. */ private ContentRecordingSession() { } - /** - * Returns an instance initialized for recording the indicated display. - */ + /** Returns an instance initialized for recording the indicated display. */ public static ContentRecordingSession createDisplaySession(int displayToMirror) { - return new ContentRecordingSession().setDisplayToRecord(displayToMirror) - .setContentToRecord(RECORD_CONTENT_DISPLAY); + return new ContentRecordingSession() + .setDisplayToRecord(displayToMirror) + .setContentToRecord(RECORD_CONTENT_DISPLAY) + .setTargetUid(TARGET_UID_FULL_SCREEN); } - /** - * Returns an instance initialized for task recording. - */ + /** Returns an instance initialized for task recording. */ public static ContentRecordingSession createTaskSession( @NonNull IBinder taskWindowContainerToken) { - return new ContentRecordingSession().setContentToRecord(RECORD_CONTENT_TASK) - .setTokenToRecord(taskWindowContainerToken); + return createTaskSession(taskWindowContainerToken, TARGET_UID_UNKNOWN); + } + + /** Returns an instance initialized for task recording. */ + public static ContentRecordingSession createTaskSession( + @NonNull IBinder taskWindowContainerToken, int targetUid) { + return new ContentRecordingSession() + .setContentToRecord(RECORD_CONTENT_TASK) + .setTokenToRecord(taskWindowContainerToken) + .setTargetUid(targetUid); } /** @@ -175,13 +190,33 @@ public final class ContentRecordingSession implements Parcelable { } } + @IntDef(prefix = "TARGET_UID_", value = { + TARGET_UID_FULL_SCREEN, + TARGET_UID_UNKNOWN + }) + @Retention(RetentionPolicy.SOURCE) + @DataClass.Generated.Member + public @interface TargetUid {} + + @DataClass.Generated.Member + public static String targetUidToString(@TargetUid int value) { + switch (value) { + case TARGET_UID_FULL_SCREEN: + return "TARGET_UID_FULL_SCREEN"; + case TARGET_UID_UNKNOWN: + return "TARGET_UID_UNKNOWN"; + default: return Integer.toHexString(value); + } + } + @DataClass.Generated.Member /* package-private */ ContentRecordingSession( int virtualDisplayId, @RecordContent int contentToRecord, int displayToRecord, @Nullable IBinder tokenToRecord, - boolean waitingForConsent) { + boolean waitingForConsent, + int targetUid) { this.mVirtualDisplayId = virtualDisplayId; this.mContentToRecord = contentToRecord; @@ -196,6 +231,7 @@ public final class ContentRecordingSession implements Parcelable { this.mDisplayToRecord = displayToRecord; this.mTokenToRecord = tokenToRecord; this.mWaitingForConsent = waitingForConsent; + this.mTargetUid = targetUid; // onConstructed(); // You can define this method to get a callback } @@ -251,6 +287,14 @@ public final class ContentRecordingSession implements Parcelable { } /** + * UID of the package that is captured if selected. + */ + @DataClass.Generated.Member + public int getTargetUid() { + return mTargetUid; + } + + /** * Unique logical identifier of the {@link android.hardware.display.VirtualDisplay} that has * recorded content rendered to its surface. */ @@ -314,6 +358,15 @@ public final class ContentRecordingSession implements Parcelable { return this; } + /** + * UID of the package that is captured if selected. + */ + @DataClass.Generated.Member + public @NonNull ContentRecordingSession setTargetUid( int value) { + mTargetUid = value; + return this; + } + @Override @DataClass.Generated.Member public String toString() { @@ -325,7 +378,8 @@ public final class ContentRecordingSession implements Parcelable { "contentToRecord = " + recordContentToString(mContentToRecord) + ", " + "displayToRecord = " + mDisplayToRecord + ", " + "tokenToRecord = " + mTokenToRecord + ", " + - "waitingForConsent = " + mWaitingForConsent + + "waitingForConsent = " + mWaitingForConsent + ", " + + "targetUid = " + mTargetUid + " }"; } @@ -346,7 +400,8 @@ public final class ContentRecordingSession implements Parcelable { && mContentToRecord == that.mContentToRecord && mDisplayToRecord == that.mDisplayToRecord && java.util.Objects.equals(mTokenToRecord, that.mTokenToRecord) - && mWaitingForConsent == that.mWaitingForConsent; + && mWaitingForConsent == that.mWaitingForConsent + && mTargetUid == that.mTargetUid; } @Override @@ -361,6 +416,7 @@ public final class ContentRecordingSession implements Parcelable { _hash = 31 * _hash + mDisplayToRecord; _hash = 31 * _hash + java.util.Objects.hashCode(mTokenToRecord); _hash = 31 * _hash + Boolean.hashCode(mWaitingForConsent); + _hash = 31 * _hash + mTargetUid; return _hash; } @@ -378,6 +434,7 @@ public final class ContentRecordingSession implements Parcelable { dest.writeInt(mContentToRecord); dest.writeInt(mDisplayToRecord); if (mTokenToRecord != null) dest.writeStrongBinder(mTokenToRecord); + dest.writeInt(mTargetUid); } @Override @@ -397,6 +454,7 @@ public final class ContentRecordingSession implements Parcelable { int contentToRecord = in.readInt(); int displayToRecord = in.readInt(); IBinder tokenToRecord = (flg & 0x8) == 0 ? null : (IBinder) in.readStrongBinder(); + int targetUid = in.readInt(); this.mVirtualDisplayId = virtualDisplayId; this.mContentToRecord = contentToRecord; @@ -412,6 +470,7 @@ public final class ContentRecordingSession implements Parcelable { this.mDisplayToRecord = displayToRecord; this.mTokenToRecord = tokenToRecord; this.mWaitingForConsent = waitingForConsent; + this.mTargetUid = targetUid; // onConstructed(); // You can define this method to get a callback } @@ -442,6 +501,7 @@ public final class ContentRecordingSession implements Parcelable { private int mDisplayToRecord; private @Nullable IBinder mTokenToRecord; private boolean mWaitingForConsent; + private int mTargetUid; private long mBuilderFieldsSet = 0L; @@ -513,10 +573,21 @@ public final class ContentRecordingSession implements Parcelable { return this; } + /** + * UID of the package that is captured if selected. + */ + @DataClass.Generated.Member + public @NonNull Builder setTargetUid(int value) { + checkNotUsed(); + mBuilderFieldsSet |= 0x20; + mTargetUid = value; + return this; + } + /** Builds the instance. This builder should not be touched after calling this! */ public @NonNull ContentRecordingSession build() { checkNotUsed(); - mBuilderFieldsSet |= 0x20; // Mark builder used + mBuilderFieldsSet |= 0x40; // Mark builder used if ((mBuilderFieldsSet & 0x1) == 0) { mVirtualDisplayId = INVALID_DISPLAY; @@ -533,17 +604,21 @@ public final class ContentRecordingSession implements Parcelable { if ((mBuilderFieldsSet & 0x10) == 0) { mWaitingForConsent = false; } + if ((mBuilderFieldsSet & 0x20) == 0) { + mTargetUid = TARGET_UID_UNKNOWN; + } ContentRecordingSession o = new ContentRecordingSession( mVirtualDisplayId, mContentToRecord, mDisplayToRecord, mTokenToRecord, - mWaitingForConsent); + mWaitingForConsent, + mTargetUid); return o; } private void checkNotUsed() { - if ((mBuilderFieldsSet & 0x20) != 0) { + if ((mBuilderFieldsSet & 0x40) != 0) { throw new IllegalStateException( "This Builder should not be reused. Use a new Builder instance instead"); } @@ -551,10 +626,10 @@ public final class ContentRecordingSession implements Parcelable { } @DataClass.Generated( - time = 1683628463074L, + time = 1697456140720L, codegenVersion = "1.0.23", sourceFile = "frameworks/base/core/java/android/view/ContentRecordingSession.java", - inputSignatures = "public static final int RECORD_CONTENT_DISPLAY\npublic static final int RECORD_CONTENT_TASK\nprivate int mVirtualDisplayId\nprivate @android.view.ContentRecordingSession.RecordContent int mContentToRecord\nprivate int mDisplayToRecord\nprivate @android.annotation.Nullable android.os.IBinder mTokenToRecord\nprivate boolean mWaitingForConsent\npublic static android.view.ContentRecordingSession createDisplaySession(int)\npublic static android.view.ContentRecordingSession createTaskSession(android.os.IBinder)\npublic static boolean isValid(android.view.ContentRecordingSession)\npublic static boolean isProjectionOnSameDisplay(android.view.ContentRecordingSession,android.view.ContentRecordingSession)\nclass ContentRecordingSession extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genConstructor=false, genToString=true, genSetters=true, genEqualsHashCode=true)") + inputSignatures = "public static final int RECORD_CONTENT_DISPLAY\npublic static final int RECORD_CONTENT_TASK\npublic static final int TARGET_UID_FULL_SCREEN\npublic static final int TARGET_UID_UNKNOWN\nprivate int mVirtualDisplayId\nprivate @android.view.ContentRecordingSession.RecordContent int mContentToRecord\nprivate int mDisplayToRecord\nprivate @android.annotation.Nullable android.os.IBinder mTokenToRecord\nprivate boolean mWaitingForConsent\nprivate int mTargetUid\npublic static android.view.ContentRecordingSession createDisplaySession(int)\npublic static android.view.ContentRecordingSession createTaskSession(android.os.IBinder)\npublic static android.view.ContentRecordingSession createTaskSession(android.os.IBinder,int)\npublic static boolean isValid(android.view.ContentRecordingSession)\npublic static boolean isProjectionOnSameDisplay(android.view.ContentRecordingSession,android.view.ContentRecordingSession)\nclass ContentRecordingSession extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genConstructor=false, genToString=true, genSetters=true, genEqualsHashCode=true)") @Deprecated private void __metadata() {} diff --git a/core/java/android/view/SurfaceControlRegistry.java b/core/java/android/view/SurfaceControlRegistry.java index 67ac811287cb..0f35048cac67 100644 --- a/core/java/android/view/SurfaceControlRegistry.java +++ b/core/java/android/view/SurfaceControlRegistry.java @@ -78,6 +78,11 @@ public class SurfaceControlRegistry { for (int i = 0; i < size; i++) { final Map.Entry<SurfaceControl, Long> entry = entries.get(i); final SurfaceControl sc = entry.getKey(); + if (sc == null) { + // Just skip if the key has since been removed from the weak hash map + continue; + } + final long timeRegistered = entry.getValue(); pw.print(" "); pw.print(sc.getName()); diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java index e9d0e4c557c6..49b16c7697c9 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -19,6 +19,8 @@ package android.view; import static android.content.res.Resources.ID_NULL; import static android.os.Trace.TRACE_TAG_APP; import static android.view.ContentInfo.SOURCE_DRAG_AND_DROP; +import static android.view.Surface.FRAME_RATE_CATEGORY_LOW; +import static android.view.Surface.FRAME_RATE_CATEGORY_NORMAL; import static android.view.accessibility.AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED; import static android.view.displayhash.DisplayHashResultCallback.DISPLAY_HASH_ERROR_INVALID_BOUNDS; import static android.view.displayhash.DisplayHashResultCallback.DISPLAY_HASH_ERROR_MISSING_WINDOW; @@ -27,6 +29,7 @@ import static android.view.displayhash.DisplayHashResultCallback.DISPLAY_HASH_ER import static android.view.displayhash.DisplayHashResultCallback.EXTRA_DISPLAY_HASH; import static android.view.displayhash.DisplayHashResultCallback.EXTRA_DISPLAY_HASH_ERROR_CODE; import static android.view.flags.Flags.FLAG_VIEW_VELOCITY_API; +import static android.view.flags.Flags.toolkitSetFrameRate; import static android.view.flags.Flags.viewVelocityApi; import static com.android.internal.util.FrameworkStatsLog.TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__DEEP_PRESS; @@ -114,6 +117,7 @@ import android.sysprop.DisplayProperties; import android.text.InputType; import android.text.TextUtils; import android.util.AttributeSet; +import android.util.DisplayMetrics; import android.util.FloatProperty; import android.util.LayoutDirection; import android.util.Log; @@ -5509,6 +5513,11 @@ public class View implements Drawable.Callback, KeyEvent.Callback, private ViewTranslationResponse mViewTranslationResponse; /** + * A threshold value to determine the frame rate category of the View based on the size. + */ + private static final float FRAME_RATE_SIZE_PERCENTAGE_THRESHOLD = 0.07f; + + /** * Simple constructor to use when creating a view from code. * * @param context The Context the view is running in, through which it can @@ -20183,6 +20192,9 @@ public class View implements Drawable.Callback, KeyEvent.Callback, return; } + // For VRR to vote the preferred frame rate + votePreferredFrameRate(); + // Reset content capture caches mPrivateFlags4 &= ~PFLAG4_CONTENT_CAPTURE_IMPORTANCE_MASK; mContentCaptureSessionCached = false; @@ -20285,6 +20297,8 @@ public class View implements Drawable.Callback, KeyEvent.Callback, */ protected void damageInParent() { if (mParent != null && mAttachInfo != null) { + // For VRR to vote the preferred frame rate + votePreferredFrameRate(); mParent.onDescendantInvalidated(this, this); } } @@ -32981,6 +32995,40 @@ public class View implements Drawable.Callback, KeyEvent.Callback, return null; } + private float getSizePercentage() { + if (mResources == null || getAlpha() == 0 || getVisibility() != VISIBLE) { + return 0; + } + + DisplayMetrics displayMetrics = mResources.getDisplayMetrics(); + int screenSize = displayMetrics.widthPixels + * displayMetrics.heightPixels; + int viewSize = getWidth() * getHeight(); + + if (screenSize == 0 || viewSize == 0) { + return 0f; + } + return (float) viewSize / screenSize; + } + + private int calculateFrameRateCategory() { + float sizePercentage = getSizePercentage(); + + if (sizePercentage <= FRAME_RATE_SIZE_PERCENTAGE_THRESHOLD) { + return FRAME_RATE_CATEGORY_LOW; + } else { + return FRAME_RATE_CATEGORY_NORMAL; + } + } + + private void votePreferredFrameRate() { + // use toolkitSetFrameRate flag to gate the change + ViewRootImpl viewRootImpl = getViewRootImpl(); + if (toolkitSetFrameRate() && viewRootImpl != null && getSizePercentage() > 0) { + viewRootImpl.votePreferredFrameRateCategory(calculateFrameRateCategory()); + } + } + /** * Set the current velocity of the View, we only track positive value. * We will use the velocity information to adjust the frame rate when applicable. diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index 939278314df4..9d15c78a6e9e 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -24,6 +24,8 @@ import static android.view.Display.DEFAULT_DISPLAY; 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_NO_PREFERENCE; import static android.view.View.PFLAG_DRAW_ANIMATION; import static android.view.View.SYSTEM_UI_FLAG_FULLSCREEN; import static android.view.View.SYSTEM_UI_FLAG_HIDE_NAVIGATION; @@ -74,7 +76,10 @@ import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_LAYOUT_SIZE_E import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_OPTIMIZE_MEASURE; import static android.view.WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE; import static android.view.WindowManager.LayoutParams.SOFT_INPUT_MASK_ADJUST; +import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION; import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_STARTING; +import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION; +import static android.view.WindowManager.LayoutParams.TYPE_DRAWN_APPLICATION; import static android.view.WindowManager.LayoutParams.TYPE_INPUT_METHOD; import static android.view.WindowManager.LayoutParams.TYPE_STATUS_BAR_ADDITIONAL; import static android.view.WindowManager.LayoutParams.TYPE_SYSTEM_ALERT; @@ -87,6 +92,7 @@ import static android.view.WindowManagerGlobal.RELAYOUT_RES_SURFACE_CHANGED; import static android.view.accessibility.Flags.forceInvertColor; import static android.view.inputmethod.InputMethodEditorTraceProto.InputMethodClientsTraceProto.ClientSideProto.IME_FOCUS_CONTROLLER; import static android.view.inputmethod.InputMethodEditorTraceProto.InputMethodClientsTraceProto.ClientSideProto.INSETS_CONTROLLER; +import static android.view.flags.Flags.toolkitSetFrameRate; import android.Manifest; import android.accessibilityservice.AccessibilityService; @@ -732,6 +738,7 @@ public final class ViewRootImpl implements ViewParent, private SurfaceControl mBoundsLayer; private final SurfaceSession mSurfaceSession = new SurfaceSession(); private final Transaction mTransaction = new Transaction(); + private final Transaction mFrameRateTransaction = new Transaction(); @UnsupportedAppUsage boolean mAdded; @@ -955,6 +962,34 @@ public final class ViewRootImpl implements ViewParent, private AccessibilityWindowAttributes mAccessibilityWindowAttributes; + /* + * for Variable Refresh Rate project + */ + + // The preferred frame rate category of the view that + // could be updated on a frame-by-frame basis. + private int mPreferredFrameRateCategory = FRAME_RATE_CATEGORY_NO_PREFERENCE; + // The preferred frame rate category of the last frame that + // could be used to lower frame rate after touch boost + private int mLastPreferredFrameRateCategory = FRAME_RATE_CATEGORY_NO_PREFERENCE; + // The preferred frame rate of the view that is mainly used for + // touch boosting, view velocity handling, and TextureView. + private float mPreferredFrameRate = 0; + // 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. + private boolean mIsFrameRateBoosting = false; + // Used to check if there is a message in the message queue + // for idleness handling. + private boolean mHasIdledMessage = false; + // time for touch boost period. + private static final int FRAME_RATE_TOUCH_BOOST_TIME = 1500; + // time for checking idle status periodically. + private static final int FRAME_RATE_IDLENESS_CHECK_TIME_MILLIS = 500; + // time for revaluating the idle status before lowering the frame rate. + private static final int FRAME_RATE_IDLENESS_REEVALUATE_TIME = 500; + /** * A temporary object used so relayoutWindow can return the latest SyncSeqId * system. The SyncSeqId system was designed to work without synchronous relayout @@ -3918,6 +3953,12 @@ public final class ViewRootImpl implements ViewParent, mWmsRequestSyncGroupState = WMS_SYNC_NONE; } } + + // For the variable refresh rate project. + setPreferredFrameRate(mPreferredFrameRate); + setPreferredFrameRateCategory(mPreferredFrameRateCategory); + mLastPreferredFrameRateCategory = mPreferredFrameRateCategory; + mPreferredFrameRateCategory = FRAME_RATE_CATEGORY_NO_PREFERENCE; } private void createSyncIfNeeded() { @@ -5970,6 +6011,8 @@ public final class ViewRootImpl implements ViewParent, private static final int MSG_REPORT_KEEP_CLEAR_RECTS = 36; private static final int MSG_PAUSED_FOR_SYNC_TIMEOUT = 37; private static final int MSG_DECOR_VIEW_GESTURE_INTERCEPTION = 38; + private static final int MSG_TOUCH_BOOST_TIMEOUT = 39; + private static final int MSG_CHECK_INVALIDATION_IDLE = 40; final class ViewRootHandler extends Handler { @Override @@ -6265,6 +6308,32 @@ public final class ViewRootImpl implements ViewParent, mNumPausedForSync = 0; scheduleTraversals(); break; + case MSG_TOUCH_BOOST_TIMEOUT: + /** + * Lower the frame rate after the boosting period (FRAME_RATE_TOUCH_BOOST_TIME). + */ + mIsFrameRateBoosting = false; + setPreferredFrameRateCategory(Math.max(mPreferredFrameRateCategory, + mLastPreferredFrameRateCategory)); + break; + case MSG_CHECK_INVALIDATION_IDLE: + if (!mHasInvalidation && !mIsFrameRateBoosting) { + mPreferredFrameRateCategory = FRAME_RATE_CATEGORY_NO_PREFERENCE; + setPreferredFrameRateCategory(mPreferredFrameRateCategory); + mHasIdledMessage = false; + } else { + /** + * If there is no invalidation within a certain period, + * we consider the display is idled. + * We then set the frame rate catetogry to NO_PREFERENCE. + * Note that SurfaceFlinger also has a mechanism to lower the refresh rate + * if there is no updates of the buffer. + */ + mHasInvalidation = false; + mHandler.sendEmptyMessageDelayed(MSG_CHECK_INVALIDATION_IDLE, + FRAME_RATE_IDLENESS_REEVALUATE_TIME); + } + break; } } } @@ -7207,6 +7276,7 @@ public final class ViewRootImpl implements ViewParent, private int processPointerEvent(QueuedInputEvent q) { final MotionEvent event = (MotionEvent)q.mEvent; + final int action = event.getAction(); boolean handled = mHandwritingInitiator.onTouchEvent(event); if (handled) { // If handwriting is started, toolkit doesn't receive ACTION_UP. @@ -7227,6 +7297,22 @@ public final class ViewRootImpl implements ViewParent, scheduleConsumeBatchedInputImmediately(); } } + + // For the variable refresh rate project + if (handled && shouldTouchBoost(action, mWindowAttributes.type)) { + // set the frame rate to the maximum value. + mIsFrameRateBoosting = true; + setPreferredFrameRateCategory(mPreferredFrameRateCategory); + } + /** + * We want to lower the refresh rate when MotionEvent.ACTION_UP, + * MotionEvent.ACTION_CANCEL is detected. + * Not using ACTION_MOVE to avoid checking and sending messages too frequently. + */ + if (mIsFrameRateBoosting && (action == MotionEvent.ACTION_UP + || action == MotionEvent.ACTION_CANCEL)) { + sendDelayedEmptyMessage(MSG_TOUCH_BOOST_TIMEOUT, FRAME_RATE_TOUCH_BOOST_TIME); + } return handled ? FINISH_HANDLED : FORWARD; } @@ -11810,4 +11896,94 @@ public final class ViewRootImpl implements ViewParent, @NonNull Consumer<Boolean> listener) { t.clearTrustedPresentationCallback(getSurfaceControl()); } + + private void setPreferredFrameRateCategory(int preferredFrameRateCategory) { + if (!shouldSetFrameRateCategory()) { + return; + } + + int frameRateCategory = mIsFrameRateBoosting + ? FRAME_RATE_CATEGORY_HIGH : preferredFrameRateCategory; + + try { + mFrameRateTransaction.setFrameRateCategory(mSurfaceControl, + frameRateCategory, false).apply(); + } catch (Exception e) { + Log.e(mTag, "Unable to set frame rate category", e); + } + + if (mPreferredFrameRateCategory != FRAME_RATE_CATEGORY_NO_PREFERENCE && !mHasIdledMessage) { + // Check where the display is idled periodically. + // If so, set the frame rate category to NO_PREFERENCE + mHandler.sendEmptyMessageDelayed(MSG_CHECK_INVALIDATION_IDLE, + FRAME_RATE_IDLENESS_CHECK_TIME_MILLIS); + mHasIdledMessage = true; + } + } + + private void setPreferredFrameRate(float preferredFrameRate) { + if (!shouldSetFrameRate()) { + return; + } + + try { + mFrameRateTransaction.setFrameRate(mSurfaceControl, + preferredFrameRate, Surface.FRAME_RATE_COMPATIBILITY_DEFAULT).apply(); + } catch (Exception e) { + Log.e(mTag, "Unable to set frame rate", e); + } + } + + private void sendDelayedEmptyMessage(int message, int delayedTime) { + mHandler.removeMessages(message); + + mHandler.sendEmptyMessageDelayed(message, delayedTime); + } + + private boolean shouldSetFrameRateCategory() { + // use toolkitSetFrameRate flag to gate the change + return mSurface.isValid() && toolkitSetFrameRate(); + } + + private boolean shouldSetFrameRate() { + // use toolkitSetFrameRate flag to gate the change + return mPreferredFrameRate > 0 && toolkitSetFrameRate(); + } + + private boolean shouldTouchBoost(int motionEventAction, int windowType) { + boolean desiredAction = motionEventAction == MotionEvent.ACTION_DOWN + || motionEventAction == MotionEvent.ACTION_MOVE + || motionEventAction == MotionEvent.ACTION_UP; + boolean desiredType = windowType == TYPE_BASE_APPLICATION || windowType == TYPE_APPLICATION + || windowType == TYPE_APPLICATION_STARTING || windowType == TYPE_DRAWN_APPLICATION; + // use toolkitSetFrameRate flag to gate the change + return desiredAction && desiredType && toolkitSetFrameRate(); + } + + /** + * Allow Views to vote for the preferred frame rate category + * + * @param frameRateCategory the preferred frame rate category of a View + */ + @VisibleForTesting(visibility = VisibleForTesting.Visibility.PROTECTED) + public void votePreferredFrameRateCategory(int frameRateCategory) { + mPreferredFrameRateCategory = Math.max(mPreferredFrameRateCategory, frameRateCategory); + mHasInvalidation = true; + } + + /** + * Get the value of mPreferredFrameRateCategory + */ + @VisibleForTesting + public int getPreferredFrameRateCategory() { + return mPreferredFrameRateCategory; + } + + /** + * Get the value of mPreferredFrameRate + */ + @VisibleForTesting + public float getPreferredFrameRate() { + return mPreferredFrameRate; + } } diff --git a/core/java/android/view/contentcapture/MainContentCaptureSession.java b/core/java/android/view/contentcapture/MainContentCaptureSession.java index 2241fd5dc37a..b44d6a496058 100644 --- a/core/java/android/view/contentcapture/MainContentCaptureSession.java +++ b/core/java/android/view/contentcapture/MainContentCaptureSession.java @@ -317,16 +317,14 @@ public final class MainContentCaptureSession extends ContentCaptureSession { } } - // Should not be possible for mComponentName to be null here but check anyway - if (mManager.mOptions.contentProtectionOptions.enableReceiver - && mManager.getContentProtectionEventBuffer() != null - && mComponentName != null) { + if (isContentProtectionEnabled()) { mContentProtectionEventProcessor = new ContentProtectionEventProcessor( mManager.getContentProtectionEventBuffer(), mHandler, mSystemServerInterface, - mComponentName.getPackageName()); + mComponentName.getPackageName(), + mManager.mOptions.contentProtectionOptions); } else { mContentProtectionEventProcessor = null; } @@ -956,4 +954,15 @@ public final class MainContentCaptureSession extends ContentCaptureSession { private boolean isContentCaptureReceiverEnabled() { return mManager.mOptions.enableReceiver; } + + @UiThread + private boolean isContentProtectionEnabled() { + // Should not be possible for mComponentName to be null here but check anyway + // Should not be possible for groups to be empty if receiver is enabled but check anyway + return mManager.mOptions.contentProtectionOptions.enableReceiver + && mManager.getContentProtectionEventBuffer() != null + && mComponentName != null + && (!mManager.mOptions.contentProtectionOptions.requiredGroups.isEmpty() + || !mManager.mOptions.contentProtectionOptions.optionalGroups.isEmpty()); + } } diff --git a/core/java/android/view/contentprotection/ContentProtectionEventProcessor.java b/core/java/android/view/contentprotection/ContentProtectionEventProcessor.java index b44abf3eb04d..aaf90bd00535 100644 --- a/core/java/android/view/contentprotection/ContentProtectionEventProcessor.java +++ b/core/java/android/view/contentprotection/ContentProtectionEventProcessor.java @@ -19,9 +19,9 @@ package android.view.contentprotection; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.UiThread; +import android.content.ContentCaptureOptions; import android.content.pm.ParceledListSlice; import android.os.Handler; -import android.text.InputType; import android.util.Log; import android.view.contentcapture.ContentCaptureEvent; import android.view.contentcapture.IContentCaptureManager; @@ -33,10 +33,10 @@ import com.android.internal.util.RingBuffer; import java.time.Duration; import java.time.Instant; import java.util.Arrays; -import java.util.Collections; -import java.util.HashSet; +import java.util.Collection; import java.util.List; import java.util.Set; +import java.util.stream.Stream; /** * Main entry point for processing {@link ContentCaptureEvent} for the content protection flow. @@ -47,33 +47,13 @@ public class ContentProtectionEventProcessor { private static final String TAG = "ContentProtectionEventProcessor"; - private static final List<Integer> PASSWORD_FIELD_INPUT_TYPES = - Collections.unmodifiableList( - Arrays.asList( - InputType.TYPE_NUMBER_VARIATION_PASSWORD, - InputType.TYPE_TEXT_VARIATION_PASSWORD, - InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD, - InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD)); - - private static final List<String> PASSWORD_TEXTS = - Collections.unmodifiableList( - Arrays.asList("password", "pass word", "code", "pin", "credential")); - - private static final List<String> ADDITIONAL_SUSPICIOUS_TEXTS = - Collections.unmodifiableList( - Arrays.asList("user", "mail", "phone", "number", "login", "log in", "sign in")); - private static final Duration MIN_DURATION_BETWEEN_FLUSHING = Duration.ofSeconds(3); - private static final String ANDROID_CLASS_NAME_PREFIX = "android."; - private static final Set<Integer> EVENT_TYPES_TO_STORE = - Collections.unmodifiableSet( - new HashSet<>( - Arrays.asList( - ContentCaptureEvent.TYPE_VIEW_APPEARED, - ContentCaptureEvent.TYPE_VIEW_DISAPPEARED, - ContentCaptureEvent.TYPE_VIEW_TEXT_CHANGED))); + Set.of( + ContentCaptureEvent.TYPE_VIEW_APPEARED, + ContentCaptureEvent.TYPE_VIEW_DISAPPEARED, + ContentCaptureEvent.TYPE_VIEW_TEXT_CHANGED); private static final int RESET_LOGIN_TOTAL_EVENTS_TO_PROCESS = 150; @@ -85,11 +65,7 @@ public class ContentProtectionEventProcessor { @NonNull private final String mPackageName; - @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE) - public boolean mPasswordFieldDetected = false; - - @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE) - public boolean mSuspiciousTextDetected = false; + @NonNull private final ContentCaptureOptions.ContentProtectionOptions mOptions; @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE) @Nullable @@ -97,15 +73,32 @@ public class ContentProtectionEventProcessor { private int mResetLoginRemainingEventsToProcess; + private boolean mAnyGroupFound = false; + + // Ordered by priority + private final List<SearchGroup> mGroupsRequired; + + // Ordered by priority + private final List<SearchGroup> mGroupsOptional; + + // Ordered by priority + private final List<SearchGroup> mGroupsAll; + public ContentProtectionEventProcessor( @NonNull RingBuffer<ContentCaptureEvent> eventBuffer, @NonNull Handler handler, @NonNull IContentCaptureManager contentCaptureManager, - @NonNull String packageName) { + @NonNull String packageName, + @NonNull ContentCaptureOptions.ContentProtectionOptions options) { mEventBuffer = eventBuffer; mHandler = handler; mContentCaptureManager = contentCaptureManager; mPackageName = packageName; + mOptions = options; + mGroupsRequired = options.requiredGroups.stream().map(SearchGroup::new).toList(); + mGroupsOptional = options.optionalGroups.stream().map(SearchGroup::new).toList(); + mGroupsAll = + Stream.of(mGroupsRequired, mGroupsOptional).flatMap(Collection::stream).toList(); } /** Main entry point for {@link ContentCaptureEvent} processing. */ @@ -130,9 +123,31 @@ public class ContentProtectionEventProcessor { @UiThread private void processViewAppearedEvent(@NonNull ContentCaptureEvent event) { - mPasswordFieldDetected |= isPasswordField(event); - mSuspiciousTextDetected |= isSuspiciousText(event); - if (mPasswordFieldDetected && mSuspiciousTextDetected) { + ViewNode viewNode = event.getViewNode(); + String eventText = ContentProtectionUtils.getEventTextLower(event); + String viewNodeText = ContentProtectionUtils.getViewNodeTextLower(viewNode); + String hintText = ContentProtectionUtils.getHintTextLower(viewNode); + + mGroupsAll.stream() + .filter(group -> !group.mFound) + .filter( + group -> + group.matches(eventText) + || group.matches(viewNodeText) + || group.matches(hintText)) + .findFirst() + .ifPresent( + group -> { + group.mFound = true; + mAnyGroupFound = true; + }); + + boolean loginDetected = + mGroupsRequired.stream().allMatch(group -> group.mFound) + && mGroupsOptional.stream().filter(group -> group.mFound).count() + >= mOptions.optionalGroupsThreshold; + + if (loginDetected) { loginDetected(); } else { maybeResetLoginFlags(); @@ -150,14 +165,13 @@ public class ContentProtectionEventProcessor { @UiThread private void resetLoginFlags() { - mPasswordFieldDetected = false; - mSuspiciousTextDetected = false; - mResetLoginRemainingEventsToProcess = 0; + mGroupsAll.forEach(group -> group.mFound = false); + mAnyGroupFound = false; } @UiThread private void maybeResetLoginFlags() { - if (mPasswordFieldDetected || mSuspiciousTextDetected) { + if (mAnyGroupFound) { if (mResetLoginRemainingEventsToProcess <= 0) { mResetLoginRemainingEventsToProcess = RESET_LOGIN_TOTAL_EVENTS_TO_PROCESS; } else { @@ -194,61 +208,21 @@ public class ContentProtectionEventProcessor { } } - private boolean isPasswordField(@NonNull ContentCaptureEvent event) { - return isPasswordField(event.getViewNode()); - } - - private boolean isPasswordField(@Nullable ViewNode viewNode) { - if (viewNode == null) { - return false; - } - return isAndroidPasswordField(viewNode) || isWebViewPasswordField(viewNode); - } - - private boolean isAndroidPasswordField(@NonNull ViewNode viewNode) { - if (!isAndroidViewNode(viewNode)) { - return false; - } - int inputType = viewNode.getInputType(); - return PASSWORD_FIELD_INPUT_TYPES.stream() - .anyMatch(passwordInputType -> (inputType & passwordInputType) != 0); - } + private static final class SearchGroup { - private boolean isWebViewPasswordField(@NonNull ViewNode viewNode) { - if (viewNode.getClassName() != null) { - return false; - } - return isPasswordText(ContentProtectionUtils.getViewNodeText(viewNode)); - } + @NonNull private final List<String> mSearchStrings; - private boolean isAndroidViewNode(@NonNull ViewNode viewNode) { - String className = viewNode.getClassName(); - return className != null && className.startsWith(ANDROID_CLASS_NAME_PREFIX); - } - - private boolean isSuspiciousText(@NonNull ContentCaptureEvent event) { - return isSuspiciousText(ContentProtectionUtils.getEventText(event)) - || isSuspiciousText(ContentProtectionUtils.getViewNodeText(event)); - } + public boolean mFound = false; - private boolean isSuspiciousText(@Nullable String text) { - if (text == null) { - return false; + SearchGroup(@NonNull List<String> searchStrings) { + mSearchStrings = searchStrings; } - if (isPasswordText(text)) { - return true; - } - String lowerCaseText = text.toLowerCase(); - return ADDITIONAL_SUSPICIOUS_TEXTS.stream() - .anyMatch(suspiciousText -> lowerCaseText.contains(suspiciousText)); - } - private boolean isPasswordText(@Nullable String text) { - if (text == null) { - return false; + public boolean matches(@Nullable String text) { + if (text == null) { + return false; + } + return mSearchStrings.stream().anyMatch(text::contains); } - String lowerCaseText = text.toLowerCase(); - return PASSWORD_TEXTS.stream() - .anyMatch(passwordText -> lowerCaseText.contains(passwordText)); } } diff --git a/core/java/android/view/contentprotection/ContentProtectionUtils.java b/core/java/android/view/contentprotection/ContentProtectionUtils.java index 9abf6f10d05d..1ecac7f188ab 100644 --- a/core/java/android/view/contentprotection/ContentProtectionUtils.java +++ b/core/java/android/view/contentprotection/ContentProtectionUtils.java @@ -28,33 +28,39 @@ import android.view.contentcapture.ViewNode; */ public final class ContentProtectionUtils { - /** Returns the text extracted directly from the {@link ContentCaptureEvent}, if set. */ + /** Returns the lowercase text extracted from the {@link ContentCaptureEvent}, if set. */ @Nullable - public static String getEventText(@NonNull ContentCaptureEvent event) { + public static String getEventTextLower(@NonNull ContentCaptureEvent event) { CharSequence text = event.getText(); if (text == null) { return null; } - return text.toString(); + return text.toString().toLowerCase(); } - /** Returns the text extracted from the event's {@link ViewNode}, if set. */ + /** Returns the lowercase text extracted from the {@link ViewNode}, if set. */ @Nullable - public static String getViewNodeText(@NonNull ContentCaptureEvent event) { - ViewNode viewNode = event.getViewNode(); + public static String getViewNodeTextLower(@Nullable ViewNode viewNode) { if (viewNode == null) { return null; } - return getViewNodeText(viewNode); + CharSequence text = viewNode.getText(); + if (text == null) { + return null; + } + return text.toString().toLowerCase(); } - /** Returns the text extracted directly from the {@link ViewNode}, if set. */ + /** Returns the lowercase hint text extracted from the {@link ViewNode}, if set. */ @Nullable - public static String getViewNodeText(@NonNull ViewNode viewNode) { - CharSequence text = viewNode.getText(); + public static String getHintTextLower(@Nullable ViewNode viewNode) { + if (viewNode == null) { + return null; + } + String text = viewNode.getHint(); if (text == null) { return null; } - return text.toString(); + return text.toLowerCase(); } } diff --git a/core/java/android/window/TaskFragmentOperation.java b/core/java/android/window/TaskFragmentOperation.java index 43fa0be6c1b7..4e0f9a51c0a0 100644 --- a/core/java/android/window/TaskFragmentOperation.java +++ b/core/java/android/window/TaskFragmentOperation.java @@ -88,6 +88,26 @@ public final class TaskFragmentOperation implements Parcelable { */ public static final int OP_TYPE_SET_ISOLATED_NAVIGATION = 11; + /** + * Reorders the TaskFragment to be the bottom-most in the Task. Note that this op will bring the + * TaskFragment to the bottom of the Task below all the other Activities and TaskFragments. + * + * This is only allowed for system organizers. See + * {@link com.android.server.wm.TaskFragmentOrganizerController#registerOrganizer( + * ITaskFragmentOrganizer, boolean)} + */ + public static final int OP_TYPE_REORDER_TO_BOTTOM_OF_TASK = 12; + + /** + * Reorders the TaskFragment to be the top-most in the Task. Note that this op will bring the + * TaskFragment to the top of the Task above all the other Activities and TaskFragments. + * + * This is only allowed for system organizers. See + * {@link com.android.server.wm.TaskFragmentOrganizerController#registerOrganizer( + * ITaskFragmentOrganizer, boolean)} + */ + public static final int OP_TYPE_REORDER_TO_TOP_OF_TASK = 13; + @IntDef(prefix = { "OP_TYPE_" }, value = { OP_TYPE_UNKNOWN, OP_TYPE_CREATE_TASK_FRAGMENT, @@ -101,7 +121,9 @@ public final class TaskFragmentOperation implements Parcelable { OP_TYPE_SET_ANIMATION_PARAMS, OP_TYPE_SET_RELATIVE_BOUNDS, OP_TYPE_REORDER_TO_FRONT, - OP_TYPE_SET_ISOLATED_NAVIGATION + OP_TYPE_SET_ISOLATED_NAVIGATION, + OP_TYPE_REORDER_TO_BOTTOM_OF_TASK, + OP_TYPE_REORDER_TO_TOP_OF_TASK, }) @Retention(RetentionPolicy.SOURCE) public @interface OperationType {} diff --git a/core/java/android/window/flags/windowing_frontend.aconfig b/core/java/android/window/flags/windowing_frontend.aconfig index 392aa1b13e0e..6c025a47ff5a 100644 --- a/core/java/android/window/flags/windowing_frontend.aconfig +++ b/core/java/android/window/flags/windowing_frontend.aconfig @@ -20,4 +20,11 @@ flag { description: "Refactor dim to fix flickers" bug: "281632483,295291019" is_fixed_read_only: true -}
\ No newline at end of file +} + +flag { + name: "transit_ready_tracking" + namespace: "windowing_frontend" + description: "Enable accurate transition readiness tracking" + bug: "294925498" +} diff --git a/core/java/com/android/internal/policy/WearGestureInterceptionDetector.java b/core/java/com/android/internal/policy/WearGestureInterceptionDetector.java index 6fd50180e78b..504928cd4347 100644 --- a/core/java/com/android/internal/policy/WearGestureInterceptionDetector.java +++ b/core/java/com/android/internal/policy/WearGestureInterceptionDetector.java @@ -74,16 +74,15 @@ public class WearGestureInterceptionDetector { return windowSwipeToDismiss; } - private boolean isPointerIndexValid(MotionEvent ev) { + private int getIndexForValidPointer(MotionEvent ev) { int pointerIndex = ev.findPointerIndex(mActivePointerId); if (pointerIndex == -1) { if (DEBUG) { Log.e(TAG, "Invalid pointer index: ignoring."); } mDiscardIntercept = true; - return false; } - return true; + return pointerIndex; } private void updateSwiping(MotionEvent ev) { @@ -98,7 +97,7 @@ public class WearGestureInterceptionDetector { } } - private void updateDiscardIntercept(MotionEvent ev) { + private void updateDiscardIntercept(MotionEvent ev, int pointerIndex) { if (!mSwiping) { // Don't look at canScroll until we have passed the touch slop return; @@ -107,8 +106,8 @@ public class WearGestureInterceptionDetector { return; } final boolean checkLeft = mDownX < ev.getRawX(); - final float x = ev.getX(mActivePointerId); - final float y = ev.getY(mActivePointerId); + final float x = ev.getX(pointerIndex); + final float y = ev.getY(pointerIndex); if (canScroll(mInstalledDecorView, false, checkLeft, x, y)) { mDiscardIntercept = true; } @@ -152,11 +151,12 @@ public class WearGestureInterceptionDetector { if (mDiscardIntercept) { break; } - if (!isPointerIndexValid(ev)) { + final int pointerIndex = getIndexForValidPointer(ev); + if (pointerIndex == -1) { break; } updateSwiping(ev); - updateDiscardIntercept(ev); + updateDiscardIntercept(ev, pointerIndex); break; case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_UP: diff --git a/core/jni/android_hardware_SensorManager.cpp b/core/jni/android_hardware_SensorManager.cpp index deb138fda867..9c883d18a9ae 100644 --- a/core/jni/android_hardware_SensorManager.cpp +++ b/core/jni/android_hardware_SensorManager.cpp @@ -265,6 +265,18 @@ static jboolean nativeIsDataInjectionEnabled(JNIEnv *_env, jclass _this, jlong s return mgr->isDataInjectionEnabled(); } +static jboolean nativeIsReplayDataInjectionEnabled(JNIEnv *_env, jclass _this, + jlong sensorManager) { + SensorManager *mgr = reinterpret_cast<SensorManager *>(sensorManager); + return mgr->isReplayDataInjectionEnabled(); +} + +static jboolean nativeIsHalBypassReplayDataInjectionEnabled(JNIEnv *_env, jclass _this, + jlong sensorManager) { + SensorManager *mgr = reinterpret_cast<SensorManager *>(sensorManager); + return mgr->isHalBypassReplayDataInjectionEnabled(); +} + static jint nativeCreateDirectChannel(JNIEnv *_env, jclass _this, jlong sensorManager, jint deviceId, jlong size, jint channelType, jint fd, jobject hardwareBufferObj) { @@ -533,6 +545,11 @@ static const JNINativeMethod gSystemSensorManagerMethods[] = { {"nativeIsDataInjectionEnabled", "(J)Z", (void *)nativeIsDataInjectionEnabled}, + {"nativeIsReplayDataInjectionEnabled", "(J)Z", (void *)nativeIsReplayDataInjectionEnabled}, + + {"nativeIsHalBypassReplayDataInjectionEnabled", "(J)Z", + (void *)nativeIsHalBypassReplayDataInjectionEnabled}, + {"nativeCreateDirectChannel", "(JIJIILandroid/hardware/HardwareBuffer;)I", (void *)nativeCreateDirectChannel}, diff --git a/core/proto/android/service/package.proto b/core/proto/android/service/package.proto index eb14db070f9f..068f4dd07ccb 100644 --- a/core/proto/android/service/package.proto +++ b/core/proto/android/service/package.proto @@ -115,6 +115,9 @@ message PackageProto { // Only set if the app defined a monochrome icon. optional string monochrome_icon_bitmap_path = 3; + + // The component name of the original activity (pre-archival). + optional string original_component_name = 4; } /** Information about main activities. */ diff --git a/core/res/Android.bp b/core/res/Android.bp index b71995f899c5..4e686b7ad80c 100644 --- a/core/res/Android.bp +++ b/core/res/Android.bp @@ -104,6 +104,7 @@ genrule { android_app { name: "framework-res", + use_resource_processor: false, sdk_version: "core_platform", certificate: "platform", diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index 88b578ba772b..cffbaa75ab76 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -6123,7 +6123,7 @@ <uses-permission android:name="android.permission.PACKAGE_USAGE_STATS" /> <!-- @SystemApi @hide - @FlaggedApi("backstage_power.report_usage_stats_permission") + @FlaggedApi("android.app.usage.report_usage_stats_permission") Allows trusted system components to report events to UsageStatsManager --> <permission android:name="android.permission.REPORT_USAGE_STATS" android:protectionLevel="signature|module" /> diff --git a/core/tests/coretests/Android.bp b/core/tests/coretests/Android.bp index 2993a0e63228..445ddf52bf1c 100644 --- a/core/tests/coretests/Android.bp +++ b/core/tests/coretests/Android.bp @@ -65,6 +65,7 @@ android_test { "device-time-shell-utils", "testables", "com.android.text.flags-aconfig-java", + "flag-junit", ], libs: [ @@ -75,6 +76,7 @@ android_test { "framework", "ext", "framework-res", + "android.view.flags-aconfig-java", ], jni_libs: [ "libpowermanagertest_jni", diff --git a/core/tests/coretests/src/android/hardware/face/FaceManagerTest.java b/core/tests/coretests/src/android/hardware/face/FaceManagerTest.java index 07dec5d9e222..b843ad75ac0f 100644 --- a/core/tests/coretests/src/android/hardware/face/FaceManagerTest.java +++ b/core/tests/coretests/src/android/hardware/face/FaceManagerTest.java @@ -79,6 +79,8 @@ public class FaceManagerTest { private FaceManager.AuthenticationCallback mAuthCallback; @Mock private FaceManager.EnrollmentCallback mEnrollmentCallback; + @Mock + private FaceManager.FaceDetectionCallback mFaceDetectionCallback; @Captor private ArgumentCaptor<IFaceAuthenticatorsRegisteredCallback> mCaptor; @@ -191,6 +193,23 @@ public class FaceManagerTest { any(), anyString(), any(), any(), anyBoolean()); } + @Test + public void detectClient_onError() throws RemoteException { + ArgumentCaptor<IFaceServiceReceiver> argumentCaptor = + ArgumentCaptor.forClass(IFaceServiceReceiver.class); + + CancellationSignal cancellationSignal = new CancellationSignal(); + mFaceManager.detectFace(cancellationSignal, mFaceDetectionCallback, + new FaceAuthenticateOptions.Builder().build()); + + verify(mService).detectFace(any(), argumentCaptor.capture(), any()); + + argumentCaptor.getValue().onError(5 /* error */, 0 /* vendorCode */); + mLooper.dispatchAll(); + + verify(mFaceDetectionCallback).onDetectionError(anyInt()); + } + private void initializeProperties() throws RemoteException { verify(mService).addAuthenticatorsRegisteredCallback(mCaptor.capture()); diff --git a/core/tests/coretests/src/android/hardware/fingerprint/FingerprintManagerTest.java b/core/tests/coretests/src/android/hardware/fingerprint/FingerprintManagerTest.java index 625e2e3723a7..70313b8c9ea7 100644 --- a/core/tests/coretests/src/android/hardware/fingerprint/FingerprintManagerTest.java +++ b/core/tests/coretests/src/android/hardware/fingerprint/FingerprintManagerTest.java @@ -74,6 +74,8 @@ public class FingerprintManagerTest { private FingerprintManager.AuthenticationCallback mAuthCallback; @Mock private FingerprintManager.EnrollmentCallback mEnrollCallback; + @Mock + private FingerprintManager.FingerprintDetectionCallback mFingerprintDetectionCallback; @Captor private ArgumentCaptor<IFingerprintAuthenticatorsRegisteredCallback> mCaptor; @@ -166,4 +168,21 @@ public class FingerprintManagerTest { anyString()); verify(mService, never()).enroll(any(), any(), anyInt(), any(), anyString(), anyInt()); } + + @Test + public void detectClient_onError() throws RemoteException { + ArgumentCaptor<IFingerprintServiceReceiver> argumentCaptor = + ArgumentCaptor.forClass(IFingerprintServiceReceiver.class); + + mFingerprintManager.detectFingerprint(new CancellationSignal(), + mFingerprintDetectionCallback, + new FingerprintAuthenticateOptions.Builder().build()); + + verify(mService).detectFingerprint(any(), argumentCaptor.capture(), any()); + + argumentCaptor.getValue().onError(5 /* error */, 0 /* vendorCode */); + mLooper.dispatchAll(); + + verify(mFingerprintDetectionCallback).onDetectionError(anyInt()); + } } diff --git a/core/tests/coretests/src/android/view/ViewRootImplTest.java b/core/tests/coretests/src/android/view/ViewRootImplTest.java index 6a9fc04230f8..1a38decae604 100644 --- a/core/tests/coretests/src/android/view/ViewRootImplTest.java +++ b/core/tests/coretests/src/android/view/ViewRootImplTest.java @@ -17,6 +17,11 @@ package android.view; import static android.view.accessibility.Flags.FLAG_FORCE_INVERT_COLOR; +import static android.view.flags.Flags.FLAG_TOOLKIT_SET_FRAME_RATE; +import static android.view.Surface.FRAME_RATE_CATEGORY_HIGH; +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; import static android.view.View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY; import static android.view.View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN; import static android.view.View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION; @@ -48,8 +53,12 @@ import android.hardware.display.DisplayManagerGlobal; import android.os.Binder; import android.os.SystemProperties; import android.platform.test.annotations.Presubmit; +import android.platform.test.annotations.RequiresFlagsEnabled; +import android.platform.test.flag.junit.CheckFlagsRule; +import android.platform.test.flag.junit.DeviceFlagsValueProvider; import android.platform.test.flag.junit.SetFlagsRule; import android.provider.Settings; +import android.util.DisplayMetrics; import android.util.Log; import android.view.WindowInsets.Side; import android.view.WindowInsets.Type; @@ -97,6 +106,10 @@ public class ViewRootImplTest { // state after the test completes. private static boolean sOriginalTouchMode; + @Rule + public final CheckFlagsRule mCheckFlagsRule = + DeviceFlagsValueProvider.createCheckFlagsRule(); + @BeforeClass public static void setUpClass() { sContext = sInstrumentation.getTargetContext(); @@ -427,6 +440,129 @@ public class ViewRootImplTest { assertThat(result).isFalse(); } + /** + * Test the default values are properly set + */ + @UiThreadTest + @Test + @RequiresFlagsEnabled(FLAG_TOOLKIT_SET_FRAME_RATE) + public void votePreferredFrameRate_getDefaultValues() { + ViewRootImpl viewRootImpl = new ViewRootImpl(sContext, + sContext.getDisplayNoVerify()); + assertEquals(viewRootImpl.getPreferredFrameRateCategory(), + FRAME_RATE_CATEGORY_NO_PREFERENCE); + assertEquals(viewRootImpl.getPreferredFrameRate(), 0, 0.1); + } + + /** + * Test the value of the frame rate cateogry based on the visibility of a view + * Invsible: FRAME_RATE_CATEGORY_NO_PREFERENCE + * Visible: FRAME_RATE_CATEGORY_NORMAL + */ + @Test + @RequiresFlagsEnabled(FLAG_TOOLKIT_SET_FRAME_RATE) + public void votePreferredFrameRate_voteFrameRateCategory_visibility() { + View view = new View(sContext); + attachViewToWindow(view); + ViewRootImpl viewRootImpl = view.getViewRootImpl(); + sInstrumentation.runOnMainSync(() -> { + view.setVisibility(View.INVISIBLE); + view.invalidate(); + assertEquals(viewRootImpl.getPreferredFrameRateCategory(), + FRAME_RATE_CATEGORY_NO_PREFERENCE); + }); + + sInstrumentation.runOnMainSync(() -> { + view.setVisibility(View.VISIBLE); + view.invalidate(); + assertEquals(viewRootImpl.getPreferredFrameRateCategory(), + FRAME_RATE_CATEGORY_NORMAL); + }); + } + + /** + * Test the value of the frame rate cateogry based on the size of a view. + * The current threshold value is 7% of the screen size + * <7%: FRAME_RATE_CATEGORY_LOW + */ + @Test + @RequiresFlagsEnabled(FLAG_TOOLKIT_SET_FRAME_RATE) + public void votePreferredFrameRate_voteFrameRateCategory_smallSize() { + View view = new View(sContext); + WindowManager.LayoutParams wmlp = new WindowManager.LayoutParams(TYPE_APPLICATION_OVERLAY); + wmlp.token = new Binder(); // Set a fake token to bypass 'is your activity running' check + wmlp.width = 1; + wmlp.height = 1; + + sInstrumentation.runOnMainSync(() -> { + WindowManager wm = sContext.getSystemService(WindowManager.class); + wm.addView(view, wmlp); + }); + sInstrumentation.waitForIdleSync(); + + ViewRootImpl viewRootImpl = view.getViewRootImpl(); + sInstrumentation.runOnMainSync(() -> { + view.invalidate(); + assertEquals(viewRootImpl.getPreferredFrameRateCategory(), FRAME_RATE_CATEGORY_LOW); + }); + } + + /** + * Test the value of the frame rate cateogry based on the size of a view. + * The current threshold value is 7% of the screen size + * >=7% : FRAME_RATE_CATEGORY_NORMAL + */ + @Test + @RequiresFlagsEnabled(FLAG_TOOLKIT_SET_FRAME_RATE) + public void votePreferredFrameRate_voteFrameRateCategory_normalSize() { + View view = new View(sContext); + WindowManager.LayoutParams wmlp = new WindowManager.LayoutParams(TYPE_APPLICATION_OVERLAY); + wmlp.token = new Binder(); // Set a fake token to bypass 'is your activity running' check + + sInstrumentation.runOnMainSync(() -> { + WindowManager wm = sContext.getSystemService(WindowManager.class); + Display display = wm.getDefaultDisplay(); + DisplayMetrics metrics = new DisplayMetrics(); + display.getMetrics(metrics); + wmlp.width = (int) (metrics.widthPixels * 0.9); + wmlp.height = (int) (metrics.heightPixels * 0.9); + wm.addView(view, wmlp); + }); + sInstrumentation.waitForIdleSync(); + + ViewRootImpl viewRootImpl = view.getViewRootImpl(); + sInstrumentation.runOnMainSync(() -> { + view.invalidate(); + assertEquals(viewRootImpl.getPreferredFrameRateCategory(), FRAME_RATE_CATEGORY_NORMAL); + }); + } + + /** + * Test how values of the frame rate cateogry are aggregated. + * It should take the max value among all of the voted categories per frame. + */ + @Test + @RequiresFlagsEnabled(FLAG_TOOLKIT_SET_FRAME_RATE) + public void votePreferredFrameRate_voteFrameRateCategory_aggregate() { + View view = new View(sContext); + attachViewToWindow(view); + sInstrumentation.runOnMainSync(() -> { + ViewRootImpl viewRootImpl = view.getViewRootImpl(); + assertEquals(viewRootImpl.getPreferredFrameRateCategory(), + FRAME_RATE_CATEGORY_NO_PREFERENCE); + viewRootImpl.votePreferredFrameRateCategory(FRAME_RATE_CATEGORY_LOW); + 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); + 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); + assertEquals(viewRootImpl.getPreferredFrameRateCategory(), FRAME_RATE_CATEGORY_HIGH); + }); + } + @Test public void forceInvertOffDarkThemeOff_forceDarkModeDisabled() { mSetFlagsRule.enableFlags(FLAG_FORCE_INVERT_COLOR); diff --git a/core/tests/coretests/src/android/view/contentcapture/MainContentCaptureSessionTest.java b/core/tests/coretests/src/android/view/contentcapture/MainContentCaptureSessionTest.java index e76d266c614c..d47d7891d0e4 100644 --- a/core/tests/coretests/src/android/view/contentcapture/MainContentCaptureSessionTest.java +++ b/core/tests/coretests/src/android/view/contentcapture/MainContentCaptureSessionTest.java @@ -115,6 +115,26 @@ public class MainContentCaptureSessionTest { new ContentCaptureOptions.ContentProtectionOptions( /* enableReceiver= */ true, -BUFFER_SIZE, + /* requiredGroups= */ List.of(List.of("a")), + /* optionalGroups= */ Collections.emptyList(), + /* optionalGroupsThreshold= */ 0)); + MainContentCaptureSession session = createSession(options); + session.mContentProtectionEventProcessor = mMockContentProtectionEventProcessor; + + session.onSessionStarted(/* resultCode= */ 0, /* binder= */ null); + + assertThat(session.mContentProtectionEventProcessor).isNull(); + verifyZeroInteractions(mMockContentProtectionEventProcessor); + } + + @Test + public void onSessionStarted_contentProtectionNoGroups_processorNotCreated() { + ContentCaptureOptions options = + createOptions( + /* enableContentCaptureReceiver= */ true, + new ContentCaptureOptions.ContentProtectionOptions( + /* enableReceiver= */ true, + BUFFER_SIZE, /* requiredGroups= */ Collections.emptyList(), /* optionalGroups= */ Collections.emptyList(), /* optionalGroupsThreshold= */ 0)); @@ -320,7 +340,7 @@ public class MainContentCaptureSessionTest { new ContentCaptureOptions.ContentProtectionOptions( enableContentProtectionReceiver, BUFFER_SIZE, - /* requiredGroups= */ Collections.emptyList(), + /* requiredGroups= */ List.of(List.of("a")), /* optionalGroups= */ Collections.emptyList(), /* optionalGroupsThreshold= */ 0)); } diff --git a/core/tests/coretests/src/android/view/contentprotection/ContentProtectionEventProcessorTest.java b/core/tests/coretests/src/android/view/contentprotection/ContentProtectionEventProcessorTest.java index 39a2e0e048e8..ba0dbf454ad2 100644 --- a/core/tests/coretests/src/android/view/contentprotection/ContentProtectionEventProcessorTest.java +++ b/core/tests/coretests/src/android/view/contentprotection/ContentProtectionEventProcessorTest.java @@ -29,12 +29,13 @@ import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.verifyZeroInteractions; import static org.mockito.Mockito.when; +import android.annotation.NonNull; import android.annotation.Nullable; +import android.content.ContentCaptureOptions; import android.content.Context; import android.content.pm.ParceledListSlice; import android.os.Handler; -import android.os.Looper; -import android.text.InputType; +import android.os.test.TestLooper; import android.view.View; import android.view.contentcapture.ContentCaptureEvent; import android.view.contentcapture.IContentCaptureManager; @@ -57,6 +58,7 @@ import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; +import org.mockito.verification.VerificationMode; import java.time.Instant; import java.util.ArrayList; @@ -75,13 +77,25 @@ public class ContentProtectionEventProcessorTest { private static final String PACKAGE_NAME = "com.test.package.name"; - private static final String ANDROID_CLASS_NAME = "android.test.some.class.name"; + private static final String TEXT_REQUIRED1 = "TEXT REQUIRED1 TEXT"; - private static final String PASSWORD_TEXT = "ENTER PASSWORD HERE"; + private static final String TEXT_REQUIRED2 = "TEXT REQUIRED2 TEXT"; - private static final String SUSPICIOUS_TEXT = "PLEASE SIGN IN"; + private static final String TEXT_OPTIONAL1 = "TEXT OPTIONAL1 TEXT"; - private static final String SAFE_TEXT = "SAFE TEXT"; + private static final String TEXT_OPTIONAL2 = "TEXT OPTIONAL2 TEXT"; + + private static final String TEXT_CONTAINS_OPTIONAL3 = "TEXTOPTIONAL3TEXT"; + + private static final String TEXT_SHARED = "TEXT SHARED TEXT"; + + private static final String TEXT_SAFE = "TEXT SAFE TEXT"; + + private static final List<List<String>> REQUIRED_GROUPS = + List.of(List.of("required1", "missing"), List.of("required2", "shared")); + + private static final List<List<String>> OPTIONAL_GROUPS = + List.of(List.of("optional1"), List.of("optional2", "optional3"), List.of("shared")); private static final ContentCaptureEvent PROCESS_EVENT = createProcessEvent(); @@ -91,7 +105,17 @@ public class ContentProtectionEventProcessorTest { private static final Set<Integer> EVENT_TYPES_TO_STORE = ImmutableSet.of(TYPE_VIEW_APPEARED, TYPE_VIEW_DISAPPEARED, TYPE_VIEW_TEXT_CHANGED); - private static final int RESET_LOGIN_TOTAL_EVENTS_TO_PROCESS = 150; + private static final int BUFFER_SIZE = 150; + + private static final int OPTIONAL_GROUPS_THRESHOLD = 1; + + private static final ContentCaptureOptions.ContentProtectionOptions OPTIONS = + new ContentCaptureOptions.ContentProtectionOptions( + /* enableReceiver= */ true, + BUFFER_SIZE, + REQUIRED_GROUPS, + OPTIONAL_GROUPS, + OPTIONAL_GROUPS_THRESHOLD); @Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule(); @@ -101,16 +125,19 @@ public class ContentProtectionEventProcessorTest { private final Context mContext = ApplicationProvider.getApplicationContext(); - private ContentProtectionEventProcessor mContentProtectionEventProcessor; + private final TestLooper mTestLooper = new TestLooper(); + + @NonNull private ContentProtectionEventProcessor mContentProtectionEventProcessor; @Before public void setup() { mContentProtectionEventProcessor = new ContentProtectionEventProcessor( mMockEventBuffer, - new Handler(Looper.getMainLooper()), + new Handler(mTestLooper.getLooper()), mMockContentCaptureManager, - PACKAGE_NAME); + PACKAGE_NAME, + OPTIONS); } @Test @@ -156,347 +183,224 @@ public class ContentProtectionEventProcessorTest { } @Test - public void processEvent_loginDetected_inspectsOnlyTypeViewAppeared() { - mContentProtectionEventProcessor.mPasswordFieldDetected = true; - mContentProtectionEventProcessor.mSuspiciousTextDetected = true; - - for (int type = -100; type <= 100; type++) { - if (type == TYPE_VIEW_APPEARED) { - continue; - } - - mContentProtectionEventProcessor.processEvent(createEvent(type)); - - assertThat(mContentProtectionEventProcessor.mPasswordFieldDetected).isTrue(); - assertThat(mContentProtectionEventProcessor.mSuspiciousTextDetected).isTrue(); - } - - verify(mMockEventBuffer, never()).clear(); - verify(mMockEventBuffer, never()).toArray(); - verifyZeroInteractions(mMockContentCaptureManager); - } - - @Test - public void processEvent_loginDetected() throws Exception { + public void processEvent_loginDetected_true_eventText() throws Exception { when(mMockEventBuffer.toArray()).thenReturn(BUFFERED_EVENTS); - mContentProtectionEventProcessor.mPasswordFieldDetected = true; - mContentProtectionEventProcessor.mSuspiciousTextDetected = true; - mContentProtectionEventProcessor.processEvent(PROCESS_EVENT); - - assertThat(mContentProtectionEventProcessor.mPasswordFieldDetected).isFalse(); - assertThat(mContentProtectionEventProcessor.mSuspiciousTextDetected).isFalse(); - verify(mMockEventBuffer).clear(); - verify(mMockEventBuffer).toArray(); - assertOnLoginDetected(); - } - - @Test - public void processEvent_loginDetected_passwordFieldNotDetected() { - mContentProtectionEventProcessor.mSuspiciousTextDetected = true; - assertThat(mContentProtectionEventProcessor.mPasswordFieldDetected).isFalse(); - - mContentProtectionEventProcessor.processEvent(PROCESS_EVENT); - - assertThat(mContentProtectionEventProcessor.mPasswordFieldDetected).isFalse(); - assertThat(mContentProtectionEventProcessor.mSuspiciousTextDetected).isTrue(); - verify(mMockEventBuffer, never()).clear(); - verify(mMockEventBuffer, never()).toArray(); - verifyZeroInteractions(mMockContentCaptureManager); - } - - @Test - public void processEvent_loginDetected_suspiciousTextNotDetected() { - mContentProtectionEventProcessor.mPasswordFieldDetected = true; - assertThat(mContentProtectionEventProcessor.mSuspiciousTextDetected).isFalse(); - - mContentProtectionEventProcessor.processEvent(PROCESS_EVENT); - - assertThat(mContentProtectionEventProcessor.mPasswordFieldDetected).isTrue(); - assertThat(mContentProtectionEventProcessor.mSuspiciousTextDetected).isFalse(); - verify(mMockEventBuffer, never()).clear(); - verify(mMockEventBuffer, never()).toArray(); - verifyZeroInteractions(mMockContentCaptureManager); - } - - @Test - public void processEvent_loginDetected_withoutViewNode() { - assertThat(mContentProtectionEventProcessor.mPasswordFieldDetected).isFalse(); - assertThat(mContentProtectionEventProcessor.mSuspiciousTextDetected).isFalse(); - - mContentProtectionEventProcessor.processEvent(PROCESS_EVENT); + mContentProtectionEventProcessor.processEvent( + createProcessEvent( + /* eventText= */ TEXT_REQUIRED1, + /* viewNodeText= */ null, + /* hintText= */ null)); + mContentProtectionEventProcessor.processEvent( + createProcessEvent( + /* eventText= */ TEXT_REQUIRED2, + /* viewNodeText= */ null, + /* hintText= */ null)); + mContentProtectionEventProcessor.processEvent( + createProcessEvent( + /* eventText= */ TEXT_OPTIONAL1, + /* viewNodeText= */ null, + /* hintText= */ null)); - assertThat(mContentProtectionEventProcessor.mPasswordFieldDetected).isFalse(); - assertThat(mContentProtectionEventProcessor.mSuspiciousTextDetected).isFalse(); - verify(mMockEventBuffer, never()).clear(); - verify(mMockEventBuffer, never()).toArray(); - verifyZeroInteractions(mMockContentCaptureManager); + assertLoginDetected(); } @Test - public void processEvent_loginDetected_belowResetLimit() throws Exception { + public void processEvent_loginDetected_true_viewNodeText() throws Exception { when(mMockEventBuffer.toArray()).thenReturn(BUFFERED_EVENTS); - mContentProtectionEventProcessor.mSuspiciousTextDetected = true; - ContentCaptureEvent event = - createAndroidPasswordFieldEvent( - ANDROID_CLASS_NAME, InputType.TYPE_TEXT_VARIATION_PASSWORD); - - for (int i = 0; i < RESET_LOGIN_TOTAL_EVENTS_TO_PROCESS; i++) { - mContentProtectionEventProcessor.processEvent(PROCESS_EVENT); - } - assertThat(mContentProtectionEventProcessor.mPasswordFieldDetected).isFalse(); - assertThat(mContentProtectionEventProcessor.mSuspiciousTextDetected).isTrue(); - verify(mMockEventBuffer, never()).clear(); - verify(mMockEventBuffer, never()).toArray(); - - mContentProtectionEventProcessor.processEvent(event); + mContentProtectionEventProcessor.processEvent( + createProcessEvent( + /* eventText= */ null, + /* viewNodeText= */ TEXT_REQUIRED1, + /* hintText= */ null)); + mContentProtectionEventProcessor.processEvent( + createProcessEvent( + /* eventText= */ null, + /* viewNodeText= */ TEXT_REQUIRED2, + /* hintText= */ null)); + mContentProtectionEventProcessor.processEvent( + createProcessEvent( + /* eventText= */ null, + /* viewNodeText= */ TEXT_OPTIONAL1, + /* hintText= */ null)); - assertThat(mContentProtectionEventProcessor.mPasswordFieldDetected).isFalse(); - assertThat(mContentProtectionEventProcessor.mSuspiciousTextDetected).isFalse(); - verify(mMockEventBuffer).clear(); - verify(mMockEventBuffer).toArray(); - assertOnLoginDetected(); + assertLoginDetected(); } @Test - public void processEvent_loginDetected_aboveResetLimit() throws Exception { - mContentProtectionEventProcessor.mSuspiciousTextDetected = true; - ContentCaptureEvent event = - createAndroidPasswordFieldEvent( - ANDROID_CLASS_NAME, InputType.TYPE_TEXT_VARIATION_PASSWORD); - - for (int i = 0; i < RESET_LOGIN_TOTAL_EVENTS_TO_PROCESS + 1; i++) { - mContentProtectionEventProcessor.processEvent(PROCESS_EVENT); - } - - assertThat(mContentProtectionEventProcessor.mPasswordFieldDetected).isFalse(); - assertThat(mContentProtectionEventProcessor.mSuspiciousTextDetected).isFalse(); - verify(mMockEventBuffer, never()).clear(); - verify(mMockEventBuffer, never()).toArray(); + public void processEvent_loginDetected_true_hintText() throws Exception { + when(mMockEventBuffer.toArray()).thenReturn(BUFFERED_EVENTS); - mContentProtectionEventProcessor.processEvent(event); + mContentProtectionEventProcessor.processEvent( + createProcessEvent( + /* eventText= */ null, + /* viewNodeText= */ null, + /* hintText= */ TEXT_REQUIRED1)); + mContentProtectionEventProcessor.processEvent( + createProcessEvent( + /* eventText= */ null, + /* viewNodeText= */ null, + /* hintText= */ TEXT_REQUIRED2)); + mContentProtectionEventProcessor.processEvent( + createProcessEvent( + /* eventText= */ null, + /* viewNodeText= */ null, + /* hintText= */ TEXT_OPTIONAL1)); - assertThat(mContentProtectionEventProcessor.mPasswordFieldDetected).isTrue(); - assertThat(mContentProtectionEventProcessor.mSuspiciousTextDetected).isFalse(); - verify(mMockEventBuffer, never()).clear(); - verify(mMockEventBuffer, never()).toArray(); + assertLoginDetected(); } @Test - public void processEvent_multipleLoginsDetected_belowFlushThreshold() throws Exception { + public void processEvent_loginDetected_true_differentOptionalGroup() throws Exception { when(mMockEventBuffer.toArray()).thenReturn(BUFFERED_EVENTS); - mContentProtectionEventProcessor.mPasswordFieldDetected = true; - mContentProtectionEventProcessor.mSuspiciousTextDetected = true; - mContentProtectionEventProcessor.processEvent(PROCESS_EVENT); - - mContentProtectionEventProcessor.mPasswordFieldDetected = true; - mContentProtectionEventProcessor.mSuspiciousTextDetected = true; - mContentProtectionEventProcessor.processEvent(PROCESS_EVENT); + mContentProtectionEventProcessor.processEvent(createProcessEvent(TEXT_REQUIRED1)); + mContentProtectionEventProcessor.processEvent(createProcessEvent(TEXT_REQUIRED2)); + mContentProtectionEventProcessor.processEvent(createProcessEvent(TEXT_OPTIONAL2)); - assertThat(mContentProtectionEventProcessor.mPasswordFieldDetected).isFalse(); - assertThat(mContentProtectionEventProcessor.mSuspiciousTextDetected).isFalse(); - verify(mMockEventBuffer).clear(); - verify(mMockEventBuffer).toArray(); - assertOnLoginDetected(); + assertLoginDetected(); } @Test - public void processEvent_multipleLoginsDetected_aboveFlushThreshold() throws Exception { + public void processEvent_loginDetected_true_usesContains() throws Exception { when(mMockEventBuffer.toArray()).thenReturn(BUFFERED_EVENTS); - mContentProtectionEventProcessor.mPasswordFieldDetected = true; - mContentProtectionEventProcessor.mSuspiciousTextDetected = true; - mContentProtectionEventProcessor.processEvent(PROCESS_EVENT); - - mContentProtectionEventProcessor.mLastFlushTime = Instant.now().minusSeconds(5); + mContentProtectionEventProcessor.processEvent(createProcessEvent(TEXT_REQUIRED1)); + mContentProtectionEventProcessor.processEvent(createProcessEvent(TEXT_REQUIRED2)); + mContentProtectionEventProcessor.processEvent(createProcessEvent(TEXT_CONTAINS_OPTIONAL3)); - mContentProtectionEventProcessor.mPasswordFieldDetected = true; - mContentProtectionEventProcessor.mSuspiciousTextDetected = true; - mContentProtectionEventProcessor.processEvent(PROCESS_EVENT); - - assertThat(mContentProtectionEventProcessor.mPasswordFieldDetected).isFalse(); - assertThat(mContentProtectionEventProcessor.mSuspiciousTextDetected).isFalse(); - verify(mMockEventBuffer, times(2)).clear(); - verify(mMockEventBuffer, times(2)).toArray(); - assertOnLoginDetected(PROCESS_EVENT, /* times= */ 2); + assertLoginDetected(); } @Test - public void isPasswordField_android() { - ContentCaptureEvent event = - createAndroidPasswordFieldEvent( - ANDROID_CLASS_NAME, InputType.TYPE_TEXT_VARIATION_PASSWORD); - - mContentProtectionEventProcessor.processEvent(event); + public void processEvent_loginDetected_false_missingRequiredGroups() { + mContentProtectionEventProcessor.processEvent(createProcessEvent(TEXT_REQUIRED1)); + mContentProtectionEventProcessor.processEvent(createProcessEvent(TEXT_OPTIONAL1)); - assertThat(mContentProtectionEventProcessor.mPasswordFieldDetected).isTrue(); - verify(mMockEventBuffer, never()).clear(); - verify(mMockEventBuffer, never()).toArray(); - verifyZeroInteractions(mMockContentCaptureManager); + assertLoginNotDetected(); } @Test - public void isPasswordField_android_withoutClassName() { - ContentCaptureEvent event = - createAndroidPasswordFieldEvent( - /* className= */ null, InputType.TYPE_TEXT_VARIATION_PASSWORD); - - mContentProtectionEventProcessor.processEvent(event); + public void processEvent_loginDetected_false_missingOptionalGroups() { + mContentProtectionEventProcessor.processEvent(createProcessEvent(TEXT_REQUIRED1)); + mContentProtectionEventProcessor.processEvent(createProcessEvent(TEXT_REQUIRED2)); - assertThat(mContentProtectionEventProcessor.mPasswordFieldDetected).isFalse(); - verify(mMockEventBuffer, never()).clear(); - verify(mMockEventBuffer, never()).toArray(); - verifyZeroInteractions(mMockContentCaptureManager); + assertLoginNotDetected(); } @Test - public void isPasswordField_android_wrongClassName() { - ContentCaptureEvent event = - createAndroidPasswordFieldEvent( - "wrong.prefix" + ANDROID_CLASS_NAME, - InputType.TYPE_TEXT_VARIATION_PASSWORD); + public void processEvent_loginDetected_false_safeText() { + mContentProtectionEventProcessor.processEvent(createProcessEvent(TEXT_REQUIRED1)); + mContentProtectionEventProcessor.processEvent(createProcessEvent(TEXT_REQUIRED2)); + mContentProtectionEventProcessor.processEvent(createProcessEvent(TEXT_SAFE)); + mContentProtectionEventProcessor.processEvent(createProcessEvent(TEXT_SAFE)); + mContentProtectionEventProcessor.processEvent(createProcessEvent(TEXT_SAFE)); - mContentProtectionEventProcessor.processEvent(event); - - assertThat(mContentProtectionEventProcessor.mPasswordFieldDetected).isFalse(); - verify(mMockEventBuffer, never()).clear(); - verify(mMockEventBuffer, never()).toArray(); - verifyZeroInteractions(mMockContentCaptureManager); + assertLoginNotDetected(); } @Test - public void isPasswordField_android_wrongInputType() { - ContentCaptureEvent event = - createAndroidPasswordFieldEvent( - ANDROID_CLASS_NAME, InputType.TYPE_TEXT_VARIATION_NORMAL); + public void processEvent_loginDetected_false_sharedTextOnce() { + mContentProtectionEventProcessor.processEvent(createProcessEvent(TEXT_REQUIRED1)); + mContentProtectionEventProcessor.processEvent(createProcessEvent(TEXT_SHARED)); - mContentProtectionEventProcessor.processEvent(event); - - assertThat(mContentProtectionEventProcessor.mPasswordFieldDetected).isFalse(); - verify(mMockEventBuffer, never()).clear(); - verify(mMockEventBuffer, never()).toArray(); - verifyZeroInteractions(mMockContentCaptureManager); + assertLoginNotDetected(); } @Test - public void isPasswordField_webView() throws Exception { - ContentCaptureEvent event = - createWebViewPasswordFieldEvent( - /* className= */ null, /* eventText= */ null, PASSWORD_TEXT); - when(mMockEventBuffer.toArray()).thenReturn(new ContentCaptureEvent[] {event}); + public void processEvent_loginDetected_true_sharedTextMultiple() throws Exception { + when(mMockEventBuffer.toArray()).thenReturn(BUFFERED_EVENTS); - mContentProtectionEventProcessor.processEvent(event); + mContentProtectionEventProcessor.processEvent(createProcessEvent(TEXT_REQUIRED1)); + mContentProtectionEventProcessor.processEvent(createProcessEvent(TEXT_SHARED)); + mContentProtectionEventProcessor.processEvent(createProcessEvent(TEXT_SHARED)); - assertThat(mContentProtectionEventProcessor.mPasswordFieldDetected).isFalse(); - verify(mMockEventBuffer).clear(); - verify(mMockEventBuffer).toArray(); - assertOnLoginDetected(event, /* times= */ 1); + assertLoginDetected(); } @Test - public void isPasswordField_webView_withClassName() { - ContentCaptureEvent event = - createWebViewPasswordFieldEvent( - /* className= */ "any.class.name", /* eventText= */ null, PASSWORD_TEXT); + public void processEvent_loginDetected_false_inspectsOnlyTypeViewAppeared() { + mContentProtectionEventProcessor.processEvent(createProcessEvent(TEXT_REQUIRED1)); + mContentProtectionEventProcessor.processEvent(createProcessEvent(TEXT_REQUIRED2)); - mContentProtectionEventProcessor.processEvent(event); + for (int type = -100; type <= 100; type++) { + if (type == TYPE_VIEW_APPEARED) { + continue; + } + ContentCaptureEvent event = createEvent(type); + event.setText(TEXT_OPTIONAL1); + mContentProtectionEventProcessor.processEvent(event); + } - assertThat(mContentProtectionEventProcessor.mPasswordFieldDetected).isFalse(); - verify(mMockEventBuffer, never()).clear(); - verify(mMockEventBuffer, never()).toArray(); - verifyZeroInteractions(mMockContentCaptureManager); + assertLoginNotDetected(); } @Test - public void isPasswordField_webView_withSafeViewNodeText() { - ContentCaptureEvent event = - createWebViewPasswordFieldEvent( - /* className= */ null, /* eventText= */ null, SAFE_TEXT); + public void processEvent_loginDetected_true_belowResetLimit() throws Exception { + when(mMockEventBuffer.toArray()).thenReturn(BUFFERED_EVENTS); - mContentProtectionEventProcessor.processEvent(event); + mContentProtectionEventProcessor.processEvent(createProcessEvent(TEXT_REQUIRED1)); + mContentProtectionEventProcessor.processEvent(createProcessEvent(TEXT_REQUIRED2)); - assertThat(mContentProtectionEventProcessor.mPasswordFieldDetected).isFalse(); - verify(mMockEventBuffer, never()).clear(); - verify(mMockEventBuffer, never()).toArray(); - verifyZeroInteractions(mMockContentCaptureManager); - } + for (int i = 0; i < BUFFER_SIZE - 2; i++) { + mContentProtectionEventProcessor.processEvent(PROCESS_EVENT); + } - @Test - public void isPasswordField_webView_withEventText() { - ContentCaptureEvent event = - createWebViewPasswordFieldEvent(/* className= */ null, PASSWORD_TEXT, SAFE_TEXT); + assertLoginNotDetected(); - mContentProtectionEventProcessor.processEvent(event); + mContentProtectionEventProcessor.processEvent(createProcessEvent(TEXT_OPTIONAL1)); - assertThat(mContentProtectionEventProcessor.mPasswordFieldDetected).isFalse(); - verify(mMockEventBuffer, never()).clear(); - verify(mMockEventBuffer, never()).toArray(); - verifyZeroInteractions(mMockContentCaptureManager); + assertLoginDetected(); } @Test - public void isSuspiciousText_withSafeText() { - ContentCaptureEvent event = createSuspiciousTextEvent(SAFE_TEXT, SAFE_TEXT); + public void processEvent_loginDetected_false_aboveResetLimit() { + mContentProtectionEventProcessor.processEvent(createProcessEvent(TEXT_REQUIRED1)); + mContentProtectionEventProcessor.processEvent(createProcessEvent(TEXT_REQUIRED2)); - mContentProtectionEventProcessor.processEvent(event); - - assertThat(mContentProtectionEventProcessor.mSuspiciousTextDetected).isFalse(); - verify(mMockEventBuffer, never()).clear(); - verify(mMockEventBuffer, never()).toArray(); - verifyZeroInteractions(mMockContentCaptureManager); - } + for (int i = 0; i < BUFFER_SIZE - 1; i++) { + mContentProtectionEventProcessor.processEvent(PROCESS_EVENT); + } - @Test - public void isSuspiciousText_eventText_suspiciousText() { - ContentCaptureEvent event = createSuspiciousTextEvent(SUSPICIOUS_TEXT, SAFE_TEXT); + assertLoginNotDetected(); - mContentProtectionEventProcessor.processEvent(event); + mContentProtectionEventProcessor.processEvent(createProcessEvent(TEXT_OPTIONAL1)); - assertThat(mContentProtectionEventProcessor.mSuspiciousTextDetected).isTrue(); - verify(mMockEventBuffer, never()).clear(); - verify(mMockEventBuffer, never()).toArray(); - verifyZeroInteractions(mMockContentCaptureManager); + assertLoginNotDetected(); } @Test - public void isSuspiciousText_viewNodeText_suspiciousText() { - ContentCaptureEvent event = createSuspiciousTextEvent(SAFE_TEXT, SUSPICIOUS_TEXT); + public void processEvent_multipleLoginsDetected_belowFlushThreshold() throws Exception { + when(mMockEventBuffer.toArray()).thenReturn(BUFFERED_EVENTS); - mContentProtectionEventProcessor.processEvent(event); + for (int i = 0; i < 2; i++) { + mContentProtectionEventProcessor.processEvent(createProcessEvent(TEXT_REQUIRED1)); + mContentProtectionEventProcessor.processEvent(createProcessEvent(TEXT_REQUIRED2)); + mContentProtectionEventProcessor.processEvent(createProcessEvent(TEXT_OPTIONAL1)); + mContentProtectionEventProcessor.processEvent(PROCESS_EVENT); + } - assertThat(mContentProtectionEventProcessor.mSuspiciousTextDetected).isTrue(); - verify(mMockEventBuffer, never()).clear(); - verify(mMockEventBuffer, never()).toArray(); - verifyZeroInteractions(mMockContentCaptureManager); + assertLoginDetected(); } @Test - public void isSuspiciousText_eventText_passwordText() { - ContentCaptureEvent event = createSuspiciousTextEvent(PASSWORD_TEXT, SAFE_TEXT); - - mContentProtectionEventProcessor.processEvent(event); + public void processEvent_multipleLoginsDetected_aboveFlushThreshold() throws Exception { + when(mMockEventBuffer.toArray()).thenReturn(BUFFERED_EVENTS); - assertThat(mContentProtectionEventProcessor.mSuspiciousTextDetected).isTrue(); - verify(mMockEventBuffer, never()).clear(); - verify(mMockEventBuffer, never()).toArray(); - verifyZeroInteractions(mMockContentCaptureManager); - } + mContentProtectionEventProcessor.processEvent(createProcessEvent(TEXT_REQUIRED1)); + mContentProtectionEventProcessor.processEvent(createProcessEvent(TEXT_REQUIRED2)); + mContentProtectionEventProcessor.processEvent(createProcessEvent(TEXT_OPTIONAL1)); + mContentProtectionEventProcessor.processEvent(PROCESS_EVENT); - @Test - public void isSuspiciousText_viewNodeText_passwordText() { - // Specify the class to differ from {@link isPasswordField_webView} test in this version - ContentCaptureEvent event = - createProcessEvent( - "test.class.not.a.web.view", /* inputType= */ 0, SAFE_TEXT, PASSWORD_TEXT); + mContentProtectionEventProcessor.mLastFlushTime = Instant.now().minusSeconds(5); - mContentProtectionEventProcessor.processEvent(event); + mContentProtectionEventProcessor.processEvent(createProcessEvent(TEXT_REQUIRED1)); + mContentProtectionEventProcessor.processEvent(createProcessEvent(TEXT_REQUIRED2)); + mContentProtectionEventProcessor.processEvent(createProcessEvent(TEXT_OPTIONAL1)); + mContentProtectionEventProcessor.processEvent(PROCESS_EVENT); - assertThat(mContentProtectionEventProcessor.mSuspiciousTextDetected).isTrue(); - verify(mMockEventBuffer, never()).clear(); - verify(mMockEventBuffer, never()).toArray(); - verifyZeroInteractions(mMockContentCaptureManager); + assertLoginDetected(times(2)); } private static ContentCaptureEvent createEvent(int type) { @@ -511,20 +415,20 @@ public class ContentProtectionEventProcessorTest { return createEvent(TYPE_VIEW_APPEARED); } + private ContentCaptureEvent createProcessEvent(@Nullable String eventText) { + return createProcessEvent(eventText, /* viewNodeText= */ null, /* hintText= */ null); + } + private ContentCaptureEvent createProcessEvent( - @Nullable String className, - int inputType, - @Nullable String eventText, - @Nullable String viewNodeText) { + @Nullable String eventText, @Nullable String viewNodeText, @Nullable String hintText) { View view = new View(mContext); ViewStructureImpl viewStructure = new ViewStructureImpl(view); - if (className != null) { - viewStructure.setClassName(className); - } if (viewNodeText != null) { viewStructure.setText(viewNodeText); } - viewStructure.setInputType(inputType); + if (hintText != null) { + viewStructure.setHint(hintText); + } ContentCaptureEvent event = createProcessEvent(); event.setViewNode(viewStructure.getNode()); @@ -535,34 +439,28 @@ public class ContentProtectionEventProcessorTest { return event; } - private ContentCaptureEvent createAndroidPasswordFieldEvent( - @Nullable String className, int inputType) { - return createProcessEvent( - className, inputType, /* eventText= */ null, /* viewNodeText= */ null); - } - - private ContentCaptureEvent createWebViewPasswordFieldEvent( - @Nullable String className, @Nullable String eventText, @Nullable String viewNodeText) { - return createProcessEvent(className, /* inputType= */ 0, eventText, viewNodeText); + private void assertLoginNotDetected() { + mTestLooper.dispatchAll(); + verify(mMockEventBuffer, never()).clear(); + verify(mMockEventBuffer, never()).toArray(); + verifyZeroInteractions(mMockContentCaptureManager); } - private ContentCaptureEvent createSuspiciousTextEvent( - @Nullable String eventText, @Nullable String viewNodeText) { - return createProcessEvent( - /* className= */ null, /* inputType= */ 0, eventText, viewNodeText); + private void assertLoginDetected() throws Exception { + assertLoginDetected(times(1)); } - private void assertOnLoginDetected() throws Exception { - assertOnLoginDetected(PROCESS_EVENT, /* times= */ 1); - } + private void assertLoginDetected(@NonNull VerificationMode verificationMode) throws Exception { + mTestLooper.dispatchAll(); + verify(mMockEventBuffer, verificationMode).clear(); + verify(mMockEventBuffer, verificationMode).toArray(); - private void assertOnLoginDetected(ContentCaptureEvent event, int times) throws Exception { ArgumentCaptor<ParceledListSlice> captor = ArgumentCaptor.forClass(ParceledListSlice.class); - verify(mMockContentCaptureManager, times(times)).onLoginDetected(captor.capture()); + verify(mMockContentCaptureManager, verificationMode).onLoginDetected(captor.capture()); assertThat(captor.getValue()).isNotNull(); List<ContentCaptureEvent> actual = captor.getValue().getList(); assertThat(actual).isNotNull(); - assertThat(actual).containsExactly(event); + assertThat(actual).containsExactly(PROCESS_EVENT); } } diff --git a/core/tests/coretests/src/android/view/contentprotection/ContentProtectionUtilsTest.java b/core/tests/coretests/src/android/view/contentprotection/ContentProtectionUtilsTest.java index 1459799adee5..fbe478e31888 100644 --- a/core/tests/coretests/src/android/view/contentprotection/ContentProtectionUtilsTest.java +++ b/core/tests/coretests/src/android/view/contentprotection/ContentProtectionUtilsTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2013 The Android Open Source Project + * 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. @@ -44,68 +44,74 @@ public class ContentProtectionUtilsTest { private static final String TEXT = "TEST_TEXT"; - private static final ContentCaptureEvent EVENT = createEvent(); - - private static final ViewNode VIEW_NODE = new ViewNode(); - - private static final ViewNode VIEW_NODE_WITH_TEXT = createViewNodeWithText(); + private static final String TEXT_LOWER = TEXT.toLowerCase(); @Test - public void event_getEventText_null() { - String actual = ContentProtectionUtils.getEventText(EVENT); + public void getEventTextLower_null() { + String actual = ContentProtectionUtils.getEventTextLower(createEvent()); assertThat(actual).isNull(); } @Test - public void event_getEventText_notNull() { - ContentCaptureEvent event = createEvent(); - event.setText(TEXT); - - String actual = ContentProtectionUtils.getEventText(event); + public void getEventTextLower_notNull() { + String actual = ContentProtectionUtils.getEventTextLower(createEventWithText()); - assertThat(actual).isEqualTo(TEXT); + assertThat(actual).isEqualTo(TEXT_LOWER); } @Test - public void event_getViewNodeText_null() { - String actual = ContentProtectionUtils.getViewNodeText(EVENT); + public void getViewNodeTextLower_null() { + String actual = ContentProtectionUtils.getViewNodeTextLower(new ViewNode()); assertThat(actual).isNull(); } @Test - public void event_getViewNodeText_notNull() { - ContentCaptureEvent event = createEvent(); - event.setViewNode(VIEW_NODE_WITH_TEXT); - - String actual = ContentProtectionUtils.getViewNodeText(event); + public void getViewNodeTextLower_notNull() { + String actual = ContentProtectionUtils.getViewNodeTextLower(createViewNodeWithText()); - assertThat(actual).isEqualTo(TEXT); + assertThat(actual).isEqualTo(TEXT_LOWER); } @Test - public void viewNode_getViewNodeText_null() { - String actual = ContentProtectionUtils.getViewNodeText(VIEW_NODE); + public void getHintTextLower_null() { + String actual = ContentProtectionUtils.getHintTextLower(new ViewNode()); assertThat(actual).isNull(); } @Test - public void viewNode_getViewNodeText_notNull() { - String actual = ContentProtectionUtils.getViewNodeText(VIEW_NODE_WITH_TEXT); + public void getHintTextLower_notNull() { + String actual = ContentProtectionUtils.getHintTextLower(createViewNodeWithHint()); - assertThat(actual).isEqualTo(TEXT); + assertThat(actual).isEqualTo(TEXT_LOWER); } private static ContentCaptureEvent createEvent() { return new ContentCaptureEvent(/* sessionId= */ 123, TYPE_SESSION_STARTED); } - private static ViewNode createViewNodeWithText() { + private static ContentCaptureEvent createEventWithText() { + ContentCaptureEvent event = createEvent(); + event.setText(TEXT); + return event; + } + + private static ViewStructureImpl createViewStructureImpl() { View view = new View(ApplicationProvider.getApplicationContext()); - ViewStructureImpl viewStructure = new ViewStructureImpl(view); - viewStructure.setText(TEXT); - return viewStructure.getNode(); + return new ViewStructureImpl(view); + } + + private static ViewNode createViewNodeWithText() { + ViewStructureImpl viewStructureImpl = createViewStructureImpl(); + viewStructureImpl.setText(TEXT); + return viewStructureImpl.getNode(); + } + + private static ViewNode createViewNodeWithHint() { + ViewStructureImpl viewStructureImpl = createViewStructureImpl(); + viewStructureImpl.setHint(TEXT); + return viewStructureImpl.getNode(); } } diff --git a/core/tests/coretests/src/android/widget/HorizontalScrollViewFunctionalTest.java b/core/tests/coretests/src/android/widget/HorizontalScrollViewFunctionalTest.java index 86f26e59e370..df212ebe1744 100644 --- a/core/tests/coretests/src/android/widget/HorizontalScrollViewFunctionalTest.java +++ b/core/tests/coretests/src/android/widget/HorizontalScrollViewFunctionalTest.java @@ -17,7 +17,9 @@ package android.widget; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import android.content.Context; import android.platform.test.annotations.Presubmit; import android.util.PollingCheck; @@ -32,6 +34,9 @@ import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + @RunWith(AndroidJUnit4.class) @MediumTest @Presubmit @@ -49,23 +54,43 @@ public class HorizontalScrollViewFunctionalTest { } @Test - public void testScrollAfterFlingTop() { - mHorizontalScrollView.scrollTo(100, 0); - mHorizontalScrollView.fling(-10000); - PollingCheck.waitFor(() -> mHorizontalScrollView.mEdgeGlowLeft.getDistance() > 0); - PollingCheck.waitFor(() -> mHorizontalScrollView.mEdgeGlowLeft.getDistance() == 0f); + public void testScrollAfterFlingLeft() throws Throwable { + WatchedEdgeEffect edgeEffect = new WatchedEdgeEffect(mActivity); + mHorizontalScrollView.mEdgeGlowLeft = edgeEffect; + mActivityRule.runOnUiThread(() -> mHorizontalScrollView.scrollTo(100, 0)); + mActivityRule.runOnUiThread(() -> mHorizontalScrollView.fling(-10000)); + assertTrue(edgeEffect.onAbsorbLatch.await(1, TimeUnit.SECONDS)); + mActivityRule.runOnUiThread(() -> {}); // let the absorb takes effect -- least one frame + PollingCheck.waitFor(() -> edgeEffect.getDistance() == 0f); assertEquals(0, mHorizontalScrollView.getScrollX()); } @Test - public void testScrollAfterFlingBottom() { + public void testScrollAfterFlingRight() throws Throwable { + WatchedEdgeEffect edgeEffect = new WatchedEdgeEffect(mActivity); + mHorizontalScrollView.mEdgeGlowRight = edgeEffect; int childWidth = mHorizontalScrollView.getChildAt(0).getWidth(); int maxScroll = childWidth - mHorizontalScrollView.getWidth(); - mHorizontalScrollView.scrollTo(maxScroll - 100, 0); - mHorizontalScrollView.fling(10000); - PollingCheck.waitFor(() -> mHorizontalScrollView.mEdgeGlowRight.getDistance() > 0); + mActivityRule.runOnUiThread(() -> mHorizontalScrollView.scrollTo(maxScroll - 100, 0)); + mActivityRule.runOnUiThread(() -> mHorizontalScrollView.fling(10000)); + assertTrue(edgeEffect.onAbsorbLatch.await(1, TimeUnit.SECONDS)); + mActivityRule.runOnUiThread(() -> {}); // let the absorb takes effect -- at least one frame PollingCheck.waitFor(() -> mHorizontalScrollView.mEdgeGlowRight.getDistance() == 0f); assertEquals(maxScroll, mHorizontalScrollView.getScrollX()); } + + static class WatchedEdgeEffect extends EdgeEffect { + public CountDownLatch onAbsorbLatch = new CountDownLatch(1); + + WatchedEdgeEffect(Context context) { + super(context); + } + + @Override + public void onAbsorb(int velocity) { + super.onAbsorb(velocity); + onAbsorbLatch.countDown(); + } + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/OWNERS b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/OWNERS new file mode 100644 index 000000000000..74a29ddad073 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/OWNERS @@ -0,0 +1 @@ +hwwang@google.com diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java index 8a6403705c1c..1898ea737729 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java @@ -17,19 +17,28 @@ package com.android.wm.shell.dagger.pip; import android.annotation.NonNull; +import android.content.Context; import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.common.DisplayController; +import com.android.wm.shell.common.DisplayInsetsController; import com.android.wm.shell.common.pip.PipBoundsAlgorithm; import com.android.wm.shell.common.pip.PipBoundsState; +import com.android.wm.shell.common.pip.PipDisplayLayoutState; +import com.android.wm.shell.common.pip.PipUtils; import com.android.wm.shell.dagger.WMShellBaseModule; import com.android.wm.shell.dagger.WMSingleton; +import com.android.wm.shell.pip2.phone.PipController; import com.android.wm.shell.pip2.phone.PipTransition; +import com.android.wm.shell.sysui.ShellController; import com.android.wm.shell.sysui.ShellInit; import com.android.wm.shell.transition.Transitions; import dagger.Module; import dagger.Provides; +import java.util.Optional; + /** * Provides dependencies from {@link com.android.wm.shell.pip2}, this implementation is meant to be * the successor of its sibling {@link Pip1Module}. @@ -42,8 +51,26 @@ public abstract class Pip2Module { @NonNull ShellTaskOrganizer shellTaskOrganizer, @NonNull Transitions transitions, PipBoundsState pipBoundsState, - PipBoundsAlgorithm pipBoundsAlgorithm) { + PipBoundsAlgorithm pipBoundsAlgorithm, + Optional<PipController> pipController) { return new PipTransition(shellInit, shellTaskOrganizer, transitions, pipBoundsState, null, pipBoundsAlgorithm); } + + @WMSingleton + @Provides + static Optional<PipController> providePipController(Context context, + ShellInit shellInit, + ShellController shellController, + DisplayController displayController, + DisplayInsetsController displayInsetsController, + PipDisplayLayoutState pipDisplayLayoutState) { + if (!PipUtils.isPip2ExperimentEnabled()) { + return Optional.empty(); + } else { + return Optional.ofNullable(PipController.create( + context, shellInit, shellController, displayController, displayInsetsController, + pipDisplayLayoutState)); + } + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java new file mode 100644 index 000000000000..186cb615f4ec --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java @@ -0,0 +1,133 @@ +/* + * 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.pip2.phone; + +import static android.content.pm.PackageManager.FEATURE_PICTURE_IN_PICTURE; + +import android.content.Context; +import android.content.res.Configuration; +import android.view.InsetsState; + +import com.android.internal.protolog.common.ProtoLog; +import com.android.wm.shell.common.DisplayController; +import com.android.wm.shell.common.DisplayInsetsController; +import com.android.wm.shell.common.DisplayLayout; +import com.android.wm.shell.common.pip.PipDisplayLayoutState; +import com.android.wm.shell.common.pip.PipUtils; +import com.android.wm.shell.protolog.ShellProtoLogGroup; +import com.android.wm.shell.sysui.ConfigurationChangeListener; +import com.android.wm.shell.sysui.ShellController; +import com.android.wm.shell.sysui.ShellInit; + +/** + * Manages the picture-in-picture (PIP) UI and states for Phones. + */ +public class PipController implements ConfigurationChangeListener, + DisplayController.OnDisplaysChangedListener { + private static final String TAG = PipController.class.getSimpleName(); + + private Context mContext; + private ShellController mShellController; + private DisplayController mDisplayController; + private DisplayInsetsController mDisplayInsetsController; + private PipDisplayLayoutState mPipDisplayLayoutState; + + private PipController(Context context, + ShellInit shellInit, + ShellController shellController, + DisplayController displayController, + DisplayInsetsController displayInsetsController, + PipDisplayLayoutState pipDisplayLayoutState) { + mContext = context; + mShellController = shellController; + mDisplayController = displayController; + mDisplayInsetsController = displayInsetsController; + mPipDisplayLayoutState = pipDisplayLayoutState; + + if (PipUtils.isPip2ExperimentEnabled()) { + shellInit.addInitCallback(this::onInit, this); + } + } + + private void onInit() { + // Ensure that we have the display info in case we get calls to update the bounds before the + // listener calls back + mPipDisplayLayoutState.setDisplayId(mContext.getDisplayId()); + DisplayLayout layout = new DisplayLayout(mContext, mContext.getDisplay()); + mPipDisplayLayoutState.setDisplayLayout(layout); + + mShellController.addConfigurationChangeListener(this); + mDisplayController.addDisplayWindowListener(this); + mDisplayInsetsController.addInsetsChangedListener(mPipDisplayLayoutState.getDisplayId(), + new DisplayInsetsController.OnInsetsChangedListener() { + @Override + public void insetsChanged(InsetsState insetsState) { + onDisplayChanged(mDisplayController + .getDisplayLayout(mPipDisplayLayoutState.getDisplayId())); + } + }); + } + + /** + * Instantiates {@link PipController}, returns {@code null} if the feature not supported. + */ + public static PipController create(Context context, + ShellInit shellInit, + ShellController shellController, + DisplayController displayController, + DisplayInsetsController displayInsetsController, + PipDisplayLayoutState pipDisplayLayoutState) { + if (!context.getPackageManager().hasSystemFeature(FEATURE_PICTURE_IN_PICTURE)) { + ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: Device doesn't support Pip feature", TAG); + return null; + } + return new PipController(context, shellInit, shellController, displayController, + displayInsetsController, pipDisplayLayoutState); + } + + + @Override + public void onConfigurationChanged(Configuration newConfiguration) { + mPipDisplayLayoutState.onConfigurationChanged(); + } + + @Override + public void onThemeChanged() { + onDisplayChanged(new DisplayLayout(mContext, mContext.getDisplay())); + } + + @Override + public void onDisplayAdded(int displayId) { + if (displayId != mPipDisplayLayoutState.getDisplayId()) { + return; + } + onDisplayChanged(mDisplayController.getDisplayLayout(displayId)); + } + + @Override + public void onDisplayConfigurationChanged(int displayId, Configuration newConfig) { + if (displayId != mPipDisplayLayoutState.getDisplayId()) { + return; + } + onDisplayChanged(mDisplayController.getDisplayLayout(displayId)); + } + + private void onDisplayChanged(DisplayLayout layout) { + mPipDisplayLayoutState.setDisplayLayout(layout); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java index d31476c63890..d277eef761e9 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java @@ -925,19 +925,8 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler { if (toHome) wct.reorder(mRecentsTask, true /* toTop */); else wct.restoreTransientOrder(mRecentsTask); } - if (!toHome - // If a recents gesture starts on the 3p launcher, then the 3p launcher is the - // live tile (pausing app). If the gesture is "cancelled" we need to return to - // 3p launcher instead of "task-switching" away from it. - && (!mWillFinishToHome || mPausingSeparateHome) - && mPausingTasks != null && mState == STATE_NORMAL) { - if (mPausingSeparateHome) { - ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, - " returning to 3p home"); - } else { - ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, - " returning to app"); - } + if (!toHome && !mWillFinishToHome && mPausingTasks != null && mState == STATE_NORMAL) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, " returning to app"); // The gesture is returning to the pausing-task(s) rather than continuing with // recents, so end the transition by moving the app back to the top (and also // re-showing it's task). @@ -969,6 +958,15 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler { wct.restoreTransientOrder(mRecentsTask); } } else { + if (mPausingSeparateHome) { + if (mOpeningTasks.isEmpty()) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, + " recents occluded 3p home"); + } else { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, + " switch task by recents on 3p home"); + } + } ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, " normal finish"); // The general case: committing to recents, going home, or switching tasks. for (int i = 0; i < mOpeningTasks.size(); ++i) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java index 0548a8e751cc..d0e647b30a96 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java @@ -297,7 +297,7 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> } // Task surface itself - float shadowRadius = loadDimension(resources, params.mShadowRadiusId); + float shadowRadius; final Point taskPosition = mTaskInfo.positionInParent; if (isFullscreen) { // Setting the task crop to the width/height stops input events from being sent to @@ -308,9 +308,12 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> // drag-resized by the window decoration. startT.setWindowCrop(mTaskSurface, null); finishT.setWindowCrop(mTaskSurface, null); + // Shadow is not needed for fullscreen tasks + shadowRadius = 0; } else { startT.setWindowCrop(mTaskSurface, outResult.mWidth, outResult.mHeight); finishT.setWindowCrop(mTaskSurface, outResult.mWidth, outResult.mHeight); + shadowRadius = loadDimension(resources, params.mShadowRadiusId); } startT.setShadowRadius(mTaskSurface, shadowRadius) .show(mTaskSurface); diff --git a/media/java/android/media/RingtoneV1.java b/media/java/android/media/RingtoneV1.java index b761afaeaa67..3c54d4a0d166 100644 --- a/media/java/android/media/RingtoneV1.java +++ b/media/java/android/media/RingtoneV1.java @@ -16,14 +16,15 @@ package android.media; -import android.annotation.NonNull; import android.annotation.Nullable; +import android.compat.annotation.UnsupportedAppUsage; import android.content.Context; import android.content.res.AssetFileDescriptor; import android.content.res.Resources.NotFoundException; import android.media.audiofx.HapticGenerator; import android.net.Uri; import android.os.Binder; +import android.os.Build; import android.os.RemoteException; import android.os.Trace; import android.os.VibrationEffect; @@ -61,7 +62,6 @@ class RingtoneV1 implements Ringtone.ApiInterface { private final Context mContext; private final AudioManager mAudioManager; - private final Ringtone.Injectables mInjectables; private VolumeShaper.Configuration mVolumeShaperConfig; private VolumeShaper mVolumeShaper; @@ -74,10 +74,12 @@ class RingtoneV1 implements Ringtone.ApiInterface { private final IRingtonePlayer mRemotePlayer; private final Binder mRemoteToken; + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) private MediaPlayer mLocalPlayer; private final MyOnCompletionListener mCompletionListener = new MyOnCompletionListener(); private HapticGenerator mHapticGenerator; + @UnsupportedAppUsage private Uri mUri; private String mTitle; @@ -92,15 +94,10 @@ class RingtoneV1 implements Ringtone.ApiInterface { private boolean mHapticGeneratorEnabled = false; private final Object mPlaybackSettingsLock = new Object(); - /** @hide */ + /** {@hide} */ + @UnsupportedAppUsage public RingtoneV1(Context context, boolean allowRemote) { - this(context, new Ringtone.Injectables(), allowRemote); - } - - /** @hide */ - RingtoneV1(Context context, @NonNull Ringtone.Injectables injectables, boolean allowRemote) { mContext = context; - mInjectables = injectables; mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE); mAllowRemote = allowRemote; mRemotePlayer = allowRemote ? mAudioManager.getRingtonePlayer() : null; @@ -203,7 +200,7 @@ class RingtoneV1 implements Ringtone.ApiInterface { } destroyLocalPlayer(); // try opening uri locally before delegating to remote player - mLocalPlayer = mInjectables.newMediaPlayer(); + mLocalPlayer = new MediaPlayer(); try { mLocalPlayer.setDataSource(mContext, mUri); mLocalPlayer.setAudioAttributes(mAudioAttributes); @@ -243,7 +240,19 @@ class RingtoneV1 implements Ringtone.ApiInterface { */ public boolean hasHapticChannels() { // FIXME: support remote player, or internalize haptic channels support and remove entirely. - return mInjectables.hasHapticChannels(mLocalPlayer); + try { + android.os.Trace.beginSection("Ringtone.hasHapticChannels"); + if (mLocalPlayer != null) { + for(MediaPlayer.TrackInfo trackInfo : mLocalPlayer.getTrackInfo()) { + if (trackInfo.hasHapticChannels()) { + return true; + } + } + } + } finally { + android.os.Trace.endSection(); + } + return false; } /** @@ -325,7 +334,7 @@ class RingtoneV1 implements Ringtone.ApiInterface { * @see android.media.audiofx.HapticGenerator#isAvailable() */ public boolean setHapticGeneratorEnabled(boolean enabled) { - if (!mInjectables.isHapticGeneratorAvailable()) { + if (!HapticGenerator.isAvailable()) { return false; } synchronized (mPlaybackSettingsLock) { @@ -353,7 +362,7 @@ class RingtoneV1 implements Ringtone.ApiInterface { mLocalPlayer.setVolume(mVolume); mLocalPlayer.setLooping(mIsLooping); if (mHapticGenerator == null && mHapticGeneratorEnabled) { - mHapticGenerator = mInjectables.createHapticGenerator(mLocalPlayer); + mHapticGenerator = HapticGenerator.create(mLocalPlayer.getAudioSessionId()); } if (mHapticGenerator != null) { mHapticGenerator.setEnabled(mHapticGeneratorEnabled); @@ -388,6 +397,7 @@ class RingtoneV1 implements Ringtone.ApiInterface { * * @hide */ + @UnsupportedAppUsage public void setUri(Uri uri) { setUri(uri, null); } @@ -415,6 +425,7 @@ class RingtoneV1 implements Ringtone.ApiInterface { } /** {@hide} */ + @UnsupportedAppUsage public Uri getUri() { return mUri; } @@ -545,7 +556,7 @@ class RingtoneV1 implements Ringtone.ApiInterface { Log.e(TAG, "Could not load fallback ringtone"); return false; } - mLocalPlayer = mInjectables.newMediaPlayer(); + mLocalPlayer = new MediaPlayer(); if (afd.getDeclaredLength() < 0) { mLocalPlayer.setDataSource(afd.getFileDescriptor()); } else { @@ -583,12 +594,12 @@ class RingtoneV1 implements Ringtone.ApiInterface { } public boolean isLocalOnly() { - return !mAllowRemote; + return mAllowRemote; } public boolean isUsingRemotePlayer() { // V2 testing api, but this is the v1 approximation. - return (mLocalPlayer == null) && mAllowRemote && (mRemotePlayer != null) && (mUri != null); + return (mLocalPlayer == null) && mAllowRemote && (mRemotePlayer != null); } class MyOnCompletionListener implements MediaPlayer.OnCompletionListener { diff --git a/media/java/android/media/projection/IMediaProjectionManager.aidl b/media/java/android/media/projection/IMediaProjectionManager.aidl index 80e22477efed..31e65eb13926 100644 --- a/media/java/android/media/projection/IMediaProjectionManager.aidl +++ b/media/java/android/media/projection/IMediaProjectionManager.aidl @@ -175,5 +175,5 @@ interface IMediaProjectionManager { @EnforcePermission("android.Manifest.permission.MANAGE_MEDIA_PROJECTION") @JavaPassthrough(annotation = "@android.annotation.RequiresPermission(android.Manifest" + ".permission.MANAGE_MEDIA_PROJECTION)") - void notifyPermissionRequestStateChange(int hostUid, int state, int sessionCreationSource); + oneway void notifyPermissionRequestStateChange(int hostUid, int state, int sessionCreationSource); } diff --git a/media/tests/ringtone/src/com/android/media/RingtoneBuilderTest.java b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/unit/RingtoneTest.java index 2c8daba86d19..3c0c6847f557 100644 --- a/media/tests/ringtone/src/com/android/media/RingtoneBuilderTest.java +++ b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/unit/RingtoneTest.java @@ -14,22 +14,20 @@ * limitations under the License. */ -package com.android.media; +package com.android.mediaframeworktest.unit; import static android.media.Ringtone.MEDIA_SOUND; import static android.media.Ringtone.MEDIA_SOUND_AND_VIBRATION; import static android.media.Ringtone.MEDIA_VIBRATION; -import static com.android.media.testing.MediaPlayerTestHelper.verifyPlayerFallbackSetup; -import static com.android.media.testing.MediaPlayerTestHelper.verifyPlayerSetup; -import static com.android.media.testing.MediaPlayerTestHelper.verifyPlayerStarted; -import static com.android.media.testing.MediaPlayerTestHelper.verifyPlayerStopped; - import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth.assertWithMessage; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.Mockito.doCallRealMethod; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.never; @@ -55,29 +53,34 @@ import android.os.VibrationAttributes; import android.os.VibrationEffect; import android.os.Vibrator; import android.testing.TestableContext; +import android.util.ArrayMap; +import android.util.ArraySet; import androidx.test.InstrumentationRegistry; import androidx.test.runner.AndroidJUnit4; -import com.android.framework.base.media.ringtone.tests.R; -import com.android.media.testing.RingtoneInjectablesTrackingTestRule; +import com.android.mediaframeworktest.R; 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.runner.RunWith; +import org.junit.runners.model.Statement; import org.mockito.ArgumentCaptor; import org.mockito.Captor; import org.mockito.Mock; +import org.mockito.Mockito; import org.mockito.MockitoAnnotations; import java.io.FileNotFoundException; +import java.util.ArrayDeque; +import java.util.Map; +import java.util.Queue; -/** - * Test behavior of {@link Ringtone} when it's created via {@link Ringtone.Builder}. - */ @RunWith(AndroidJUnit4.class) -public class RingtoneBuilderTest { +public class RingtoneTest { private static final Uri SOUND_URI = Uri.parse("content://fake-sound-uri"); @@ -90,8 +93,11 @@ public class RingtoneBuilderTest { private static final VibrationEffect VIBRATION_EFFECT = VibrationEffect.createWaveform(new long[] { 0, 100, 50, 100}, -1); + private static final VibrationEffect VIBRATION_EFFECT_REPEATING = + VibrationEffect.createWaveform(new long[] { 0, 100, 50, 100, 50}, 1); - @Rule public final RingtoneInjectablesTrackingTestRule + @Rule + public final RingtoneInjectablesTrackingTestRule mMediaPlayerRule = new RingtoneInjectablesTrackingTestRule(); @Captor private ArgumentCaptor<IBinder> mIBinderCaptor; @@ -116,7 +122,6 @@ public class RingtoneBuilderTest { mContext = spy(testContext); } - @Test public void testRingtone_fullLifecycleUsingLocalMediaPlayer() throws Exception { MediaPlayer mockMediaPlayer = mMediaPlayerRule.expectLocalMediaPlayer(); @@ -137,14 +142,14 @@ public class RingtoneBuilderTest { assertThat(ringtone.isLocalOnly()).isFalse(); // Prepare - verifyPlayerSetup(mContext, mockMediaPlayer, SOUND_URI, RINGTONE_ATTRIBUTES); + verifyLocalPlayerSetup(mockMediaPlayer, SOUND_URI, RINGTONE_ATTRIBUTES); verify(mockMediaPlayer).setVolume(1.0f); verify(mockMediaPlayer).setLooping(false); verify(mockMediaPlayer).prepare(); // Play ringtone.play(); - verifyPlayerStarted(mockMediaPlayer); + verifyLocalPlay(mockMediaPlayer); // Verify dynamic controls. ringtone.setVolume(0.8f); @@ -160,7 +165,7 @@ public class RingtoneBuilderTest { // Release ringtone.stop(); - verifyPlayerStopped(mockMediaPlayer); + verifyLocalStop(mockMediaPlayer); // This test is intended to strictly verify all interactions with MediaPlayer in a local // playback case. This shouldn't be necessary in other tests that have the same basic @@ -194,16 +199,16 @@ public class RingtoneBuilderTest { assertThat(ringtone.getAudioAttributes()).isEqualTo(audioAttributes); // Prepare - verifyPlayerSetup(mContext, mockMediaPlayer, SOUND_URI, audioAttributes); + verifyLocalPlayerSetup(mockMediaPlayer, SOUND_URI, audioAttributes); verify(mockMediaPlayer).prepare(); // Play ringtone.play(); - verifyPlayerStarted(mockMediaPlayer); + verifyLocalPlay(mockMediaPlayer); // Release ringtone.stop(); - verifyPlayerStopped(mockMediaPlayer); + verifyLocalStop(mockMediaPlayer); verifyZeroInteractions(mMockRemotePlayer); verifyZeroInteractions(mMockVibrator); @@ -215,8 +220,8 @@ public class RingtoneBuilderTest { setupFileNotFound(mockMediaPlayer, SOUND_URI); Ringtone ringtone = newBuilder(MEDIA_SOUND, RINGTONE_ATTRIBUTES) - .setUri(SOUND_URI) - .build(); + .setUri(SOUND_URI) + .build(); assertThat(ringtone).isNotNull(); assertThat(ringtone.isUsingRemotePlayer()).isTrue(); @@ -279,7 +284,7 @@ public class RingtoneBuilderTest { // Prepare // Uses attributes with haptic channels enabled, but will use the effect when there aren't // any present. - verifyPlayerSetup(mContext, mockMediaPlayer, SOUND_URI, RINGTONE_ATTRIBUTES_WITH_HC); + verifyLocalPlayerSetup(mockMediaPlayer, SOUND_URI, RINGTONE_ATTRIBUTES_WITH_HC); verify(mockMediaPlayer).setVolume(1.0f); verify(mockMediaPlayer).setLooping(false); verify(mockMediaPlayer).prepare(); @@ -287,7 +292,7 @@ public class RingtoneBuilderTest { // Play ringtone.play(); - verifyPlayerStarted(mockMediaPlayer); + verifyLocalPlay(mockMediaPlayer); verify(mMockVibrator).vibrate(VIBRATION_EFFECT, RINGTONE_VIB_ATTRIBUTES); // Verify dynamic controls. @@ -305,7 +310,7 @@ public class RingtoneBuilderTest { // Release ringtone.stop(); - verifyPlayerStopped(mockMediaPlayer); + verifyLocalStop(mockMediaPlayer); verify(mMockVibrator).cancel(VibrationAttributes.USAGE_RINGTONE); // This test is intended to strictly verify all interactions with MediaPlayer in a local @@ -383,7 +388,7 @@ public class RingtoneBuilderTest { // Prepare // Uses attributes with haptic channels enabled, but will abandon the MediaPlayer when it // knows there aren't any. - verifyPlayerSetup(mContext, mockMediaPlayer, SOUND_URI, RINGTONE_ATTRIBUTES_WITH_HC); + verifyLocalPlayerSetup(mockMediaPlayer, SOUND_URI, RINGTONE_ATTRIBUTES_WITH_HC); verify(mockMediaPlayer).setVolume(0.0f); // Vibration-only: sound muted. verify(mockMediaPlayer).setLooping(false); verify(mockMediaPlayer).prepare(); @@ -438,7 +443,7 @@ public class RingtoneBuilderTest { // Prepare // Uses attributes with haptic channels enabled, but will use the effect when there aren't // any present. - verifyPlayerSetup(mContext, mockMediaPlayer, SOUND_URI, RINGTONE_ATTRIBUTES_WITH_HC); + verifyLocalPlayerSetup(mockMediaPlayer, SOUND_URI, RINGTONE_ATTRIBUTES_WITH_HC); verify(mockMediaPlayer).setVolume(0.0f); // Vibration-only: sound muted. verify(mockMediaPlayer).setLooping(false); verify(mockMediaPlayer).prepare(); @@ -446,7 +451,7 @@ public class RingtoneBuilderTest { // Play ringtone.play(); // Vibrator.vibrate isn't called because the vibration comes from the sound. - verifyPlayerStarted(mockMediaPlayer); + verifyLocalPlay(mockMediaPlayer); // Verify dynamic controls (no-op without sound) ringtone.setVolume(0.8f); @@ -461,7 +466,7 @@ public class RingtoneBuilderTest { // Release ringtone.stop(); - verifyPlayerStopped(mockMediaPlayer); + verifyLocalStop(mockMediaPlayer); // This test is intended to strictly verify all interactions with MediaPlayer in a local // playback case. This shouldn't be necessary in other tests that have the same basic @@ -491,17 +496,17 @@ public class RingtoneBuilderTest { // Prepare // The attributes here have haptic channels enabled (unlike above) - verifyPlayerSetup(mContext, mockMediaPlayer, SOUND_URI, RINGTONE_ATTRIBUTES_WITH_HC); + verifyLocalPlayerSetup(mockMediaPlayer, SOUND_URI, RINGTONE_ATTRIBUTES_WITH_HC); verify(mockMediaPlayer).prepare(); // Play ringtone.play(); when(mockMediaPlayer.isPlaying()).thenReturn(true); - verifyPlayerStarted(mockMediaPlayer); + verifyLocalPlay(mockMediaPlayer); // Release ringtone.stop(); - verifyPlayerStopped(mockMediaPlayer); + verifyLocalStop(mockMediaPlayer); verifyZeroInteractions(mMockRemotePlayer); // Nothing after the initial hasVibrator - it uses audio-coupled. @@ -531,7 +536,7 @@ public class RingtoneBuilderTest { // Prepare // The attributes here have haptic channels enabled (unlike above) - verifyPlayerSetup(mContext, mockMediaPlayer, SOUND_URI, RINGTONE_ATTRIBUTES_WITH_HC); + verifyLocalPlayerSetup(mockMediaPlayer, SOUND_URI, RINGTONE_ATTRIBUTES_WITH_HC); verify(mockMediaPlayer).prepare(); // Play @@ -554,7 +559,7 @@ public class RingtoneBuilderTest { @Test public void testRingtone_nullMediaOnBuilderUsesFallback() throws Exception { AssetFileDescriptor testResourceFd = - mContext.getResources().openRawResourceFd(R.raw.test_sound_file); + mContext.getResources().openRawResourceFd(R.raw.shortmp3); // Ensure it will flow as expected. assertThat(testResourceFd).isNotNull(); assertThat(testResourceFd.getDeclaredLength()).isAtLeast(0); @@ -570,18 +575,18 @@ public class RingtoneBuilderTest { // Delegates straight to fallback in local player. // Prepare - verifyPlayerFallbackSetup(mockMediaPlayer, testResourceFd, RINGTONE_ATTRIBUTES); + verifyLocalPlayerFallbackSetup(mockMediaPlayer, testResourceFd, RINGTONE_ATTRIBUTES); verify(mockMediaPlayer).setVolume(1.0f); verify(mockMediaPlayer).setLooping(false); verify(mockMediaPlayer).prepare(); // Play ringtone.play(); - verifyPlayerStarted(mockMediaPlayer); + verifyLocalPlay(mockMediaPlayer); // Release ringtone.stop(); - verifyPlayerStopped(mockMediaPlayer); + verifyLocalStop(mockMediaPlayer); verifyNoMoreInteractions(mockMediaPlayer); verifyNoMoreInteractions(mMockRemotePlayer); @@ -610,10 +615,24 @@ public class RingtoneBuilderTest { verifyNoMoreInteractions(mMockRemotePlayer); } + @Test + public void testRingtone_noMediaSetOnBuilderFallbackFailsAndNoRemote() throws Exception { + mContext.getOrCreateTestableResources() + .addOverride(com.android.internal.R.raw.fallbackring, null); + Ringtone ringtone = newBuilder(MEDIA_SOUND, RINGTONE_ATTRIBUTES) + .setUri(null) + .setLocalOnly() + .build(); + // Local player fallback fails as the resource isn't found (no media player creation is + // attempted), and since there is no local player, the ringtone ends up having nothing to + // do. + assertThat(ringtone).isNull(); + } + private Ringtone.Builder newBuilder(@Ringtone.RingtoneMedia int ringtoneMedia, AudioAttributes audioAttributes) { return new Ringtone.Builder(mContext, ringtoneMedia, audioAttributes) - .setInjectables(mMediaPlayerRule.getRingtoneTestInjectables()); + .setInjectables(mMediaPlayerRule.injectables); } private static AudioAttributes audioAttributes(int audioUsage) { @@ -628,4 +647,194 @@ public class RingtoneBuilderTest { doThrow(new FileNotFoundException("Fake file not found")) .when(mockMediaPlayer).setDataSource(any(Context.class), eq(uri)); } + + private void verifyLocalPlayerSetup(MediaPlayer mockPlayer, Uri expectedUri, + AudioAttributes expectedAudioAttributes) throws Exception { + verify(mockPlayer).setDataSource(mContext, expectedUri); + verify(mockPlayer).setAudioAttributes(expectedAudioAttributes); + verify(mockPlayer).setPreferredDevice(null); + verify(mockPlayer).prepare(); + } + + private void verifyLocalPlayerFallbackSetup(MediaPlayer mockPlayer, AssetFileDescriptor afd, + AudioAttributes expectedAudioAttributes) throws Exception { + // This is very specific but it's a simple way to test that the test resource matches. + if (afd.getDeclaredLength() < 0) { + verify(mockPlayer).setDataSource(afd.getFileDescriptor()); + } else { + verify(mockPlayer).setDataSource(afd.getFileDescriptor(), + afd.getStartOffset(), + afd.getDeclaredLength()); + } + verify(mockPlayer).setAudioAttributes(expectedAudioAttributes); + verify(mockPlayer).setPreferredDevice(null); + verify(mockPlayer).prepare(); + } + + private void verifyLocalPlay(MediaPlayer mockMediaPlayer) { + verify(mockMediaPlayer).setOnCompletionListener(any()); + verify(mockMediaPlayer).start(); + } + + private void verifyLocalStop(MediaPlayer mockMediaPlayer) { + verify(mockMediaPlayer).stop(); + verify(mockMediaPlayer).setOnCompletionListener(isNull()); + verify(mockMediaPlayer).reset(); + verify(mockMediaPlayer).release(); + } + + /** + * This rule ensures that all expected media player creations from the factory do actually + * occur. The reason for this level of control is that creating a media player is fairly + * expensive and blocking, so we do want unit tests of this class to "declare" interactions + * of all created media players. + * + * This needs to be a TestRule so that the teardown assertions can be skipped if the test has + * failed (and media player assertions may just be a distracting side effect). Otherwise, the + * teardown failures hide the real test ones. + */ + public static class RingtoneInjectablesTrackingTestRule implements TestRule { + public Ringtone.Injectables injectables = new TestInjectables(); + public boolean hapticGeneratorAvailable = true; + + // Queue of (local) media players, in order of expected creation. Enqueue using + // expectNewMediaPlayer(), dequeued by the media player factory passed to Ringtone. + // This queue is asserted to be empty at the end of the test. + private Queue<MediaPlayer> mMockMediaPlayerQueue = new ArrayDeque<>(); + + // Similar to media players, but for haptic generator, which also needs releasing. + private Map<MediaPlayer, HapticGenerator> mMockHapticGeneratorMap = new ArrayMap<>(); + + // Media players with haptic channels. + private ArraySet<MediaPlayer> mHapticChannels = new ArraySet<>(); + + @Override + public Statement apply(Statement base, Description description) { + return new Statement() { + @Override + public void evaluate() throws Throwable { + base.evaluate(); + // Only assert if the test didn't fail (base.evaluate() would throw). + assertWithMessage("Test setup an expectLocalMediaPlayer but it wasn't consumed") + .that(mMockMediaPlayerQueue).isEmpty(); + // Only assert if the test didn't fail (base.evaluate() would throw). + assertWithMessage( + "Test setup an expectLocalHapticGenerator but it wasn't consumed") + .that(mMockHapticGeneratorMap).isEmpty(); + } + }; + } + + private TestMediaPlayer expectLocalMediaPlayer() { + TestMediaPlayer mockMediaPlayer = Mockito.mock(TestMediaPlayer.class); + // Delegate to simulated methods. This means they can be verified but also reflect + // realistic transitions from the TestMediaPlayer. + doCallRealMethod().when(mockMediaPlayer).start(); + doCallRealMethod().when(mockMediaPlayer).stop(); + doCallRealMethod().when(mockMediaPlayer).setLooping(anyBoolean()); + when(mockMediaPlayer.isLooping()).thenCallRealMethod(); + when(mockMediaPlayer.isLooping()).thenCallRealMethod(); + mMockMediaPlayerQueue.add(mockMediaPlayer); + return mockMediaPlayer; + } + + private HapticGenerator expectHapticGenerator(MediaPlayer mockMediaPlayer) { + HapticGenerator mockHapticGenerator = Mockito.mock(HapticGenerator.class); + // A test should never want this. + assertWithMessage("Can't expect a second haptic generator created " + + "for one media player") + .that(mMockHapticGeneratorMap.put(mockMediaPlayer, mockHapticGenerator)) + .isNull(); + return mockHapticGenerator; + } + + private void setHasHapticChannels(MediaPlayer mp, boolean hasHapticChannels) { + if (hasHapticChannels) { + mHapticChannels.add(mp); + } else { + mHapticChannels.remove(mp); + } + } + + private class TestInjectables extends Ringtone.Injectables { + @Override + public MediaPlayer newMediaPlayer() { + assertWithMessage( + "Unexpected MediaPlayer creation. Bug or need expectNewMediaPlayer") + .that(mMockMediaPlayerQueue) + .isNotEmpty(); + return mMockMediaPlayerQueue.remove(); + } + + @Override + public boolean isHapticGeneratorAvailable() { + return hapticGeneratorAvailable; + } + + @Override + public HapticGenerator createHapticGenerator(MediaPlayer mediaPlayer) { + HapticGenerator mockHapticGenerator = mMockHapticGeneratorMap.remove(mediaPlayer); + assertWithMessage("Unexpected HapticGenerator creation. " + + "Bug or need expectHapticGenerator") + .that(mockHapticGenerator) + .isNotNull(); + return mockHapticGenerator; + } + + @Override + public boolean isHapticPlaybackSupported() { + return true; + } + + @Override + public boolean hasHapticChannels(MediaPlayer mp) { + return mHapticChannels.contains(mp); + } + } + } + + /** + * MediaPlayer relies on a native backend and so its necessary to intercept calls from + * fake usage hitting them. + * + * Mocks don't work directly on native calls, but if they're overridden then it does work. + * Some basic state faking is also done to make the mocks more realistic. + */ + private static class TestMediaPlayer extends MediaPlayer { + private boolean mIsPlaying = false; + private boolean mIsLooping = false; + + @Override + public void start() { + mIsPlaying = true; + } + + @Override + public void stop() { + mIsPlaying = false; + } + + @Override + public void setLooping(boolean value) { + mIsLooping = value; + } + + @Override + public boolean isLooping() { + return mIsLooping; + } + + @Override + public boolean isPlaying() { + return mIsPlaying; + } + + void simulatePlayingFinished() { + if (!mIsPlaying) { + throw new IllegalStateException( + "Attempted to pretend playing finished when not playing"); + } + mIsPlaying = false; + } + } } diff --git a/media/tests/ringtone/Android.bp b/media/tests/ringtone/Android.bp index 8d1e5e3a5bab..55b98c4704b1 100644 --- a/media/tests/ringtone/Android.bp +++ b/media/tests/ringtone/Android.bp @@ -9,24 +9,15 @@ android_test { srcs: ["src/**/*.java"], libs: [ - "android.test.base", - "android.test.mock", "android.test.runner", + "android.test.base", ], static_libs: [ - "androidx.test.ext.junit", - "androidx.test.ext.truth", "androidx.test.rules", - "frameworks-base-testutils", - "mockito-target-inline-minus-junit4", - "testables", "testng", - ], - - jni_libs: [ - "libdexmakerjvmtiagent", - "libstaticjvmtiagent", + "androidx.test.ext.truth", + "frameworks-base-testutils", ], test_suites: [ diff --git a/media/tests/ringtone/OWNERS b/media/tests/ringtone/OWNERS deleted file mode 100644 index 93b44f4788c5..000000000000 --- a/media/tests/ringtone/OWNERS +++ /dev/null @@ -1,3 +0,0 @@ -# Bug component: 345036 - -include /services/core/java/com/android/server/vibrator/OWNERS diff --git a/media/tests/ringtone/src/com/android/media/testing/MediaPlayerTestHelper.java b/media/tests/ringtone/src/com/android/media/testing/MediaPlayerTestHelper.java deleted file mode 100644 index e97e1173a1ea..000000000000 --- a/media/tests/ringtone/src/com/android/media/testing/MediaPlayerTestHelper.java +++ /dev/null @@ -1,75 +0,0 @@ -/* - * 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 com.android.media.testing; - -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.isNull; -import static org.mockito.Mockito.verify; - -import android.content.Context; -import android.content.res.AssetFileDescriptor; -import android.media.AudioAttributes; -import android.media.MediaPlayer; -import android.net.Uri; - -/** - * Helper class with assertion methods on mock {@link MediaPlayer} instances. - */ -public final class MediaPlayerTestHelper { - - /** Verify this local media player mock instance was started. */ - public static void verifyPlayerStarted(MediaPlayer mockMediaPlayer) { - verify(mockMediaPlayer).setOnCompletionListener(any()); - verify(mockMediaPlayer).start(); - } - - /** Verify this local media player mock instance was stopped and released. */ - public static void verifyPlayerStopped(MediaPlayer mockMediaPlayer) { - verify(mockMediaPlayer).stop(); - verify(mockMediaPlayer).setOnCompletionListener(isNull()); - verify(mockMediaPlayer).reset(); - verify(mockMediaPlayer).release(); - } - - /** Verify this local media player mock instance was setup with given attributes. */ - public static void verifyPlayerSetup(Context context, MediaPlayer mockPlayer, - Uri expectedUri, AudioAttributes expectedAudioAttributes) throws Exception { - verify(mockPlayer).setDataSource(context, expectedUri); - verify(mockPlayer).setAudioAttributes(expectedAudioAttributes); - verify(mockPlayer).setPreferredDevice(null); - verify(mockPlayer).prepare(); - } - - /** Verify this local media player mock instance was setup with given fallback attributes. */ - public static void verifyPlayerFallbackSetup(MediaPlayer mockPlayer, - AssetFileDescriptor afd, AudioAttributes expectedAudioAttributes) throws Exception { - // This is very specific but it's a simple way to test that the test resource matches. - if (afd.getDeclaredLength() < 0) { - verify(mockPlayer).setDataSource(afd.getFileDescriptor()); - } else { - verify(mockPlayer).setDataSource(afd.getFileDescriptor(), - afd.getStartOffset(), - afd.getDeclaredLength()); - } - verify(mockPlayer).setAudioAttributes(expectedAudioAttributes); - verify(mockPlayer).setPreferredDevice(null); - verify(mockPlayer).prepare(); - } - - private MediaPlayerTestHelper() { - } -} diff --git a/media/tests/ringtone/src/com/android/media/testing/RingtoneInjectablesTrackingTestRule.java b/media/tests/ringtone/src/com/android/media/testing/RingtoneInjectablesTrackingTestRule.java deleted file mode 100644 index 25752ce83e5c..000000000000 --- a/media/tests/ringtone/src/com/android/media/testing/RingtoneInjectablesTrackingTestRule.java +++ /dev/null @@ -1,225 +0,0 @@ -/* - * 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 com.android.media.testing; - -import static com.google.common.truth.Truth.assertWithMessage; - -import static org.mockito.ArgumentMatchers.anyBoolean; -import static org.mockito.Mockito.doCallRealMethod; -import static org.mockito.Mockito.when; - -import android.media.MediaPlayer; -import android.media.Ringtone; -import android.media.audiofx.HapticGenerator; -import android.util.ArrayMap; -import android.util.ArraySet; - -import org.junit.rules.TestRule; -import org.junit.runner.Description; -import org.junit.runners.model.Statement; -import org.mockito.Mockito; - -import java.util.ArrayDeque; -import java.util.Map; -import java.util.Queue; - -/** - * This rule ensures that all expected media player creations from the factory do actually - * occur. The reason for this level of control is that creating a media player is fairly - * expensive and blocking, so we do want unit tests of this class to "declare" interactions - * of all created media players. - * <p> - * This needs to be a TestRule so that the teardown assertions can be skipped if the test has - * failed (and media player assertions may just be a distracting side effect). Otherwise, the - * teardown failures hide the real test ones. - */ -public class RingtoneInjectablesTrackingTestRule implements TestRule { - - private final Ringtone.Injectables mRingtoneTestInjectables = new TestInjectables(); - - // Queue of (local) media players, in order of expected creation. Enqueue using - // expectNewMediaPlayer(), dequeued by the media player factory passed to Ringtone. - // This queue is asserted to be empty at the end of the test. - private final Queue<MediaPlayer> mMockMediaPlayerQueue = new ArrayDeque<>(); - - // Similar to media players, but for haptic generator, which also needs releasing. - private final Map<MediaPlayer, HapticGenerator> mMockHapticGeneratorMap = new ArrayMap<>(); - - // Media players with haptic channels. - private final ArraySet<MediaPlayer> mHapticChannels = new ArraySet<>(); - - private boolean mHapticGeneratorAvailable = true; - - @Override - public Statement apply(Statement base, Description description) { - return new Statement() { - @Override - public void evaluate() throws Throwable { - base.evaluate(); - // Only assert if the test didn't fail (base.evaluate() would throw). - assertWithMessage("Test setup an expectLocalMediaPlayer but it wasn't consumed") - .that(mMockMediaPlayerQueue).isEmpty(); - // Only assert if the test didn't fail (base.evaluate() would throw). - assertWithMessage( - "Test setup an expectLocalHapticGenerator but it wasn't consumed") - .that(mMockHapticGeneratorMap).isEmpty(); - } - }; - } - - /** The {@link Ringtone.Injectables} to be used for creating a testable {@link Ringtone}. */ - public Ringtone.Injectables getRingtoneTestInjectables() { - return mRingtoneTestInjectables; - } - - /** - * Create a test {@link MediaPlayer} that will be provided to the {@link Ringtone} instance - * created with {@link #getRingtoneTestInjectables()}. - * - * <p>If a media player is not created during the test execution after this method is called - * then the test will fail. It will also fail if the ringtone attempts to create one without - * this method being called first. - */ - public TestMediaPlayer expectLocalMediaPlayer() { - TestMediaPlayer mockMediaPlayer = Mockito.mock(TestMediaPlayer.class); - // Delegate to simulated methods. This means they can be verified but also reflect - // realistic transitions from the TestMediaPlayer. - doCallRealMethod().when(mockMediaPlayer).start(); - doCallRealMethod().when(mockMediaPlayer).stop(); - doCallRealMethod().when(mockMediaPlayer).setLooping(anyBoolean()); - when(mockMediaPlayer.isLooping()).thenCallRealMethod(); - mMockMediaPlayerQueue.add(mockMediaPlayer); - return mockMediaPlayer; - } - - /** - * Create a test {@link HapticGenerator} that will be provided to the {@link Ringtone} instance - * created with {@link #getRingtoneTestInjectables()}. - * - * <p>If a haptic generator is not created during the test execution after this method is called - * then the test will fail. It will also fail if the ringtone attempts to create one without - * this method being called first. - */ - public HapticGenerator expectHapticGenerator(MediaPlayer mediaPlayer) { - HapticGenerator mockHapticGenerator = Mockito.mock(HapticGenerator.class); - // A test should never want this. - assertWithMessage("Can't expect a second haptic generator created " - + "for one media player") - .that(mMockHapticGeneratorMap.put(mediaPlayer, mockHapticGenerator)) - .isNull(); - return mockHapticGenerator; - } - - /** - * Configures the {@link MediaPlayer} to always return given flag when - * {@link Ringtone.Injectables#hasHapticChannels(MediaPlayer)} is called. - */ - public void setHasHapticChannels(MediaPlayer mp, boolean hasHapticChannels) { - if (hasHapticChannels) { - mHapticChannels.add(mp); - } else { - mHapticChannels.remove(mp); - } - } - - /** Test implementation of {@link Ringtone.Injectables} that uses the test rule setup. */ - private class TestInjectables extends Ringtone.Injectables { - @Override - public MediaPlayer newMediaPlayer() { - assertWithMessage( - "Unexpected MediaPlayer creation. Bug or need expectNewMediaPlayer") - .that(mMockMediaPlayerQueue) - .isNotEmpty(); - return mMockMediaPlayerQueue.remove(); - } - - @Override - public boolean isHapticGeneratorAvailable() { - return mHapticGeneratorAvailable; - } - - @Override - public HapticGenerator createHapticGenerator(MediaPlayer mediaPlayer) { - HapticGenerator mockHapticGenerator = mMockHapticGeneratorMap.remove(mediaPlayer); - assertWithMessage("Unexpected HapticGenerator creation. " - + "Bug or need expectHapticGenerator") - .that(mockHapticGenerator) - .isNotNull(); - return mockHapticGenerator; - } - - @Override - public boolean isHapticPlaybackSupported() { - return true; - } - - @Override - public boolean hasHapticChannels(MediaPlayer mp) { - return mHapticChannels.contains(mp); - } - } - - /** - * MediaPlayer relies on a native backend and so its necessary to intercept calls from - * fake usage hitting them. - * <p> - * Mocks don't work directly on native calls, but if they're overridden then it does work. - * Some basic state faking is also done to make the mocks more realistic. - */ - public static class TestMediaPlayer extends MediaPlayer { - private boolean mIsPlaying = false; - private boolean mIsLooping = false; - - @Override - public void start() { - mIsPlaying = true; - } - - @Override - public void stop() { - mIsPlaying = false; - } - - @Override - public void setLooping(boolean value) { - mIsLooping = value; - } - - @Override - public boolean isLooping() { - return mIsLooping; - } - - @Override - public boolean isPlaying() { - return mIsPlaying; - } - - /** - * Updates {@link #isPlaying()} result to false, if it's set to true. - * - * @throws IllegalStateException is {@link #isPlaying()} is already false - */ - public void simulatePlayingFinished() { - if (!mIsPlaying) { - throw new IllegalStateException( - "Attempted to pretend playing finished when not playing"); - } - mIsPlaying = false; - } - } -} diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt index ce96bbfc7976..abc62c4682cc 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt @@ -43,6 +43,7 @@ import androidx.compose.ui.unit.Constraints import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.round import com.android.compose.animation.scene.transformation.PropertyTransformation +import com.android.compose.animation.scene.transformation.SharedElementTransformation import com.android.compose.modifiers.thenIf import com.android.compose.ui.util.lerp @@ -196,29 +197,44 @@ private fun shouldDrawElement( state.fromScene == state.toScene || !layoutImpl.isTransitionReady(state) || state.fromScene !in element.sceneValues || - state.toScene !in element.sceneValues || - !isSharedElementEnabled(layoutImpl, state, element.key) + state.toScene !in element.sceneValues ) { return true } - val otherScene = - layoutImpl.scenes.getValue( - if (scene.key == state.fromScene) { - state.toScene - } else { - state.fromScene - } - ) - - // When the element is shared, draw the one in the highest scene unless it is a background, i.e. - // it is usually drawn below everything else. - val isHighestScene = scene.zIndex > otherScene.zIndex - return if (element.key.isBackground) { - !isHighestScene - } else { - isHighestScene + val sharedTransformation = sharedElementTransformation(layoutImpl, state, element.key) + if (sharedTransformation?.enabled == false) { + return true } + + return shouldDrawOrComposeSharedElement( + layoutImpl, + state, + scene.key, + element.key, + sharedTransformation, + ) +} + +internal fun shouldDrawOrComposeSharedElement( + layoutImpl: SceneTransitionLayoutImpl, + transition: TransitionState.Transition, + scene: SceneKey, + element: ElementKey, + sharedTransformation: SharedElementTransformation? +): Boolean { + val scenePicker = sharedTransformation?.scenePicker ?: DefaultSharedElementScenePicker + val fromScene = transition.fromScene + val toScene = transition.toScene + + return scenePicker.sceneDuringTransition( + element = element, + fromScene = fromScene, + toScene = toScene, + progress = transition::progress, + fromSceneZIndex = layoutImpl.scenes.getValue(fromScene).zIndex, + toSceneZIndex = layoutImpl.scenes.getValue(toScene).zIndex, + ) == scene } private fun isSharedElementEnabled( @@ -226,6 +242,14 @@ private fun isSharedElementEnabled( transition: TransitionState.Transition, element: ElementKey, ): Boolean { + return sharedElementTransformation(layoutImpl, transition, element)?.enabled ?: true +} + +internal fun sharedElementTransformation( + layoutImpl: SceneTransitionLayoutImpl, + transition: TransitionState.Transition, + element: ElementKey, +): SharedElementTransformation? { val spec = layoutImpl.transitions.transitionSpec(transition.fromScene, transition.toScene) val sharedInFromScene = spec.transformations(element, transition.fromScene).shared val sharedInToScene = spec.transformations(element, transition.toScene).shared @@ -238,7 +262,7 @@ private fun isSharedElementEnabled( ) } - return sharedInFromScene?.enabled ?: true + return sharedInFromScene } /** diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MovableElement.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MovableElement.kt index 6dbeb69ff450..fa385d014ccb 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MovableElement.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MovableElement.kt @@ -22,6 +22,8 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Spacer import androidx.compose.runtime.Composable import androidx.compose.runtime.State +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.runtime.snapshots.Snapshot import androidx.compose.ui.Modifier @@ -60,7 +62,16 @@ internal fun MovableElement( // which case we still need to draw it. val picture = remember { Picture() } - if (shouldComposeMovableElement(layoutImpl, scene.key, element)) { + // Whether we should compose the movable element here. The scene picker logic to know in + // which scene we should compose/draw a movable element might depend on the current + // transition progress, so we put this in a derivedStateOf to prevent many recompositions + // during the transition. + val shouldComposeMovableElement by + remember(layoutImpl, scene.key, element) { + derivedStateOf { shouldComposeMovableElement(layoutImpl, scene.key, element) } + } + + if (shouldComposeMovableElement) { Box( Modifier.drawWithCache { val width = size.width.toInt() @@ -172,14 +183,13 @@ private fun shouldComposeMovableElement( return scene == fromScene } - // If we are ready in both scenes, then compose in the scene that has the highest zIndex (unless - // it is a background) given that this is the one that is going to be drawn. - val isHighestScene = layoutImpl.scene(scene).zIndex > layoutImpl.scene(otherScene).zIndex - return if (element.key.isBackground) { - !isHighestScene - } else { - isHighestScene - } + return shouldDrawOrComposeSharedElement( + layoutImpl, + transitionState, + scene, + element.key, + sharedElementTransformation(layoutImpl, transitionState, element.key), + ) } private class MovableElementScopeImpl( diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt index 49669775fedd..7b7ddfa5ec4e 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt @@ -120,8 +120,14 @@ interface TransitionBuilder : PropertyTransformationBuilder { * * @param enabled whether the matched element(s) should actually be shared in this transition. * Defaults to true. + * @param scenePicker the [SharedElementScenePicker] to use when deciding in which scene we + * should draw or compose this shared element. */ - fun sharedElement(matcher: ElementMatcher, enabled: Boolean = true) + fun sharedElement( + matcher: ElementMatcher, + enabled: Boolean = true, + scenePicker: SharedElementScenePicker = DefaultSharedElementScenePicker, + ) /** * Punch a hole in the element(s) matching [matcher] that has the same bounds as [bounds] and @@ -144,6 +150,44 @@ interface TransitionBuilder : PropertyTransformationBuilder { fun reversed(builder: TransitionBuilder.() -> Unit) } +interface SharedElementScenePicker { + /** + * Return the scene in which [element] should be drawn (when using `Modifier.element(key)`) or + * composed (when using `MovableElement(key)`) during the transition from [fromScene] to + * [toScene]. + */ + fun sceneDuringTransition( + element: ElementKey, + fromScene: SceneKey, + toScene: SceneKey, + progress: () -> Float, + fromSceneZIndex: Float, + toSceneZIndex: Float, + ): SceneKey +} + +object DefaultSharedElementScenePicker : SharedElementScenePicker { + override fun sceneDuringTransition( + element: ElementKey, + fromScene: SceneKey, + toScene: SceneKey, + progress: () -> Float, + fromSceneZIndex: Float, + toSceneZIndex: Float + ): SceneKey { + // By default shared elements are drawn in the highest scene possible, unless it is a + // background. + return if ( + (fromSceneZIndex > toSceneZIndex && !element.isBackground) || + (fromSceneZIndex < toSceneZIndex && element.isBackground) + ) { + fromScene + } else { + toScene + } + } +} + @TransitionDsl interface PropertyTransformationBuilder { /** diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDslImpl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDslImpl.kt index f1c27178391c..d2bfd91842ae 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDslImpl.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDslImpl.kt @@ -111,8 +111,12 @@ internal class TransitionBuilderImpl : TransitionBuilder { range = null } - override fun sharedElement(matcher: ElementMatcher, enabled: Boolean) { - transformations.add(SharedElementTransformation(matcher, enabled)) + override fun sharedElement( + matcher: ElementMatcher, + enabled: Boolean, + scenePicker: SharedElementScenePicker, + ) { + transformations.add(SharedElementTransformation(matcher, enabled, scenePicker)) } override fun timestampRange( diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Transformation.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Transformation.kt index 2ef8d56c6bc6..0db8469466ef 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Transformation.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Transformation.kt @@ -21,6 +21,7 @@ import com.android.compose.animation.scene.Element import com.android.compose.animation.scene.ElementMatcher import com.android.compose.animation.scene.Scene import com.android.compose.animation.scene.SceneTransitionLayoutImpl +import com.android.compose.animation.scene.SharedElementScenePicker import com.android.compose.animation.scene.TransitionState /** A transformation applied to one or more elements during a transition. */ @@ -48,6 +49,7 @@ sealed interface Transformation { internal class SharedElementTransformation( override val matcher: ElementMatcher, internal val enabled: Boolean, + internal val scenePicker: SharedElementScenePicker, ) : Transformation /** A transformation that is applied on the element during the whole transition. */ diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MovableElementTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MovableElementTest.kt index 4204cd5f0da0..83af630ab098 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MovableElementTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MovableElementTest.kt @@ -144,7 +144,36 @@ class MovableElementTest { rule.testTransition( fromSceneContent = { MovableCounter(TestElements.Foo, Modifier.size(50.dp)) }, toSceneContent = { MovableCounter(TestElements.Foo, Modifier.size(100.dp)) }, - transition = { spec = tween(durationMillis = 16 * 4, easing = LinearEasing) }, + transition = { + spec = tween(durationMillis = 16 * 4, easing = LinearEasing) + sharedElement( + TestElements.Foo, + scenePicker = + object : SharedElementScenePicker { + override fun sceneDuringTransition( + element: ElementKey, + fromScene: SceneKey, + toScene: SceneKey, + progress: () -> Float, + fromSceneZIndex: Float, + toSceneZIndex: Float + ): SceneKey { + assertThat(fromScene).isEqualTo(TestScenes.SceneA) + assertThat(toScene).isEqualTo(TestScenes.SceneB) + assertThat(fromSceneZIndex).isEqualTo(0) + assertThat(toSceneZIndex).isEqualTo(1) + + // Compose Foo in Scene A if progress < 0.65f, otherwise compose it + // in Scene B. + return if (progress() < 0.65f) { + TestScenes.SceneA + } else { + TestScenes.SceneB + } + } + } + ) + }, fromScene = TestScenes.SceneA, toScene = TestScenes.SceneB, ) { @@ -170,9 +199,12 @@ class MovableElementTest { at(32) { // During the transition, there is a single counter that is moved, with the current - // value. + // value. Given that progress = 0.5f, it is currently composed in SceneA. rule - .onNode(hasText("count: 3")) + .onNode( + hasText("count: 3") and + hasParent(isElement(TestElements.Foo, scene = TestScenes.SceneA)) + ) .assertIsDisplayed() .assertSizeIsEqualTo(75.dp, 75.dp) @@ -186,6 +218,26 @@ class MovableElementTest { .isEqualTo(1) } + at(48) { + // During the transition, there is a single counter that is moved, with the current + // value. Given that progress = 0.75f, it is currently composed in SceneB. + rule + .onNode( + hasText("count: 3") and + hasParent(isElement(TestElements.Foo, scene = TestScenes.SceneB)) + ) + .assertIsDisplayed() + + // There are no other counters. + assertThat( + rule + .onAllNodesWithText("count: ", substring = true) + .fetchSemanticsNodes() + .size + ) + .isEqualTo(1) + } + after { // At the end of the transition, the counter still has the current value. rule diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordViewController.java index 959cf6fb8565..01fc03516edc 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPasswordViewController.java @@ -51,7 +51,6 @@ import java.util.List; public class KeyguardPasswordViewController extends KeyguardAbsKeyInputViewController<KeyguardPasswordView> { - private static final int DELAY_MILLIS_TO_REEVALUATE_IME_SWITCH_ICON = 500; // 500ms private final KeyguardSecurityCallback mKeyguardSecurityCallback; private final DevicePostureController mPostureController; private final DevicePostureController.Callback mPostureCallback = posture -> @@ -164,13 +163,6 @@ public class KeyguardPasswordViewController // If there's more than one IME, enable the IME switcher button updateSwitchImeButton(); - - // When we the current user is switching, InputMethodManagerService sometimes has not - // switched internal state yet here. As a quick workaround, we check the keyboard state - // again. - // TODO: Remove this workaround by ensuring such a race condition never happens. - mMainExecutor.executeDelayed( - this::updateSwitchImeButton, DELAY_MILLIS_TO_REEVALUATE_IME_SWITCH_ICON); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/DeviceEntryModule.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/DeviceEntryModule.kt index e7f835f7b858..c3aaef76cb2f 100644 --- a/packages/SystemUI/src/com/android/systemui/deviceentry/DeviceEntryModule.kt +++ b/packages/SystemUI/src/com/android/systemui/deviceentry/DeviceEntryModule.kt @@ -1,5 +1,6 @@ package com.android.systemui.deviceentry +import com.android.systemui.deviceentry.data.repository.DeviceEntryHapticsRepositoryModule import com.android.systemui.deviceentry.data.repository.DeviceEntryRepositoryModule import dagger.Module @@ -7,6 +8,7 @@ import dagger.Module includes = [ DeviceEntryRepositoryModule::class, + DeviceEntryHapticsRepositoryModule::class, ], ) object DeviceEntryModule diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/data/repository/DeviceEntryHapticsRepository.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/data/repository/DeviceEntryHapticsRepository.kt new file mode 100644 index 000000000000..1458404446e6 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/deviceentry/data/repository/DeviceEntryHapticsRepository.kt @@ -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 com.android.systemui.deviceentry.data.repository + +import com.android.systemui.dagger.SysUISingleton +import dagger.Binds +import dagger.Module +import javax.inject.Inject +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow + +/** Interface for classes that can access device-entry haptics application state. */ +interface DeviceEntryHapticsRepository { + /** + * Whether a successful biometric haptic has been requested. Has not yet been handled if true. + */ + val successHapticRequest: Flow<Boolean> + + /** Whether an error biometric haptic has been requested. Has not yet been handled if true. */ + val errorHapticRequest: Flow<Boolean> + + fun requestSuccessHaptic() + fun handleSuccessHaptic() + fun requestErrorHaptic() + fun handleErrorHaptic() +} + +/** Encapsulates application state for device entry haptics. */ +@SysUISingleton +class DeviceEntryHapticsRepositoryImpl @Inject constructor() : DeviceEntryHapticsRepository { + private val _successHapticRequest = MutableStateFlow(false) + override val successHapticRequest: Flow<Boolean> = _successHapticRequest.asStateFlow() + + private val _errorHapticRequest = MutableStateFlow(false) + override val errorHapticRequest: Flow<Boolean> = _errorHapticRequest.asStateFlow() + + override fun requestSuccessHaptic() { + _successHapticRequest.value = true + } + + override fun handleSuccessHaptic() { + _successHapticRequest.value = false + } + + override fun requestErrorHaptic() { + _errorHapticRequest.value = true + } + + override fun handleErrorHaptic() { + _errorHapticRequest.value = false + } +} + +@Module +interface DeviceEntryHapticsRepositoryModule { + @Binds fun repository(impl: DeviceEntryHapticsRepositoryImpl): DeviceEntryHapticsRepository +} diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractor.kt new file mode 100644 index 000000000000..53d6f737af8d --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractor.kt @@ -0,0 +1,133 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.systemui.deviceentry.domain.interactor + +import com.android.keyguard.logging.BiometricUnlockLogger +import com.android.systemui.biometrics.data.repository.FingerprintPropertyRepository +import com.android.systemui.biometrics.shared.model.FingerprintSensorType +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.deviceentry.data.repository.DeviceEntryHapticsRepository +import com.android.systemui.keyevent.domain.interactor.KeyEventInteractor +import com.android.systemui.keyguard.data.repository.BiometricSettingsRepository +import com.android.systemui.power.domain.interactor.PowerInteractor +import com.android.systemui.power.shared.model.WakeSleepReason +import com.android.systemui.util.kotlin.sample +import com.android.systemui.util.time.SystemClock +import javax.inject.Inject +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.combineTransform +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onStart + +/** + * Business logic for device entry haptic events. Determines whether the haptic should play. In + * particular, there are extra guards for whether device entry error and successes hatpics should + * play when the physical fingerprint sensor is located on the power button. + */ +@ExperimentalCoroutinesApi +@SysUISingleton +class DeviceEntryHapticsInteractor +@Inject +constructor( + private val repository: DeviceEntryHapticsRepository, + fingerprintPropertyRepository: FingerprintPropertyRepository, + biometricSettingsRepository: BiometricSettingsRepository, + keyEventInteractor: KeyEventInteractor, + powerInteractor: PowerInteractor, + private val systemClock: SystemClock, + private val logger: BiometricUnlockLogger, +) { + private val powerButtonSideFpsEnrolled = + combineTransform( + fingerprintPropertyRepository.sensorType, + biometricSettingsRepository.isFingerprintEnrolledAndEnabled, + ) { sensorType, enrolledAndEnabled -> + if (sensorType == FingerprintSensorType.POWER_BUTTON) { + emit(enrolledAndEnabled) + } else { + emit(false) + } + } + .distinctUntilChanged() + private val powerButtonDown: Flow<Boolean> = keyEventInteractor.isPowerButtonDown + private val lastPowerButtonWakeup: Flow<Long> = + powerInteractor.detailedWakefulness + .filter { it.isAwakeFrom(WakeSleepReason.POWER_BUTTON) } + .map { systemClock.uptimeMillis() } + .onStart { + // If the power button hasn't been pressed, we still want this to evaluate to true: + // `uptimeMillis() - lastPowerButtonWakeup > recentPowerButtonPressThresholdMs` + emit(recentPowerButtonPressThresholdMs * -1L - 1L) + } + + val playSuccessHaptic: Flow<Boolean> = + repository.successHapticRequest + .filter { it } + .sample( + combine( + powerButtonSideFpsEnrolled, + powerButtonDown, + lastPowerButtonWakeup, + ::Triple + ) + ) + .map { (sideFpsEnrolled, powerButtonDown, lastPowerButtonWakeup) -> + val sideFpsAllowsHaptic = + !powerButtonDown && + systemClock.uptimeMillis() - lastPowerButtonWakeup > + recentPowerButtonPressThresholdMs + val allowHaptic = !sideFpsEnrolled || sideFpsAllowsHaptic + if (!allowHaptic) { + logger.d("Skip success haptic. Recent power button press or button is down.") + handleSuccessHaptic() // immediately handle, don't vibrate + } + allowHaptic + } + val playErrorHaptic: Flow<Boolean> = + repository.errorHapticRequest + .filter { it } + .sample(combine(powerButtonSideFpsEnrolled, powerButtonDown, ::Pair)) + .map { (sideFpsEnrolled, powerButtonDown) -> + val allowHaptic = !sideFpsEnrolled || !powerButtonDown + if (!allowHaptic) { + logger.d("Skip error haptic. Power button is down.") + handleErrorHaptic() // immediately handle, don't vibrate + } + allowHaptic + } + + fun vibrateSuccess() { + repository.requestSuccessHaptic() + } + + fun vibrateError() { + repository.requestErrorHaptic() + } + + fun handleSuccessHaptic() { + repository.handleSuccessHaptic() + } + + fun handleErrorHaptic() { + repository.handleErrorHaptic() + } + + private val recentPowerButtonPressThresholdMs = 400L +} diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt index 081618ef9d63..472cc24080d5 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt +++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt @@ -259,7 +259,7 @@ object Flags { // TODO(b/290652751): Tracking bug. @JvmField val MIGRATE_SPLIT_KEYGUARD_BOTTOM_AREA = - unreleasedFlag("migrate_split_keyguard_bottom_area", teamfood = true) + releasedFlag("migrate_split_keyguard_bottom_area") // TODO(b/297037052): Tracking bug. @JvmField @@ -274,7 +274,7 @@ object Flags { /** Migrate the lock icon view to the new keyguard root view. */ // TODO(b/286552209): Tracking bug. - @JvmField val MIGRATE_LOCK_ICON = unreleasedFlag("migrate_lock_icon", teamfood = true) + @JvmField val MIGRATE_LOCK_ICON = releasedFlag("migrate_lock_icon") // TODO(b/288276738): Tracking bug. @JvmField val WIDGET_ON_KEYGUARD = unreleasedFlag("widget_on_keyguard") @@ -419,7 +419,7 @@ object Flags { releasedFlag("incompatible_charging_battery_icon") // TODO(b/293585143): Tracking Bug - val INSTANT_TETHER = unreleasedFlag("instant_tether") + val INSTANT_TETHER = unreleasedFlag("instant_tether", teamfood = true) // TODO(b/294588085): Tracking Bug val WIFI_SECONDARY_NETWORKS = releasedFlag("wifi_secondary_networks") diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt index a511713eddd3..119ade48d4f7 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt @@ -28,6 +28,7 @@ import com.android.keyguard.LockIconViewController import com.android.keyguard.dagger.KeyguardStatusViewComponent import com.android.systemui.CoreStartable import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.deviceentry.domain.interactor.DeviceEntryHapticsInteractor import com.android.systemui.flags.FeatureFlags import com.android.systemui.flags.Flags import com.android.systemui.keyguard.ui.binder.KeyguardBlueprintViewBinder @@ -44,6 +45,7 @@ import com.android.systemui.res.R import com.android.systemui.shade.NotificationShadeWindowView import com.android.systemui.shade.domain.interactor.ShadeInteractor import com.android.systemui.statusbar.KeyguardIndicationController +import com.android.systemui.statusbar.VibratorHelper import com.android.systemui.statusbar.policy.KeyguardStateController import com.android.systemui.temporarydisplay.chipbar.ChipbarCoordinator import javax.inject.Inject @@ -72,7 +74,9 @@ constructor( private val keyguardIndicationController: KeyguardIndicationController, private val lockIconViewController: LockIconViewController, private val shadeInteractor: ShadeInteractor, - private val interactionJankMonitor: InteractionJankMonitor + private val interactionJankMonitor: InteractionJankMonitor, + private val deviceEntryHapticsInteractor: DeviceEntryHapticsInteractor, + private val vibratorHelper: VibratorHelper, ) : CoreStartable { private var rootViewHandle: DisposableHandle? = null @@ -143,6 +147,8 @@ constructor( shadeInteractor, { keyguardStatusViewController!!.getClockController() }, interactionJankMonitor, + deviceEntryHapticsInteractor, + vibratorHelper, ) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt index c72e6ce0b7d6..4d5c503d1c4e 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt @@ -17,6 +17,7 @@ package com.android.systemui.keyguard.ui.binder import android.annotation.DrawableRes +import android.view.HapticFeedbackConstants import android.view.View import android.view.View.OnLayoutChangeListener import android.view.ViewGroup @@ -29,6 +30,7 @@ import com.android.keyguard.KeyguardClockSwitch.MISSING_CLOCK_ID import com.android.systemui.common.shared.model.Icon import com.android.systemui.common.shared.model.Text import com.android.systemui.common.shared.model.TintedIcon +import com.android.systemui.deviceentry.domain.interactor.DeviceEntryHapticsInteractor import com.android.systemui.flags.FeatureFlags import com.android.systemui.flags.Flags import com.android.systemui.keyguard.shared.model.TransitionState @@ -38,6 +40,7 @@ import com.android.systemui.lifecycle.repeatWhenAttached import com.android.systemui.plugins.ClockController import com.android.systemui.res.R import com.android.systemui.shade.domain.interactor.ShadeInteractor +import com.android.systemui.statusbar.VibratorHelper import com.android.systemui.statusbar.policy.KeyguardStateController import com.android.systemui.temporarydisplay.ViewPriority import com.android.systemui.temporarydisplay.chipbar.ChipbarCoordinator @@ -45,6 +48,7 @@ import com.android.systemui.temporarydisplay.chipbar.ChipbarInfo import javax.inject.Provider import kotlinx.coroutines.DisposableHandle import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.filter import kotlinx.coroutines.launch /** Bind occludingAppDeviceEntryMessageViewModel to run whenever the keyguard view is attached. */ @@ -62,6 +66,8 @@ object KeyguardRootViewBinder { shadeInteractor: ShadeInteractor, clockControllerProvider: Provider<ClockController>?, interactionJankMonitor: InteractionJankMonitor?, + deviceEntryHapticsInteractor: DeviceEntryHapticsInteractor?, + vibratorHelper: VibratorHelper?, ): DisposableHandle { var onLayoutChangeListener: OnLayoutChange? = null val childViews = mutableMapOf<Int, View?>() @@ -177,6 +183,44 @@ object KeyguardRootViewBinder { } } } + + if (deviceEntryHapticsInteractor != null && vibratorHelper != null) { + launch { + deviceEntryHapticsInteractor.playSuccessHaptic + .filter { it } + .collect { + if ( + featureFlags.isEnabled(Flags.ONE_WAY_HAPTICS_API_MIGRATION) + ) { + vibratorHelper.performHapticFeedback( + view, + HapticFeedbackConstants.CONFIRM, + ) + } else { + vibratorHelper.vibrateAuthSuccess("device-entry::success") + } + deviceEntryHapticsInteractor.handleSuccessHaptic() + } + } + + launch { + deviceEntryHapticsInteractor.playErrorHaptic + .filter { it } + .collect { + if ( + featureFlags.isEnabled(Flags.ONE_WAY_HAPTICS_API_MIGRATION) + ) { + vibratorHelper.performHapticFeedback( + view, + HapticFeedbackConstants.REJECT, + ) + } else { + vibratorHelper.vibrateAuthSuccess("device-entry::error") + } + deviceEntryHapticsInteractor.handleErrorHaptic() + } + } + } } } viewModel.clockControllerProvider = clockControllerProvider @@ -189,7 +233,7 @@ object KeyguardRootViewBinder { view.setOnHierarchyChangeListener( object : OnHierarchyChangeListener { override fun onChildViewAdded(parent: View, child: View) { - childViews.put(child.id, view) + childViews.put(child.id, child) } override fun onChildViewRemoved(parent: View, child: View) { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt index 5a4bbef587af..692984a90a14 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt @@ -46,6 +46,7 @@ import com.android.systemui.biometrics.domain.interactor.UdfpsOverlayInteractor import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.deviceentry.domain.interactor.DeviceEntryHapticsInteractor import com.android.systemui.flags.FeatureFlags import com.android.systemui.flags.Flags import com.android.systemui.keyguard.ui.binder.KeyguardPreviewClockViewBinder @@ -114,6 +115,7 @@ constructor( private val chipbarCoordinator: ChipbarCoordinator, private val keyguardStateController: KeyguardStateController, private val shadeInteractor: ShadeInteractor, + private val deviceEntryHapticsInteractor: DeviceEntryHapticsInteractor, ) { val hostToken: IBinder? = bundle.getBinder(KEY_HOST_TOKEN) @@ -339,6 +341,8 @@ constructor( shadeInteractor, null, // clock provider only needed for burn in null, // jank monitor not required for preview mode + null, // device entry haptics not required for preview mode + null, // device entry haptics not required for preview mode ) ) rootView.addView( diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultAmbientIndicationAreaSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultAmbientIndicationAreaSection.kt index 9371d4e2d465..342a440d972b 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultAmbientIndicationAreaSection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultAmbientIndicationAreaSection.kt @@ -50,11 +50,8 @@ constructor( override fun addViews(constraintLayout: ConstraintLayout) { if (featureFlags.isEnabled(Flags.MIGRATE_SPLIT_KEYGUARD_BOTTOM_AREA)) { - val view = - LayoutInflater.from(constraintLayout.context) - .inflate(R.layout.ambient_indication, constraintLayout, false) - - constraintLayout.addView(view) + LayoutInflater.from(constraintLayout.context) + .inflate(R.layout.ambient_indication, constraintLayout, true) } } diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/MediaProjectionMetricsLogger.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/MediaProjectionMetricsLogger.kt index 8634b0911391..a53f0f11c380 100644 --- a/packages/SystemUI/src/com/android/systemui/mediaprojection/MediaProjectionMetricsLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/MediaProjectionMetricsLogger.kt @@ -19,7 +19,11 @@ import android.media.projection.IMediaProjectionManager import android.os.Process import android.os.RemoteException import android.util.Log -import com.android.internal.util.FrameworkStatsLog +import com.android.internal.util.FrameworkStatsLog.MEDIA_PROJECTION_STATE_CHANGED__CREATION_SOURCE__CREATION_SOURCE_APP as METRICS_CREATION_SOURCE_APP +import com.android.internal.util.FrameworkStatsLog.MEDIA_PROJECTION_STATE_CHANGED__CREATION_SOURCE__CREATION_SOURCE_CAST as METRICS_CREATION_SOURCE_CAST +import com.android.internal.util.FrameworkStatsLog.MEDIA_PROJECTION_STATE_CHANGED__CREATION_SOURCE__CREATION_SOURCE_SYSTEM_UI_SCREEN_RECORDER as METRICS_CREATION_SOURCE_SYSTEM_UI_SCREEN_RECORDER +import com.android.internal.util.FrameworkStatsLog.MEDIA_PROJECTION_STATE_CHANGED__CREATION_SOURCE__CREATION_SOURCE_UNKNOWN as METRICS_CREATION_SOURCE_UNKNOWN +import com.android.internal.util.FrameworkStatsLog.MEDIA_PROJECTION_STATE_CHANGED__STATE__MEDIA_PROJECTION_STATE_INITIATED import com.android.systemui.dagger.SysUISingleton import javax.inject.Inject @@ -36,21 +40,23 @@ constructor(private val service: IMediaProjectionManager) { * * @param sessionCreationSource The entry point requesting permission to capture. */ - fun notifyPermissionProgress(state: Int, sessionCreationSource: Int) { - // TODO check that state & SessionCreationSource matches expected values - notifyToServer(state, sessionCreationSource) + fun notifyProjectionInitiated(sessionCreationSource: SessionCreationSource) { + notifyToServer( + MEDIA_PROJECTION_STATE_CHANGED__STATE__MEDIA_PROJECTION_STATE_INITIATED, + sessionCreationSource + ) } /** * Request to log that the permission request moved to the given state. * - * Should not be used for the initialization state, since that + * Should not be used for the initialization state, since that should use {@link + * MediaProjectionMetricsLogger#notifyProjectionInitiated(Int)} and pass the + * sessionCreationSource. */ fun notifyPermissionProgress(state: Int) { // TODO validate state is valid - notifyToServer( - state, - FrameworkStatsLog.MEDIA_PROJECTION_STATE_CHANGED__STATE__MEDIA_PROJECTION_STATE_UNKNOWN) + notifyToServer(state, SessionCreationSource.UNKNOWN) } /** @@ -64,16 +70,21 @@ constructor(private val service: IMediaProjectionManager) { * Indicates the entry point for requesting the permission. Must be a valid state defined in * the SessionCreationSource enum. */ - private fun notifyToServer(state: Int, sessionCreationSource: Int) { + private fun notifyToServer(state: Int, sessionCreationSource: SessionCreationSource) { Log.v(TAG, "FOO notifyToServer of state $state and source $sessionCreationSource") try { service.notifyPermissionRequestStateChange( - Process.myUid(), state, sessionCreationSource) + Process.myUid(), + state, + sessionCreationSource.toMetricsConstant() + ) } catch (e: RemoteException) { Log.e( TAG, - "Error notifying server of permission flow state $state from source $sessionCreationSource", - e) + "Error notifying server of permission flow state $state from source " + + "$sessionCreationSource", + e + ) } } @@ -81,3 +92,18 @@ constructor(private val service: IMediaProjectionManager) { const val TAG = "MediaProjectionMetricsLogger" } } + +enum class SessionCreationSource { + APP, + CAST, + SYSTEM_UI_SCREEN_RECORDER, + UNKNOWN; + + fun toMetricsConstant(): Int = + when (this) { + APP -> METRICS_CREATION_SOURCE_APP + CAST -> METRICS_CREATION_SOURCE_CAST + SYSTEM_UI_SCREEN_RECORDER -> METRICS_CREATION_SOURCE_SYSTEM_UI_SCREEN_RECORDER + UNKNOWN -> METRICS_CREATION_SOURCE_UNKNOWN + } +} diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorActivity.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorActivity.kt index b5d3e913cadb..0bbcfd9de24c 100644 --- a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorActivity.kt +++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorActivity.kt @@ -87,14 +87,15 @@ class MediaProjectionAppSelectorActivity( override fun getLayoutResource() = R.layout.media_projection_app_selector - public override fun onCreate(bundle: Bundle?) { + public override fun onCreate(savedInstanceState: Bundle?) { lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE) component = componentFactory.create( hostUserHandle = hostUserHandle, callingPackage = callingPackage, view = this, - resultHandler = this + resultHandler = this, + isFirstStart = savedInstanceState == null ) component.lifecycleObservers.forEach { lifecycle.addObserver(it) } @@ -113,7 +114,7 @@ class MediaProjectionAppSelectorActivity( reviewGrantedConsentRequired = intent.getBooleanExtra(EXTRA_USER_REVIEW_GRANTED_CONSENT, false) - super.onCreate(bundle) + super.onCreate(savedInstanceState) controller.init() // we override AppList's AccessibilityDelegate set in ResolverActivity.onCreate because in // our case this delegate must extend RecyclerViewAccessibilityDelegate, otherwise diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorComponent.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorComponent.kt index 2217509167ef..8c6f307c84d6 100644 --- a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorComponent.kt +++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorComponent.kt @@ -146,6 +146,9 @@ interface MediaProjectionAppSelectorComponent { @BindsInstance @MediaProjectionAppSelector callingPackage: String?, @BindsInstance view: MediaProjectionAppSelectorView, @BindsInstance resultHandler: MediaProjectionAppSelectorResultHandler, + // Whether the app selector is starting for the first time. False when it is re-starting + // due to a config change. + @BindsInstance @MediaProjectionAppSelector isFirstStart: Boolean, ): MediaProjectionAppSelectorComponent } diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorController.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorController.kt index fced117a8132..69132d3662d4 100644 --- a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorController.kt +++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorController.kt @@ -18,6 +18,8 @@ package com.android.systemui.mediaprojection.appselector import android.content.ComponentName import android.os.UserHandle +import com.android.internal.util.FrameworkStatsLog.MEDIA_PROJECTION_STATE_CHANGED__STATE__MEDIA_PROJECTION_STATE_APP_SELECTOR_DISPLAYED as STATE_APP_SELECTOR_DISPLAYED +import com.android.systemui.mediaprojection.MediaProjectionMetricsLogger import com.android.systemui.mediaprojection.appselector.data.RecentTask import com.android.systemui.mediaprojection.appselector.data.RecentTaskListProvider import com.android.systemui.mediaprojection.appselector.data.RecentTaskThumbnailLoader @@ -43,9 +45,17 @@ constructor( @MediaProjectionAppSelector private val appSelectorComponentName: ComponentName, @MediaProjectionAppSelector private val callerPackageName: String?, private val thumbnailLoader: RecentTaskThumbnailLoader, + @MediaProjectionAppSelector private val isFirstStart: Boolean, + private val logger: MediaProjectionMetricsLogger, ) { fun init() { + // Only log during the first start of the app selector. + // Don't log when the app selector restarts due to a config change. + if (isFirstStart) { + logger.notifyPermissionProgress(STATE_APP_SELECTOR_DISPLAYED) + } + scope.launch { val recentTasks = recentTaskListProvider.loadRecentTasks() diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/data/RecentTask.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/data/RecentTask.kt index a9e6c53b3bcd..e9b458271ef7 100644 --- a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/data/RecentTask.kt +++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/data/RecentTask.kt @@ -22,6 +22,7 @@ import android.content.ComponentName data class RecentTask( val taskId: Int, + val displayId: Int, @UserIdInt val userId: Int, val topActivityComponent: ComponentName?, val baseIntentComponent: ComponentName?, diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/data/RecentTaskListProvider.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/data/RecentTaskListProvider.kt index aa4c4e55c718..730aa620690a 100644 --- a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/data/RecentTaskListProvider.kt +++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/data/RecentTaskListProvider.kt @@ -60,6 +60,7 @@ constructor( .map { RecentTask( it.taskId, + it.displayId, it.userId, it.topActivity, it.baseIntent?.component, diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/view/MediaProjectionRecentsViewController.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/view/MediaProjectionRecentsViewController.kt index fd1a683dc78f..ba837dba5354 100644 --- a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/view/MediaProjectionRecentsViewController.kt +++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/view/MediaProjectionRecentsViewController.kt @@ -130,10 +130,10 @@ constructor( view.width, view.height ) - activityOptions.setPendingIntentBackgroundActivityStartMode( + activityOptions.pendingIntentBackgroundActivityStartMode = MODE_BACKGROUND_ACTIVITY_START_ALLOWED - ) activityOptions.launchCookie = launchCookie + activityOptions.launchDisplayId = task.displayId activityTaskManager.startActivityFromRecents(task.taskId, activityOptions.toBundle()) resultHandler.returnSelectedApp(launchCookie) diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionActivity.java b/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionActivity.java index d08d0400f354..fa418fc8b98b 100644 --- a/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionActivity.java +++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionActivity.java @@ -53,7 +53,9 @@ import android.view.Window; import com.android.systemui.flags.FeatureFlags; import com.android.systemui.flags.Flags; +import com.android.systemui.mediaprojection.MediaProjectionMetricsLogger; import com.android.systemui.mediaprojection.MediaProjectionServiceHelper; +import com.android.systemui.mediaprojection.SessionCreationSource; import com.android.systemui.mediaprojection.appselector.MediaProjectionAppSelectorActivity; import com.android.systemui.mediaprojection.devicepolicy.ScreenCaptureDevicePolicyResolver; import com.android.systemui.mediaprojection.devicepolicy.ScreenCaptureDisabledDialog; @@ -74,6 +76,7 @@ public class MediaProjectionPermissionActivity extends Activity private final FeatureFlags mFeatureFlags; private final Lazy<ScreenCaptureDevicePolicyResolver> mScreenCaptureDevicePolicyResolver; private final StatusBarManager mStatusBarManager; + private final MediaProjectionMetricsLogger mMediaProjectionMetricsLogger; private String mPackageName; private int mUid; @@ -90,15 +93,17 @@ public class MediaProjectionPermissionActivity extends Activity @Inject public MediaProjectionPermissionActivity(FeatureFlags featureFlags, Lazy<ScreenCaptureDevicePolicyResolver> screenCaptureDevicePolicyResolver, - StatusBarManager statusBarManager) { + StatusBarManager statusBarManager, + MediaProjectionMetricsLogger mediaProjectionMetricsLogger) { mFeatureFlags = featureFlags; mScreenCaptureDevicePolicyResolver = screenCaptureDevicePolicyResolver; mStatusBarManager = statusBarManager; + mMediaProjectionMetricsLogger = mediaProjectionMetricsLogger; } @Override - public void onCreate(Bundle icicle) { - super.onCreate(icicle); + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); final Intent launchingIntent = getIntent(); mReviewGrantedConsentRequired = launchingIntent.getBooleanExtra( @@ -133,6 +138,10 @@ public class MediaProjectionPermissionActivity extends Activity try { if (MediaProjectionServiceHelper.hasProjectionPermission(mUid, mPackageName)) { + if (savedInstanceState == null) { + mMediaProjectionMetricsLogger.notifyProjectionInitiated( + SessionCreationSource.APP); + } final IMediaProjection projection = MediaProjectionServiceHelper.createOrReuseProjection(mUid, mPackageName, mReviewGrantedConsentRequired); @@ -231,6 +240,13 @@ public class MediaProjectionPermissionActivity extends Activity mDialog = dialogBuilder.create(); } + if (savedInstanceState == null) { + mMediaProjectionMetricsLogger.notifyProjectionInitiated( + appName == null + ? SessionCreationSource.CAST + : SessionCreationSource.APP); + } + setUpDialog(mDialog); mDialog.show(); } diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingController.java b/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingController.java index 7a0c087caacf..f469c6b78e87 100644 --- a/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingController.java +++ b/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingController.java @@ -38,6 +38,8 @@ import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.flags.FeatureFlags; import com.android.systemui.flags.Flags; +import com.android.systemui.mediaprojection.MediaProjectionMetricsLogger; +import com.android.systemui.mediaprojection.SessionCreationSource; import com.android.systemui.mediaprojection.devicepolicy.ScreenCaptureDevicePolicyResolver; import com.android.systemui.mediaprojection.devicepolicy.ScreenCaptureDisabledDialog; import com.android.systemui.plugins.ActivityStarter; @@ -45,13 +47,13 @@ import com.android.systemui.settings.UserContextProvider; import com.android.systemui.settings.UserTracker; import com.android.systemui.statusbar.policy.CallbackController; +import dagger.Lazy; + import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.Executor; import javax.inject.Inject; -import dagger.Lazy; - /** * Helper class to initiate a screen recording */ @@ -71,6 +73,7 @@ public class RecordingController private final FeatureFlags mFlags; private final UserContextProvider mUserContextProvider; private final UserTracker mUserTracker; + private final MediaProjectionMetricsLogger mMediaProjectionMetricsLogger; protected static final String INTENT_UPDATE_STATE = "com.android.systemui.screenrecord.UPDATE_STATE"; @@ -115,7 +118,8 @@ public class RecordingController FeatureFlags flags, UserContextProvider userContextProvider, Lazy<ScreenCaptureDevicePolicyResolver> devicePolicyResolver, - UserTracker userTracker) { + UserTracker userTracker, + MediaProjectionMetricsLogger mediaProjectionMetricsLogger) { mMainExecutor = mainExecutor; mContext = context; mFlags = flags; @@ -123,6 +127,7 @@ public class RecordingController mBroadcastDispatcher = broadcastDispatcher; mUserContextProvider = userContextProvider; mUserTracker = userTracker; + mMediaProjectionMetricsLogger = mediaProjectionMetricsLogger; BroadcastOptions options = BroadcastOptions.makeBasic(); options.setInteractive(true); @@ -149,6 +154,9 @@ public class RecordingController return new ScreenCaptureDisabledDialog(mContext); } + mMediaProjectionMetricsLogger.notifyProjectionInitiated( + SessionCreationSource.SYSTEM_UI_SCREEN_RECORDER); + return flags.isEnabled(Flags.WM_ENABLE_PARTIAL_SCREEN_SHARING) ? new ScreenRecordPermissionDialog(context, getHostUserHandle(), this, activityStarter, mUserContextProvider, onStartRecordingClicked) diff --git a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessDialog.java b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessDialog.java index 6783afa3eb9d..1ecb127f0c92 100644 --- a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessDialog.java +++ b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessDialog.java @@ -21,6 +21,7 @@ import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; import static android.view.WindowManagerPolicyConstants.EXTRA_FROM_BRIGHTNESS_KEY; import android.app.Activity; +import android.content.res.Configuration; import android.graphics.Rect; import android.os.Bundle; import android.view.Gravity; @@ -35,8 +36,8 @@ import android.widget.FrameLayout; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; -import com.android.systemui.res.R; import com.android.systemui.dagger.qualifiers.Main; +import com.android.systemui.res.R; import com.android.systemui.statusbar.policy.AccessibilityManagerWrapper; import com.android.systemui.util.concurrency.DelayableExecutor; @@ -74,21 +75,26 @@ public class BrightnessDialog extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + setWindowAttributes(); + setContentView(R.layout.brightness_mirror_container); + setBrightnessDialogViewAttributes(); + } + private void setWindowAttributes() { final Window window = getWindow(); - window.setGravity(Gravity.TOP | Gravity.CENTER_HORIZONTAL); + window.setGravity(Gravity.TOP | Gravity.LEFT); window.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND); window.requestFeature(Window.FEATURE_NO_TITLE); // Calling this creates the decor View, so setLayout takes proper effect // (see Dialog#onWindowAttributesChanged) window.getDecorView(); - window.setLayout( - WindowManager.LayoutParams.MATCH_PARENT, WindowManager.LayoutParams.WRAP_CONTENT); + window.setLayout(WRAP_CONTENT, WRAP_CONTENT); getTheme().applyStyle(R.style.Theme_SystemUI_QuickSettings, false); + } - setContentView(R.layout.brightness_mirror_container); + void setBrightnessDialogViewAttributes() { FrameLayout frame = findViewById(R.id.brightness_mirror_container); // The brightness mirror container is INVISIBLE by default. frame.setVisibility(View.VISIBLE); @@ -97,6 +103,14 @@ public class BrightnessDialog extends Activity { getResources().getDimensionPixelSize(R.dimen.notification_side_paddings); lp.leftMargin = horizontalMargin; lp.rightMargin = horizontalMargin; + + int verticalMargin = + getResources().getDimensionPixelSize( + R.dimen.notification_guts_option_vertical_padding); + + lp.topMargin = verticalMargin; + lp.bottomMargin = verticalMargin; + frame.setLayoutParams(lp); Rect bounds = new Rect(); frame.addOnLayoutChangeListener( @@ -113,6 +127,20 @@ public class BrightnessDialog extends Activity { frame.addView(controller.getRootView(), MATCH_PARENT, WRAP_CONTENT); mBrightnessController = mBrightnessControllerFactory.create(controller); + + Configuration configuration = getResources().getConfiguration(); + int orientation = configuration.orientation; + + if (orientation == Configuration.ORIENTATION_LANDSCAPE) { + lp.width = getWindowManager().getDefaultDisplay().getWidth() / 2 + - lp.leftMargin * 2; + } else if (orientation == Configuration.ORIENTATION_PORTRAIT) { + lp.width = getWindowManager().getDefaultDisplay().getWidth() + - lp.leftMargin * 2; + } + + frame.setLayoutParams(lp); + } @Override diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/MediaArtworkProcessor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/MediaArtworkProcessor.kt deleted file mode 100644 index 17b4e3baef13..000000000000 --- a/packages/SystemUI/src/com/android/systemui/statusbar/MediaArtworkProcessor.kt +++ /dev/null @@ -1,101 +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 - */ - -package com.android.systemui.statusbar - -import android.content.Context -import android.graphics.Bitmap -import android.graphics.Canvas -import android.graphics.Point -import android.graphics.Rect -import android.renderscript.Allocation -import android.renderscript.Element -import android.renderscript.RenderScript -import android.renderscript.ScriptIntrinsicBlur -import android.util.Log -import android.util.MathUtils -import com.android.internal.graphics.ColorUtils -import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.statusbar.notification.MediaNotificationProcessor -import javax.inject.Inject - -private const val TAG = "MediaArtworkProcessor" -private const val COLOR_ALPHA = (255 * 0.7f).toInt() -private const val BLUR_RADIUS = 25f -private const val DOWNSAMPLE = 6 - -@SysUISingleton -class MediaArtworkProcessor @Inject constructor() { - - private val mTmpSize = Point() - private var mArtworkCache: Bitmap? = null - - fun processArtwork(context: Context, artwork: Bitmap): Bitmap? { - if (mArtworkCache != null) { - return mArtworkCache - } - val renderScript = RenderScript.create(context) - val blur = ScriptIntrinsicBlur.create(renderScript, Element.U8_4(renderScript)) - var input: Allocation? = null - var output: Allocation? = null - var inBitmap: Bitmap? = null - try { - @Suppress("DEPRECATION") - context.display?.getSize(mTmpSize) - val rect = Rect(0, 0, artwork.width, artwork.height) - MathUtils.fitRect(rect, Math.max(mTmpSize.x / DOWNSAMPLE, mTmpSize.y / DOWNSAMPLE)) - inBitmap = Bitmap.createScaledBitmap(artwork, rect.width(), rect.height(), - true /* filter */) - // Render script blurs only support ARGB_8888, we need a conversion if we got a - // different bitmap config. - if (inBitmap.config != Bitmap.Config.ARGB_8888) { - val oldIn = inBitmap - inBitmap = oldIn.copy(Bitmap.Config.ARGB_8888, false /* isMutable */) - oldIn.recycle() - } - val outBitmap = Bitmap.createBitmap(inBitmap?.width ?: 0, inBitmap?.height ?: 0, - Bitmap.Config.ARGB_8888) - - input = Allocation.createFromBitmap(renderScript, inBitmap, - Allocation.MipmapControl.MIPMAP_NONE, Allocation.USAGE_GRAPHICS_TEXTURE) - output = Allocation.createFromBitmap(renderScript, outBitmap) - - blur.setRadius(BLUR_RADIUS) - blur.setInput(input) - blur.forEach(output) - output.copyTo(outBitmap) - - val swatch = MediaNotificationProcessor.findBackgroundSwatch(artwork) - - val canvas = Canvas(outBitmap) - canvas.drawColor(ColorUtils.setAlphaComponent(swatch.rgb, COLOR_ALPHA)) - return outBitmap - } catch (ex: IllegalArgumentException) { - Log.e(TAG, "error while processing artwork", ex) - return null - } finally { - input?.destroy() - output?.destroy() - blur.destroy() - inBitmap?.recycle() - } - } - - fun clearCache() { - mArtworkCache?.recycle() - mArtworkCache = null - } -}
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java index 5bd40b8ed714..389486f0ada3 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java @@ -15,43 +15,29 @@ */ package com.android.systemui.statusbar; -import static com.android.systemui.statusbar.StatusBarState.KEYGUARD; -import static com.android.systemui.statusbar.phone.CentralSurfaces.DEBUG_MEDIA_FAKE_ARTWORK; -import static com.android.systemui.statusbar.phone.CentralSurfaces.ENABLE_LOCKSCREEN_WALLPAPER; -import static com.android.systemui.statusbar.phone.CentralSurfaces.SHOW_LOCKSCREEN_MEDIA_ARTWORK; - -import android.annotation.MainThread; import android.annotation.NonNull; import android.annotation.Nullable; import android.app.Notification; import android.app.WallpaperManager; import android.content.Context; -import android.graphics.Bitmap; import android.graphics.Point; -import android.graphics.drawable.BitmapDrawable; -import android.graphics.drawable.ColorDrawable; -import android.graphics.drawable.Drawable; import android.graphics.drawable.Icon; import android.hardware.display.DisplayManager; import android.media.MediaMetadata; import android.media.session.MediaController; import android.media.session.MediaSession; import android.media.session.PlaybackState; -import android.os.AsyncTask; import android.os.Trace; import android.service.notification.NotificationStats; import android.service.notification.StatusBarNotification; -import android.util.ArraySet; import android.util.Log; import android.view.Display; import android.view.View; import android.widget.ImageView; -import com.android.app.animation.Interpolators; import com.android.internal.annotations.VisibleForTesting; import com.android.systemui.Dumpable; import com.android.systemui.colorextraction.SysuiColorExtractor; -import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dump.DumpManager; import com.android.systemui.media.controls.models.player.MediaData; import com.android.systemui.media.controls.models.recommendation.SmartspaceMediaData; @@ -65,18 +51,11 @@ import com.android.systemui.statusbar.notification.collection.notifcollection.Di import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener; import com.android.systemui.statusbar.notification.collection.render.NotificationVisibilityProvider; import com.android.systemui.statusbar.phone.BiometricUnlockController; -import com.android.systemui.statusbar.phone.KeyguardBypassController; import com.android.systemui.statusbar.phone.LockscreenWallpaper; import com.android.systemui.statusbar.phone.ScrimController; -import com.android.systemui.statusbar.phone.ScrimState; import com.android.systemui.statusbar.policy.KeyguardStateController; -import com.android.systemui.util.Utils; -import com.android.systemui.util.concurrency.DelayableExecutor; - -import dagger.Lazy; import java.io.PrintWriter; -import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -85,7 +64,6 @@ import java.util.HashSet; import java.util.List; import java.util.Objects; import java.util.Optional; -import java.util.Set; import java.util.stream.Collectors; /** @@ -99,7 +77,6 @@ public class NotificationMediaManager implements Dumpable { private final StatusBarStateController mStatusBarStateController; private final SysuiColorExtractor mColorExtractor; private final KeyguardStateController mKeyguardStateController; - private final KeyguardBypassController mKeyguardBypassController; private static final HashSet<Integer> PAUSED_MEDIA_STATES = new HashSet<>(); private static final HashSet<Integer> CONNECTING_MEDIA_STATES = new HashSet<>(); static { @@ -117,9 +94,6 @@ public class NotificationMediaManager implements Dumpable { private final NotifCollection mNotifCollection; @Nullable - private Lazy<NotificationShadeWindowController> mNotificationShadeWindowController; - - @Nullable private BiometricUnlockController mBiometricUnlockController; @Nullable private ScrimController mScrimController; @@ -128,12 +102,8 @@ public class NotificationMediaManager implements Dumpable { @VisibleForTesting boolean mIsLockscreenLiveWallpaperEnabled; - private final DelayableExecutor mMainExecutor; - private final Context mContext; private final ArrayList<MediaListener> mMediaListeners; - private final MediaArtworkProcessor mMediaArtworkProcessor; - private final Set<AsyncTask<?, ?, ?>> mProcessArtworkTasks = new ArraySet<>(); protected NotificationPresenter mPresenter; private MediaController mMediaController; @@ -150,8 +120,6 @@ public class NotificationMediaManager implements Dumpable { private List<String> mSmallerInternalDisplayUids; private Display mCurrentDisplay; - private LockscreenWallpaper.WallpaperDrawable mWallapperDrawable; - private final MediaController.Callback mMediaListener = new MediaController.Callback() { @Override public void onPlaybackStateChanged(PlaybackState state) { @@ -173,7 +141,6 @@ public class NotificationMediaManager implements Dumpable { if (DEBUG_MEDIA) { Log.v(TAG, "DEBUG_MEDIA: onMetadataChanged: " + metadata); } - mMediaArtworkProcessor.clearCache(); mMediaMetadata = metadata; dispatchUpdateMediaMetaData(true /* changed */, true /* allowAnimation */); } @@ -184,13 +151,9 @@ public class NotificationMediaManager implements Dumpable { */ public NotificationMediaManager( Context context, - Lazy<NotificationShadeWindowController> notificationShadeWindowController, NotificationVisibilityProvider visibilityProvider, - MediaArtworkProcessor mediaArtworkProcessor, - KeyguardBypassController keyguardBypassController, NotifPipeline notifPipeline, NotifCollection notifCollection, - @Main DelayableExecutor mainExecutor, MediaDataManager mediaDataManager, StatusBarStateController statusBarStateController, SysuiColorExtractor colorExtractor, @@ -199,12 +162,8 @@ public class NotificationMediaManager implements Dumpable { WallpaperManager wallpaperManager, DisplayManager displayManager) { mContext = context; - mMediaArtworkProcessor = mediaArtworkProcessor; - mKeyguardBypassController = keyguardBypassController; mMediaListeners = new ArrayList<>(); - mNotificationShadeWindowController = notificationShadeWindowController; mVisibilityProvider = visibilityProvider; - mMainExecutor = mainExecutor; mMediaDataManager = mediaDataManager; mNotifPipeline = notifPipeline; mNotifCollection = notifCollection; @@ -476,7 +435,6 @@ public class NotificationMediaManager implements Dumpable { } private void clearCurrentMediaNotificationSession() { - mMediaArtworkProcessor.clearCache(); mMediaMetadata = null; if (mMediaController != null) { if (DEBUG_MEDIA) { @@ -494,9 +452,6 @@ public class NotificationMediaManager implements Dumpable { public void onDisplayUpdated(Display display) { Trace.beginSection("NotificationMediaManager#onDisplayUpdated"); mCurrentDisplay = display; - if (mWallapperDrawable != null) { - mWallapperDrawable.onDisplayUpdated(isOnSmallerInternalDisplays()); - } Trace.endSection(); } @@ -531,18 +486,13 @@ public class NotificationMediaManager implements Dumpable { } /** - * Refresh or remove lockscreen artwork from media metadata or the lockscreen wallpaper. + * Update media state of lockscreen media views and controllers . */ - public void updateMediaMetaData(boolean metaDataChanged, boolean allowEnterAnimation) { + public void updateMediaMetaData(boolean metaDataChanged) { if (mIsLockscreenLiveWallpaperEnabled) return; Trace.beginSection("CentralSurfaces#updateMediaMetaData"); - if (!SHOW_LOCKSCREEN_MEDIA_ARTWORK) { - Trace.endSection(); - return; - } - if (getBackDropView() == null) { Trace.endSection(); return; // called too early @@ -566,168 +516,12 @@ public class NotificationMediaManager implements Dumpable { + " state=" + mStatusBarStateController.getState()); } - Bitmap artworkBitmap = null; - if (mediaMetadata != null && !mKeyguardBypassController.getBypassEnabled()) { - artworkBitmap = mediaMetadata.getBitmap(MediaMetadata.METADATA_KEY_ART); - if (artworkBitmap == null) { - artworkBitmap = mediaMetadata.getBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART); - } - } - - // Process artwork on a background thread and send the resulting bitmap to - // finishUpdateMediaMetaData. - if (metaDataChanged) { - for (AsyncTask<?, ?, ?> task : mProcessArtworkTasks) { - task.cancel(true); - } - mProcessArtworkTasks.clear(); - } - if (artworkBitmap != null && !Utils.useQsMediaPlayer(mContext)) { - mProcessArtworkTasks.add(new ProcessArtworkTask(this, metaDataChanged, - allowEnterAnimation).execute(artworkBitmap)); - } else { - finishUpdateMediaMetaData(metaDataChanged, allowEnterAnimation, null); - } - - Trace.endSection(); - } - - private void finishUpdateMediaMetaData(boolean metaDataChanged, boolean allowEnterAnimation, - @Nullable Bitmap bmp) { - Drawable artworkDrawable = null; - if (bmp != null) { - artworkDrawable = new BitmapDrawable(mBackdropBack.getResources(), bmp); - } - boolean hasMediaArtwork = artworkDrawable != null; - boolean allowWhenShade = false; - if (ENABLE_LOCKSCREEN_WALLPAPER && artworkDrawable == null) { - Bitmap lockWallpaper = - mLockscreenWallpaper != null ? mLockscreenWallpaper.getBitmap() : null; - if (lockWallpaper != null) { - artworkDrawable = new LockscreenWallpaper.WallpaperDrawable( - mBackdropBack.getResources(), lockWallpaper, isOnSmallerInternalDisplays()); - // We're in the SHADE mode on the SIM screen - yet we still need to show - // the lockscreen wallpaper in that mode. - allowWhenShade = mStatusBarStateController.getState() == KEYGUARD; - } - } - - NotificationShadeWindowController windowController = - mNotificationShadeWindowController.get(); - boolean hideBecauseOccluded = mKeyguardStateController.isOccluded(); - - final boolean hasArtwork = artworkDrawable != null; - mColorExtractor.setHasMediaArtwork(hasMediaArtwork); + mColorExtractor.setHasMediaArtwork(false); if (mScrimController != null) { - mScrimController.setHasBackdrop(hasArtwork); + mScrimController.setHasBackdrop(false); } - if ((hasArtwork || DEBUG_MEDIA_FAKE_ARTWORK) - && (mStatusBarStateController.getState() != StatusBarState.SHADE || allowWhenShade) - && mBiometricUnlockController != null && mBiometricUnlockController.getMode() - != BiometricUnlockController.MODE_WAKE_AND_UNLOCK_PULSING - && !hideBecauseOccluded) { - // time to show some art! - if (mBackdrop.getVisibility() != View.VISIBLE) { - mBackdrop.setVisibility(View.VISIBLE); - if (allowEnterAnimation) { - mBackdrop.setAlpha(0); - mBackdrop.animate().alpha(1f); - } else { - mBackdrop.animate().cancel(); - mBackdrop.setAlpha(1f); - } - if (windowController != null) { - windowController.setBackdropShowing(true); - } - metaDataChanged = true; - if (DEBUG_MEDIA) { - Log.v(TAG, "DEBUG_MEDIA: Fading in album artwork"); - } - } - if (metaDataChanged) { - if (mBackdropBack.getDrawable() != null) { - Drawable drawable = - mBackdropBack.getDrawable().getConstantState() - .newDrawable(mBackdropFront.getResources()).mutate(); - mBackdropFront.setImageDrawable(drawable); - mBackdropFront.setAlpha(1f); - mBackdropFront.setVisibility(View.VISIBLE); - } else { - mBackdropFront.setVisibility(View.INVISIBLE); - } - - if (DEBUG_MEDIA_FAKE_ARTWORK) { - final int c = 0xFF000000 | (int)(Math.random() * 0xFFFFFF); - Log.v(TAG, String.format("DEBUG_MEDIA: setting new color: 0x%08x", c)); - mBackdropBack.setBackgroundColor(0xFFFFFFFF); - mBackdropBack.setImageDrawable(new ColorDrawable(c)); - } else { - if (artworkDrawable instanceof LockscreenWallpaper.WallpaperDrawable) { - mWallapperDrawable = - (LockscreenWallpaper.WallpaperDrawable) artworkDrawable; - } - mBackdropBack.setImageDrawable(artworkDrawable); - } - - if (mBackdropFront.getVisibility() == View.VISIBLE) { - if (DEBUG_MEDIA) { - Log.v(TAG, "DEBUG_MEDIA: Crossfading album artwork from " - + mBackdropFront.getDrawable() - + " to " - + mBackdropBack.getDrawable()); - } - mBackdropFront.animate() - .setDuration(250) - .alpha(0f).withEndAction(mHideBackdropFront); - } - } - } else { - // need to hide the album art, either because we are unlocked, on AOD - // or because the metadata isn't there to support it - if (mBackdrop.getVisibility() != View.GONE) { - if (DEBUG_MEDIA) { - Log.v(TAG, "DEBUG_MEDIA: Fading out album artwork"); - } - boolean cannotAnimateDoze = mStatusBarStateController.isDozing() - && !ScrimState.AOD.getAnimateChange(); - if (((mBiometricUnlockController != null && mBiometricUnlockController.getMode() - == BiometricUnlockController.MODE_WAKE_AND_UNLOCK_PULSING - || cannotAnimateDoze)) - || hideBecauseOccluded) { - // We are unlocking directly - no animation! - mBackdrop.setVisibility(View.GONE); - mBackdropBack.setImageDrawable(null); - if (windowController != null) { - windowController.setBackdropShowing(false); - } - } else { - if (windowController != null) { - windowController.setBackdropShowing(false); - } - mBackdrop.animate() - .alpha(0) - .setInterpolator(Interpolators.ACCELERATE_DECELERATE) - .setDuration(300) - .setStartDelay(0) - .withEndAction(() -> { - mBackdrop.setVisibility(View.GONE); - mBackdropFront.animate().cancel(); - mBackdropBack.setImageDrawable(null); - mMainExecutor.execute(mHideBackdropFront); - }); - if (mKeyguardStateController.isKeyguardFadingAway()) { - mBackdrop.animate() - .setDuration( - mKeyguardStateController.getShortenedFadingAwayDuration()) - .setStartDelay( - mKeyguardStateController.getKeyguardFadingAwayDelay()) - .setInterpolator(Interpolators.LINEAR) - .start(); - } - } - } - } + Trace.endSection(); } public void setup(BackDropView backdrop, ImageView backdropFront, ImageView backdropBack, @@ -758,15 +552,6 @@ public class NotificationMediaManager implements Dumpable { } }; - private Bitmap processArtwork(Bitmap artwork) { - return mMediaArtworkProcessor.processArtwork(mContext, artwork); - } - - @MainThread - private void removeTask(AsyncTask<?, ?, ?> task) { - mProcessArtworkTasks.remove(task); - } - // TODO(b/273443374): remove public boolean isLockscreenWallpaperOnNotificationShade() { return mBackdrop != null && mLockscreenWallpaper != null @@ -780,52 +565,6 @@ public class NotificationMediaManager implements Dumpable { return mBackdrop; } - /** - * {@link AsyncTask} to prepare album art for use as backdrop on lock screen. - */ - private static final class ProcessArtworkTask extends AsyncTask<Bitmap, Void, Bitmap> { - - private final WeakReference<NotificationMediaManager> mManagerRef; - private final boolean mMetaDataChanged; - private final boolean mAllowEnterAnimation; - - ProcessArtworkTask(NotificationMediaManager manager, boolean changed, - boolean allowAnimation) { - mManagerRef = new WeakReference<>(manager); - mMetaDataChanged = changed; - mAllowEnterAnimation = allowAnimation; - } - - @Override - protected Bitmap doInBackground(Bitmap... bitmaps) { - NotificationMediaManager manager = mManagerRef.get(); - if (manager == null || bitmaps.length == 0 || isCancelled()) { - return null; - } - return manager.processArtwork(bitmaps[0]); - } - - @Override - protected void onPostExecute(@Nullable Bitmap result) { - NotificationMediaManager manager = mManagerRef.get(); - if (manager != null && !isCancelled()) { - manager.removeTask(this); - manager.finishUpdateMediaMetaData(mMetaDataChanged, mAllowEnterAnimation, result); - } - } - - @Override - protected void onCancelled(Bitmap result) { - if (result != null) { - result.recycle(); - } - NotificationMediaManager manager = mManagerRef.get(); - if (manager != null) { - manager.removeTask(this); - } - } - } - public interface MediaListener { /** * Called whenever there's new metadata or playback state. diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java index 7f5829d81c6f..125c8efe1884 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java @@ -31,7 +31,6 @@ import com.android.systemui.animation.DialogLaunchAnimator; import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor; import com.android.systemui.colorextraction.SysuiColorExtractor; import com.android.systemui.dagger.SysUISingleton; -import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dump.DumpHandler; import com.android.systemui.dump.DumpManager; import com.android.systemui.flags.FeatureFlags; @@ -45,12 +44,10 @@ import com.android.systemui.shade.ShadeSurface; import com.android.systemui.shade.carrier.ShadeCarrierGroupController; import com.android.systemui.statusbar.ActionClickLogger; import com.android.systemui.statusbar.CommandQueue; -import com.android.systemui.statusbar.MediaArtworkProcessor; import com.android.systemui.statusbar.NotificationClickNotifier; import com.android.systemui.statusbar.NotificationLockscreenUserManager; import com.android.systemui.statusbar.NotificationMediaManager; import com.android.systemui.statusbar.NotificationRemoteInputManager; -import com.android.systemui.statusbar.NotificationShadeWindowController; import com.android.systemui.statusbar.SmartReplyController; import com.android.systemui.statusbar.StatusBarStateControllerImpl; import com.android.systemui.statusbar.SysuiStatusBarStateController; @@ -61,7 +58,6 @@ import com.android.systemui.statusbar.notification.collection.NotifCollection; import com.android.systemui.statusbar.notification.collection.NotifPipeline; import com.android.systemui.statusbar.notification.collection.render.NotificationVisibilityProvider; import com.android.systemui.statusbar.phone.CentralSurfacesImpl; -import com.android.systemui.statusbar.phone.KeyguardBypassController; import com.android.systemui.statusbar.phone.ManagedProfileController; import com.android.systemui.statusbar.phone.ManagedProfileControllerImpl; import com.android.systemui.statusbar.phone.StatusBarIconController; @@ -71,7 +67,6 @@ import com.android.systemui.statusbar.phone.StatusBarNotificationPresenterModule import com.android.systemui.statusbar.phone.StatusBarRemoteInputCallback; import com.android.systemui.statusbar.policy.KeyguardStateController; import com.android.systemui.statusbar.policy.RemoteInputUriController; -import com.android.systemui.util.concurrency.DelayableExecutor; import dagger.Binds; import dagger.Lazy; @@ -122,13 +117,9 @@ public interface CentralSurfacesDependenciesModule { @Provides static NotificationMediaManager provideNotificationMediaManager( Context context, - Lazy<NotificationShadeWindowController> notificationShadeWindowController, NotificationVisibilityProvider visibilityProvider, - MediaArtworkProcessor mediaArtworkProcessor, - KeyguardBypassController keyguardBypassController, NotifPipeline notifPipeline, NotifCollection notifCollection, - @Main DelayableExecutor mainExecutor, MediaDataManager mediaDataManager, StatusBarStateController statusBarStateController, SysuiColorExtractor colorExtractor, @@ -138,13 +129,9 @@ public interface CentralSurfacesDependenciesModule { DisplayManager displayManager) { return new NotificationMediaManager( context, - notificationShadeWindowController, visibilityProvider, - mediaArtworkProcessor, - keyguardBypassController, notifPipeline, notifCollection, - mainExecutor, mediaDataManager, statusBarStateController, colorExtractor, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/MediaNotificationProcessor.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/MediaNotificationProcessor.java deleted file mode 100644 index 732c115571f8..000000000000 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/MediaNotificationProcessor.java +++ /dev/null @@ -1,131 +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.systemui.statusbar.notification; - -import android.graphics.Bitmap; -import android.graphics.Color; - -import androidx.palette.graphics.Palette; - -import java.util.List; - -/** - * A gutted class that now contains only a color extraction utility used by the - * MediaArtworkProcessor, which has otherwise supplanted this. - * - * TODO(b/182926117): move this into MediaArtworkProcessor.kt - */ -public class MediaNotificationProcessor { - - /** - * The population fraction to select a white or black color as the background over a color. - */ - private static final float POPULATION_FRACTION_FOR_WHITE_OR_BLACK = 2.5f; - private static final float BLACK_MAX_LIGHTNESS = 0.08f; - private static final float WHITE_MIN_LIGHTNESS = 0.90f; - private static final int RESIZE_BITMAP_AREA = 150 * 150; - - private MediaNotificationProcessor() { - } - - /** - * Finds an appropriate background swatch from media artwork. - * - * @param artwork Media artwork - * @return Swatch that should be used as the background of the media notification. - */ - public static Palette.Swatch findBackgroundSwatch(Bitmap artwork) { - return findBackgroundSwatch(generateArtworkPaletteBuilder(artwork).generate()); - } - - /** - * Finds an appropriate background swatch from the palette of media artwork. - * - * @param palette Artwork palette, should be obtained from {@link generateArtworkPaletteBuilder} - * @return Swatch that should be used as the background of the media notification. - */ - public static Palette.Swatch findBackgroundSwatch(Palette palette) { - // by default we use the dominant palette - Palette.Swatch dominantSwatch = palette.getDominantSwatch(); - if (dominantSwatch == null) { - return new Palette.Swatch(Color.WHITE, 100); - } - - if (!isWhiteOrBlack(dominantSwatch.getHsl())) { - return dominantSwatch; - } - // Oh well, we selected black or white. Lets look at the second color! - List<Palette.Swatch> swatches = palette.getSwatches(); - float highestNonWhitePopulation = -1; - Palette.Swatch second = null; - for (Palette.Swatch swatch : swatches) { - if (swatch != dominantSwatch - && swatch.getPopulation() > highestNonWhitePopulation - && !isWhiteOrBlack(swatch.getHsl())) { - second = swatch; - highestNonWhitePopulation = swatch.getPopulation(); - } - } - if (second == null) { - return dominantSwatch; - } - if (dominantSwatch.getPopulation() / highestNonWhitePopulation - > POPULATION_FRACTION_FOR_WHITE_OR_BLACK) { - // The dominant swatch is very dominant, lets take it! - // We're not filtering on white or black - return dominantSwatch; - } else { - return second; - } - } - - /** - * Generate a palette builder for media artwork. - * - * For producing a smooth background transition, the palette is extracted from only the left - * side of the artwork. - * - * @param artwork Media artwork - * @return Builder that generates the {@link Palette} for the media artwork. - */ - public static Palette.Builder generateArtworkPaletteBuilder(Bitmap artwork) { - // for the background we only take the left side of the image to ensure - // a smooth transition - return Palette.from(artwork) - .setRegion(0, 0, artwork.getWidth() / 2, artwork.getHeight()) - .clearFilters() // we want all colors, red / white / black ones too! - .resizeBitmapArea(RESIZE_BITMAP_AREA); - } - - private static boolean isWhiteOrBlack(float[] hsl) { - return isBlack(hsl) || isWhite(hsl); - } - - /** - * @return true if the color represents a color which is close to black. - */ - private static boolean isBlack(float[] hslColor) { - return hslColor[2] <= BLACK_MAX_LIGHTNESS; - } - - /** - * @return true if the color represents a color which is close to white. - */ - private static boolean isWhite(float[] hslColor) { - return hslColor[2] >= WHITE_MIN_LIGHTNESS; - } -} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java index 2809cad34143..8129b83a22d9 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java @@ -17,8 +17,6 @@ package com.android.systemui.statusbar.phone; import static android.app.StatusBarManager.SESSION_KEYGUARD; -import static com.android.systemui.flags.Flags.ONE_WAY_HAPTICS_API_MIGRATION; -import static com.android.systemui.keyguard.WakefulnessLifecycle.UNKNOWN_LAST_WAKE_TIME; import android.annotation.IntDef; import android.content.res.Resources; @@ -30,7 +28,6 @@ import android.metrics.LogMaker; import android.os.Handler; import android.os.PowerManager; import android.os.Trace; -import android.view.HapticFeedbackConstants; import androidx.annotation.Nullable; @@ -47,16 +44,17 @@ import com.android.keyguard.KeyguardUpdateMonitorCallback; import com.android.keyguard.KeyguardViewController; import com.android.keyguard.logging.BiometricUnlockLogger; import com.android.systemui.Dumpable; -import com.android.systemui.res.R; import com.android.systemui.biometrics.AuthController; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Main; +import com.android.systemui.deviceentry.domain.interactor.DeviceEntryHapticsInteractor; import com.android.systemui.dump.DumpManager; import com.android.systemui.flags.FeatureFlags; import com.android.systemui.keyguard.KeyguardViewMediator; import com.android.systemui.keyguard.WakefulnessLifecycle; import com.android.systemui.log.SessionTracker; import com.android.systemui.plugins.statusbar.StatusBarStateController; +import com.android.systemui.res.R; import com.android.systemui.statusbar.NotificationMediaManager; import com.android.systemui.statusbar.NotificationShadeWindowController; import com.android.systemui.statusbar.VibratorHelper; @@ -73,12 +71,14 @@ import java.util.Set; import javax.inject.Inject; +import kotlinx.coroutines.ExperimentalCoroutinesApi; + /** * Controller which coordinates all the biometric unlocking actions with the UI. */ +@ExperimentalCoroutinesApi @SysUISingleton public class BiometricUnlockController extends KeyguardUpdateMonitorCallback implements Dumpable { - private static final long RECENT_POWER_BUTTON_PRESS_THRESHOLD_MS = 400L; private static final long BIOMETRIC_WAKELOCK_TIMEOUT_MS = 15 * 1000; private static final String BIOMETRIC_WAKE_LOCK_NAME = "wake-and-unlock:wakelock"; private static final UiEventLogger UI_EVENT_LOGGER = new UiEventLoggerImpl(); @@ -175,6 +175,7 @@ public class BiometricUnlockController extends KeyguardUpdateMonitorCallback imp private final BiometricUnlockLogger mLogger; private final SystemClock mSystemClock; private final boolean mOrderUnlockAndWake; + private final DeviceEntryHapticsInteractor mHapticsInteractor; private long mLastFpFailureUptimeMillis; private int mNumConsecutiveFpFailures; @@ -284,7 +285,8 @@ public class BiometricUnlockController extends KeyguardUpdateMonitorCallback imp ScreenOffAnimationController screenOffAnimationController, VibratorHelper vibrator, SystemClock systemClock, - FeatureFlags featureFlags + FeatureFlags featureFlags, + DeviceEntryHapticsInteractor hapticsInteractor ) { mPowerManager = powerManager; mUpdateMonitor = keyguardUpdateMonitor; @@ -314,6 +316,7 @@ public class BiometricUnlockController extends KeyguardUpdateMonitorCallback imp mFeatureFlags = featureFlags; mOrderUnlockAndWake = resources.getBoolean( com.android.internal.R.bool.config_orderUnlockAndWake); + mHapticsInteractor = hapticsInteractor; dumpManager.registerDumpable(this); } @@ -434,7 +437,7 @@ public class BiometricUnlockController extends KeyguardUpdateMonitorCallback imp if (mode == MODE_WAKE_AND_UNLOCK || mode == MODE_WAKE_AND_UNLOCK_PULSING || mode == MODE_UNLOCK_COLLAPSING || mode == MODE_WAKE_AND_UNLOCK_FROM_DREAM || mode == MODE_DISMISS_BOUNCER) { - vibrateSuccess(biometricSourceType); + mHapticsInteractor.vibrateSuccess(); onBiometricUnlockedWithKeyguardDismissal(biometricSourceType); } startWakeAndUnlock(mode); @@ -498,8 +501,7 @@ public class BiometricUnlockController extends KeyguardUpdateMonitorCallback imp case MODE_WAKE_AND_UNLOCK: if (mMode == MODE_WAKE_AND_UNLOCK_PULSING) { Trace.beginSection("MODE_WAKE_AND_UNLOCK_PULSING"); - mMediaManager.updateMediaMetaData(false /* metaDataChanged */, - true /* allowEnterAnimation */); + mMediaManager.updateMediaMetaData(false /* metaDataChanged */); } else if (mMode == MODE_WAKE_AND_UNLOCK){ Trace.beginSection("MODE_WAKE_AND_UNLOCK"); } else { @@ -723,7 +725,7 @@ public class BiometricUnlockController extends KeyguardUpdateMonitorCallback imp && !mUpdateMonitor.getCachedIsUnlockWithFingerprintPossible( KeyguardUpdateMonitor.getCurrentUser())) || (biometricSourceType == BiometricSourceType.FINGERPRINT)) { - vibrateError(biometricSourceType); + mHapticsInteractor.vibrateError(); } cleanup(); @@ -750,45 +752,6 @@ public class BiometricUnlockController extends KeyguardUpdateMonitorCallback imp cleanup(); } - // these haptics are for device-entry only - private void vibrateSuccess(BiometricSourceType type) { - if (mAuthController.isSfpsEnrolled(KeyguardUpdateMonitor.getCurrentUser()) - && lastWakeupFromPowerButtonWithinHapticThreshold()) { - mLogger.d("Skip auth success haptic. Power button was recently pressed."); - return; - } - if (mFeatureFlags.isEnabled(ONE_WAY_HAPTICS_API_MIGRATION)) { - mVibratorHelper.performHapticFeedback( - mKeyguardViewController.getViewRootImpl().getView(), - HapticFeedbackConstants.CONFIRM - ); - } else { - mVibratorHelper.vibrateAuthSuccess( - getClass().getSimpleName() + ", type =" + type + "device-entry::success"); - } - } - - private boolean lastWakeupFromPowerButtonWithinHapticThreshold() { - final boolean lastWakeupFromPowerButton = mWakefulnessLifecycle.getLastWakeReason() - == PowerManager.WAKE_REASON_POWER_BUTTON; - return lastWakeupFromPowerButton - && mWakefulnessLifecycle.getLastWakeTime() != UNKNOWN_LAST_WAKE_TIME - && mSystemClock.uptimeMillis() - mWakefulnessLifecycle.getLastWakeTime() - < RECENT_POWER_BUTTON_PRESS_THRESHOLD_MS; - } - - private void vibrateError(BiometricSourceType type) { - if (mFeatureFlags.isEnabled(ONE_WAY_HAPTICS_API_MIGRATION)) { - mVibratorHelper.performHapticFeedback( - mKeyguardViewController.getViewRootImpl().getView(), - HapticFeedbackConstants.REJECT - ); - } else { - mVibratorHelper.vibrateAuthError( - getClass().getSimpleName() + ", type =" + type + "device-entry::error"); - } - } - private void cleanup() { releaseBiometricWakeLock(); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LockscreenWallpaper.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LockscreenWallpaper.java index 92c786fb569f..00fd9fbfffe3 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LockscreenWallpaper.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LockscreenWallpaper.java @@ -263,8 +263,7 @@ public class LockscreenWallpaper extends IWallpaperManagerCallback.Stub implemen if (result.success) { mCached = true; mCache = result.bitmap; - mMediaManager.updateMediaMetaData( - true /* metaDataChanged */, true /* allowEnterAnimation */); + mMediaManager.updateMediaMetaData(true /* metaDataChanged */); } mLoader = null; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java index 3adf3385e3cc..400ac7b415b5 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java @@ -961,7 +961,7 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb SysUiStatsLog.KEYGUARD_STATE_CHANGED__STATE__SHOWN); } if (isShowing) { - mMediaManager.updateMediaMetaData(false, animate && !isOccluded); + mMediaManager.updateMediaMetaData(false); } mNotificationShadeWindowController.setKeyguardOccluded(isOccluded); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java index 2d14f6b3c508..57a8e6fe0d91 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java @@ -221,7 +221,7 @@ class StatusBarNotificationPresenter implements NotificationPresenter, CommandQu @Override public void updateMediaMetaData(boolean metaDataChanged, boolean allowEnterAnimation) { - mMediaManager.updateMediaMetaData(metaDataChanged, allowEnterAnimation); + mMediaManager.updateMediaMetaData(metaDataChanged); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryViaTrackerLib.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryViaTrackerLib.kt index e9e52a2397e1..1670dd39ba24 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryViaTrackerLib.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryViaTrackerLib.kt @@ -104,7 +104,7 @@ constructor( val callback = object : WifiPickerTracker.WifiPickerTrackerCallback { override fun onWifiEntriesChanged() { - val connectedEntry = wifiPickerTracker?.connectedWifiEntry + val connectedEntry = wifiPickerTracker.mergedOrPrimaryConnection logOnWifiEntriesChanged(connectedEntry) val secondaryNetworks = @@ -217,6 +217,21 @@ constructor( .stateIn(scope, SharingStarted.Eagerly, emptyList()) /** + * [WifiPickerTracker.getConnectedWifiEntry] stores a [MergedCarrierEntry] separately from the + * [WifiEntry] for the primary connection. Therefore, we have to prefer the carrier-merged entry + * if it exists, falling back on the connected entry if null + */ + private val WifiPickerTracker?.mergedOrPrimaryConnection: WifiEntry? + get() { + val mergedEntry: MergedCarrierEntry? = this?.mergedCarrierEntry + return if (mergedEntry != null && mergedEntry.isDefaultNetwork) { + mergedEntry + } else { + this?.connectedWifiEntry + } + } + + /** * Converts WifiTrackerLib's [WifiEntry] into our internal model only if the entry is the * primary network. Returns an inactive network if it's not primary. */ diff --git a/packages/SystemUI/src/com/android/systemui/util/sensors/AsyncSensorManager.java b/packages/SystemUI/src/com/android/systemui/util/sensors/AsyncSensorManager.java index 9b06a37e681f..d56672564f6f 100644 --- a/packages/SystemUI/src/com/android/systemui/util/sensors/AsyncSensorManager.java +++ b/packages/SystemUI/src/com/android/systemui/util/sensors/AsyncSensorManager.java @@ -191,7 +191,7 @@ public class AsyncSensorManager extends SensorManager } @Override - protected boolean initDataInjectionImpl(boolean enable) { + protected boolean initDataInjectionImpl(boolean enable, @DataInjectionMode int mode) { throw new UnsupportedOperationException("not implemented"); } 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 new file mode 100644 index 000000000000..9b8e581d1ba4 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/deviceentry/data/repository/DeviceEntryHapticsInteractorTest.kt @@ -0,0 +1,195 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.deviceentry.data.repository + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.keyguard.logging.BiometricUnlockLogger +import com.android.systemui.SysuiTestCase +import com.android.systemui.biometrics.data.repository.FakeFingerprintPropertyRepository +import com.android.systemui.biometrics.shared.model.FingerprintSensorType +import com.android.systemui.biometrics.shared.model.SensorStrength +import com.android.systemui.classifier.FalsingCollector +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.deviceentry.domain.interactor.DeviceEntryHapticsInteractor +import com.android.systemui.keyevent.data.repository.FakeKeyEventRepository +import com.android.systemui.keyevent.domain.interactor.KeyEventInteractor +import com.android.systemui.keyguard.data.repository.FakeBiometricSettingsRepository +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.power.shared.model.WakeSleepReason +import com.android.systemui.power.shared.model.WakefulnessState +import com.android.systemui.statusbar.phone.ScreenOffAnimationController +import com.android.systemui.util.time.FakeSystemClock +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito.mock + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@RunWith(AndroidJUnit4::class) +class DeviceEntryHapticsInteractorTest : SysuiTestCase() { + + private lateinit var repository: DeviceEntryHapticsRepository + private lateinit var fingerprintPropertyRepository: FakeFingerprintPropertyRepository + private lateinit var biometricSettingsRepository: FakeBiometricSettingsRepository + private lateinit var keyEventRepository: FakeKeyEventRepository + private lateinit var powerRepository: FakePowerRepository + private lateinit var systemClock: FakeSystemClock + private lateinit var underTest: DeviceEntryHapticsInteractor + + @Before + fun setUp() { + repository = DeviceEntryHapticsRepositoryImpl() + fingerprintPropertyRepository = FakeFingerprintPropertyRepository() + biometricSettingsRepository = FakeBiometricSettingsRepository() + keyEventRepository = FakeKeyEventRepository() + powerRepository = FakePowerRepository() + systemClock = FakeSystemClock() + underTest = + DeviceEntryHapticsInteractor( + repository = repository, + fingerprintPropertyRepository = fingerprintPropertyRepository, + biometricSettingsRepository = biometricSettingsRepository, + keyEventInteractor = KeyEventInteractor(keyEventRepository), + powerInteractor = + PowerInteractor( + powerRepository, + mock(FalsingCollector::class.java), + mock(ScreenOffAnimationController::class.java), + mock(StatusBarStateController::class.java), + ), + systemClock = systemClock, + logger = mock(BiometricUnlockLogger::class.java), + ) + } + + @Test + fun nonPowerButtonFPS_vibrateSuccess() = runTest { + val playSuccessHaptic by collectLastValue(underTest.playSuccessHaptic) + setFingerprintSensorType(FingerprintSensorType.UDFPS_ULTRASONIC) + underTest.vibrateSuccess() + assertThat(playSuccessHaptic).isTrue() + } + + @Test + fun powerButtonFPS_vibrateSuccess() = runTest { + val playSuccessHaptic by collectLastValue(underTest.playSuccessHaptic) + setPowerButtonFingerprintProperty() + setFingerprintEnrolled() + keyEventRepository.setPowerButtonDown(false) + + // It's been 10 seconds since the last power button wakeup + setAwakeFromPowerButton() + runCurrent() + systemClock.setUptimeMillis(systemClock.uptimeMillis() + 10000) + + underTest.vibrateSuccess() + assertThat(playSuccessHaptic).isTrue() + } + + @Test + fun powerButtonFPS_powerDown_doNotVibrateSuccess() = runTest { + val playSuccessHaptic by collectLastValue(underTest.playSuccessHaptic) + setPowerButtonFingerprintProperty() + setFingerprintEnrolled() + keyEventRepository.setPowerButtonDown(true) // power button is currently DOWN + + // It's been 10 seconds since the last power button wakeup + setAwakeFromPowerButton() + runCurrent() + systemClock.setUptimeMillis(systemClock.uptimeMillis() + 10000) + + underTest.vibrateSuccess() + assertThat(playSuccessHaptic).isFalse() + } + + @Test + fun powerButtonFPS_powerButtonRecentlyPressed_doNotVibrateSuccess() = runTest { + val playSuccessHaptic by collectLastValue(underTest.playSuccessHaptic) + setPowerButtonFingerprintProperty() + setFingerprintEnrolled() + keyEventRepository.setPowerButtonDown(false) + + // It's only been 50ms since the last power button wakeup + setAwakeFromPowerButton() + runCurrent() + systemClock.setUptimeMillis(systemClock.uptimeMillis() + 50) + + underTest.vibrateSuccess() + assertThat(playSuccessHaptic).isFalse() + } + + @Test + fun nonPowerButtonFPS_vibrateError() = runTest { + val playErrorHaptic by collectLastValue(underTest.playErrorHaptic) + setFingerprintSensorType(FingerprintSensorType.UDFPS_ULTRASONIC) + underTest.vibrateError() + assertThat(playErrorHaptic).isTrue() + } + + @Test + fun powerButtonFPS_vibrateError() = runTest { + val playErrorHaptic by collectLastValue(underTest.playErrorHaptic) + setPowerButtonFingerprintProperty() + setFingerprintEnrolled() + underTest.vibrateError() + assertThat(playErrorHaptic).isTrue() + } + + @Test + fun powerButtonFPS_powerDown_doNotVibrateError() = runTest { + val playErrorHaptic by collectLastValue(underTest.playErrorHaptic) + setPowerButtonFingerprintProperty() + setFingerprintEnrolled() + keyEventRepository.setPowerButtonDown(true) + underTest.vibrateError() + assertThat(playErrorHaptic).isFalse() + } + + private fun setFingerprintSensorType(fingerprintSensorType: FingerprintSensorType) { + fingerprintPropertyRepository.setProperties( + sensorId = 0, + strength = SensorStrength.STRONG, + sensorType = fingerprintSensorType, + sensorLocations = mapOf(), + ) + } + + private fun setPowerButtonFingerprintProperty() { + setFingerprintSensorType(FingerprintSensorType.POWER_BUTTON) + } + + private fun setFingerprintEnrolled() { + biometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(true) + } + + private fun setAwakeFromPowerButton() { + powerRepository.updateWakefulness( + WakefulnessState.AWAKE, + WakeSleepReason.POWER_BUTTON, + WakeSleepReason.POWER_BUTTON, + powerButtonLaunchGestureTriggered = false, + ) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorControllerTest.kt index 34360d2ddd5c..6cdf4efd67da 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorControllerTest.kt @@ -4,7 +4,9 @@ import android.content.ComponentName import android.os.UserHandle import android.testing.AndroidTestingRunner import androidx.test.filters.SmallTest +import com.android.internal.util.FrameworkStatsLog.MEDIA_PROJECTION_STATE_CHANGED__STATE__MEDIA_PROJECTION_STATE_APP_SELECTOR_DISPLAYED as STATE_APP_SELECTOR_DISPLAYED import com.android.systemui.SysuiTestCase +import com.android.systemui.mediaprojection.MediaProjectionMetricsLogger import com.android.systemui.mediaprojection.appselector.data.RecentTask import com.android.systemui.mediaprojection.appselector.data.RecentTaskListProvider import com.android.systemui.mediaprojection.appselector.data.RecentTaskThumbnailLoader @@ -20,6 +22,7 @@ import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import org.mockito.Mockito.never import org.mockito.Mockito.verify @RunWith(AndroidTestingRunner::class) @@ -37,10 +40,11 @@ class MediaProjectionAppSelectorControllerTest : SysuiTestCase() { private val view: MediaProjectionAppSelectorView = mock() private val policyResolver: ScreenCaptureDevicePolicyResolver = mock() + private val logger = mock<MediaProjectionMetricsLogger>() private val thumbnailLoader = FakeThumbnailLoader() - private val controller = + private fun createController(isFirstStart: Boolean = true) = MediaProjectionAppSelectorController( taskListProvider, view, @@ -50,6 +54,8 @@ class MediaProjectionAppSelectorControllerTest : SysuiTestCase() { appSelectorComponentName, callerPackageName, thumbnailLoader, + isFirstStart, + logger ) @Before @@ -61,7 +67,7 @@ class MediaProjectionAppSelectorControllerTest : SysuiTestCase() { fun initNoRecentTasks_bindsEmptyList() { taskListProvider.tasks = emptyList() - controller.init() + createController().init() verify(view).bind(emptyList()) } @@ -70,7 +76,7 @@ class MediaProjectionAppSelectorControllerTest : SysuiTestCase() { fun initOneRecentTask_bindsList() { taskListProvider.tasks = listOf(createRecentTask(taskId = 1)) - controller.init() + createController().init() verify(view).bind(listOf(createRecentTask(taskId = 1))) } @@ -86,7 +92,7 @@ class MediaProjectionAppSelectorControllerTest : SysuiTestCase() { ) taskListProvider.tasks = tasks - controller.init() + createController().init() assertThat(thumbnailLoader.capturedTaskIds).containsExactly(2, 3) } @@ -101,7 +107,7 @@ class MediaProjectionAppSelectorControllerTest : SysuiTestCase() { ) taskListProvider.tasks = tasks - controller.init() + createController().init() verify(view) .bind( @@ -124,7 +130,7 @@ class MediaProjectionAppSelectorControllerTest : SysuiTestCase() { ) taskListProvider.tasks = tasks - controller.init() + createController().init() verify(view) .bind( @@ -147,7 +153,7 @@ class MediaProjectionAppSelectorControllerTest : SysuiTestCase() { ) taskListProvider.tasks = tasks - controller.init() + createController().init() verify(view) .bind( @@ -172,7 +178,7 @@ class MediaProjectionAppSelectorControllerTest : SysuiTestCase() { ) taskListProvider.tasks = tasks - controller.init() + createController().init() verify(view) .bind( @@ -199,11 +205,29 @@ class MediaProjectionAppSelectorControllerTest : SysuiTestCase() { taskListProvider.tasks = tasks givenCaptureAllowed(isAllow = false) - controller.init() + createController().init() verify(view).bind(emptyList()) } + @Test + fun init_firstStart_logsAppSelectorDisplayed() { + val controller = createController(isFirstStart = true) + + controller.init() + + verify(logger).notifyPermissionProgress(STATE_APP_SELECTOR_DISPLAYED) + } + + @Test + fun init_notFirstStart_doesNotLogAppSelectorDisplayed() { + val controller = createController(isFirstStart = false) + + controller.init() + + verify(logger, never()).notifyPermissionProgress(STATE_APP_SELECTOR_DISPLAYED) + } + private fun givenCaptureAllowed(isAllow: Boolean) { whenever(policyResolver.isScreenCaptureAllowed(any(), any())).thenReturn(isAllow) } @@ -216,6 +240,7 @@ class MediaProjectionAppSelectorControllerTest : SysuiTestCase() { ): RecentTask { return RecentTask( taskId = taskId, + displayId = 0, topActivityComponent = topActivityComponent, baseIntentComponent = ComponentName("com", "Test"), userId = userId, diff --git a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/appselector/data/ShellRecentTaskListProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/appselector/data/ShellRecentTaskListProviderTest.kt index 2c7ee56e9408..d75553fe57ab 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/appselector/data/ShellRecentTaskListProviderTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/appselector/data/ShellRecentTaskListProviderTest.kt @@ -128,6 +128,7 @@ class ShellRecentTaskListProviderTest : SysuiTestCase() { private fun createRecentTask(taskId: Int): RecentTask = RecentTask( taskId = taskId, + displayId = 0, userId = 0, topActivityComponent = null, baseIntentComponent = null, diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingControllerTest.java index 6e6833dc0944..90d2e78a411f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingControllerTest.java @@ -17,8 +17,10 @@ package com.android.systemui.screenrecord; import static com.google.common.truth.Truth.assertThat; + import static junit.framework.Assert.assertFalse; import static junit.framework.Assert.assertTrue; + import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.verify; @@ -37,6 +39,8 @@ import com.android.systemui.animation.DialogLaunchAnimator; import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.flags.FakeFeatureFlags; import com.android.systemui.flags.Flags; +import com.android.systemui.mediaprojection.MediaProjectionMetricsLogger; +import com.android.systemui.mediaprojection.SessionCreationSource; import com.android.systemui.mediaprojection.devicepolicy.ScreenCaptureDevicePolicyResolver; import com.android.systemui.mediaprojection.devicepolicy.ScreenCaptureDisabledDialog; import com.android.systemui.plugins.ActivityStarter; @@ -76,6 +80,8 @@ public class RecordingControllerTest extends SysuiTestCase { private ActivityStarter mActivityStarter; @Mock private UserTracker mUserTracker; + @Mock + private MediaProjectionMetricsLogger mMediaProjectionMetricsLogger; private FakeFeatureFlags mFeatureFlags; private RecordingController mController; @@ -86,8 +92,15 @@ public class RecordingControllerTest extends SysuiTestCase { public void setUp() { MockitoAnnotations.initMocks(this); mFeatureFlags = new FakeFeatureFlags(); - mController = new RecordingController(mMainExecutor, mBroadcastDispatcher, mContext, - mFeatureFlags, mUserContextProvider, () -> mDevicePolicyResolver, mUserTracker); + mController = new RecordingController( + mMainExecutor, + mBroadcastDispatcher, + mContext, + mFeatureFlags, + mUserContextProvider, + () -> mDevicePolicyResolver, + mUserTracker, + mMediaProjectionMetricsLogger); mController.addCallback(mCallback); } @@ -269,4 +282,21 @@ public class RecordingControllerTest extends SysuiTestCase { assertThat(dialog).isInstanceOf(ScreenRecordPermissionDialog.class); } + + @Test + public void testPoliciesFlagEnabled_screenCapturingAllowed_logsProjectionInitiated() { + if (Looper.myLooper() == null) { + Looper.prepare(); + } + + mFeatureFlags.set(Flags.WM_ENABLE_PARTIAL_SCREEN_SHARING, true); + mFeatureFlags.set(Flags.WM_ENABLE_PARTIAL_SCREEN_SHARING_ENTERPRISE_POLICIES, true); + when(mDevicePolicyResolver.isScreenCaptureCompletelyDisabled((any()))).thenReturn(false); + + mController.createScreenRecordDialog(mContext, mFeatureFlags, + mDialogLaunchAnimator, mActivityStarter, /* onStartRecordingClicked= */ null); + + verify(mMediaProjectionMetricsLogger) + .notifyProjectionInitiated(SessionCreationSource.SYSTEM_UI_SCREEN_RECORDER); + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/MediaArtworkProcessorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/MediaArtworkProcessorTest.kt deleted file mode 100644 index e4da53a1a0a4..000000000000 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/MediaArtworkProcessorTest.kt +++ /dev/null @@ -1,109 +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 - */ - -package com.android.systemui.statusbar - -import com.google.common.truth.Truth.assertThat - -import android.graphics.Bitmap -import android.graphics.Canvas -import android.graphics.Color -import android.graphics.Point -import android.testing.AndroidTestingRunner -import androidx.test.filters.SmallTest -import com.android.systemui.SysuiTestCase -import org.junit.After -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith - -private const val WIDTH = 200 -private const val HEIGHT = 200 - -@RunWith(AndroidTestingRunner::class) -@SmallTest -class MediaArtworkProcessorTest : SysuiTestCase() { - - private var screenWidth = 0 - private var screenHeight = 0 - - private lateinit var processor: MediaArtworkProcessor - - @Before - fun setUp() { - processor = MediaArtworkProcessor() - - val point = Point() - checkNotNull(context.display).getSize(point) - screenWidth = point.x - screenHeight = point.y - } - - @After - fun tearDown() { - processor.clearCache() - } - - @Test - fun testProcessArtwork() { - // GIVEN some "artwork", which is just a solid blue image - val artwork = Bitmap.createBitmap(WIDTH, HEIGHT, Bitmap.Config.ARGB_8888) - Canvas(artwork).drawColor(Color.BLUE) - // WHEN the background is created from the artwork - val background = processor.processArtwork(context, artwork)!! - // THEN the background has the size of the screen that has been downsamples - assertThat(background.height).isLessThan(screenHeight) - assertThat(background.width).isLessThan(screenWidth) - assertThat(background.config).isEqualTo(Bitmap.Config.ARGB_8888) - } - - @Test - fun testCache() { - // GIVEN a solid blue image - val artwork = Bitmap.createBitmap(WIDTH, HEIGHT, Bitmap.Config.ARGB_8888) - Canvas(artwork).drawColor(Color.BLUE) - // WHEN the background is processed twice - val background1 = processor.processArtwork(context, artwork)!! - val background2 = processor.processArtwork(context, artwork)!! - // THEN the two bitmaps are the same - // Note: This is currently broken and trying to use caching causes issues - assertThat(background1).isNotSameInstanceAs(background2) - } - - @Test - fun testConfig() { - // GIVEN some which is not ARGB_8888 - val artwork = Bitmap.createBitmap(WIDTH, HEIGHT, Bitmap.Config.ALPHA_8) - Canvas(artwork).drawColor(Color.BLUE) - // WHEN the background is created from the artwork - val background = processor.processArtwork(context, artwork)!! - // THEN the background has Config ARGB_8888 - assertThat(background.config).isEqualTo(Bitmap.Config.ARGB_8888) - } - - @Test - fun testRecycledArtwork() { - // GIVEN some "artwork", which is just a solid blue image - val artwork = Bitmap.createBitmap(WIDTH, HEIGHT, Bitmap.Config.ARGB_8888) - Canvas(artwork).drawColor(Color.BLUE) - // AND the artwork is recycled - artwork.recycle() - // WHEN the background is created from the artwork - val background = processor.processArtwork(context, artwork) - // THEN the processed bitmap is null - assertThat(background).isNull() - } -} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationMediaManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationMediaManagerTest.kt index 9d6ea857fefc..cfcf4257ce28 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationMediaManagerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationMediaManagerTest.kt @@ -48,9 +48,7 @@ class NotificationMediaManagerTest : SysuiTestCase() { @Before fun setUp() { MockitoAnnotations.initMocks(this) - doCallRealMethod() - .whenever(notificationMediaManager) - .updateMediaMetaData(anyBoolean(), anyBoolean()) + doCallRealMethod().whenever(notificationMediaManager).updateMediaMetaData(anyBoolean()) doReturn(mockBackDropView).whenever(notificationMediaManager).backDropView } @@ -62,7 +60,7 @@ class NotificationMediaManagerTest : SysuiTestCase() { notificationMediaManager.mIsLockscreenLiveWallpaperEnabled = true for (metaDataChanged in listOf(true, false)) { for (allowEnterAnimation in listOf(true, false)) { - notificationMediaManager.updateMediaMetaData(metaDataChanged, allowEnterAnimation) + notificationMediaManager.updateMediaMetaData(metaDataChanged) verify(notificationMediaManager, never()).mediaMetadata } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/MediaNotificationProcessorTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/MediaNotificationProcessorTest.java deleted file mode 100644 index aeb5b037be0c..000000000000 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/MediaNotificationProcessorTest.java +++ /dev/null @@ -1,87 +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.systemui.statusbar.notification; - -import static com.google.common.truth.Truth.assertThat; - -import android.annotation.Nullable; -import android.graphics.Bitmap; -import android.graphics.Canvas; -import android.graphics.Color; -import android.test.suitebuilder.annotation.SmallTest; - -import androidx.palette.graphics.Palette; -import androidx.test.runner.AndroidJUnit4; - -import com.android.systemui.SysuiTestCase; - -import org.junit.After; -import org.junit.Test; -import org.junit.runner.RunWith; - -@SmallTest -@RunWith(AndroidJUnit4.class) -public class MediaNotificationProcessorTest extends SysuiTestCase { - - private static final int BITMAP_WIDTH = 10; - private static final int BITMAP_HEIGHT = 10; - - /** - * Color tolerance is borrowed from the AndroidX test utilities for Palette. - */ - private static final int COLOR_TOLERANCE = 8; - - @Nullable private Bitmap mArtwork; - - @After - public void tearDown() { - if (mArtwork != null) { - mArtwork.recycle(); - mArtwork = null; - } - } - - @Test - public void findBackgroundSwatch_white() { - // Given artwork that is completely white. - mArtwork = Bitmap.createBitmap(BITMAP_WIDTH, BITMAP_HEIGHT, Bitmap.Config.ARGB_8888); - Canvas canvas = new Canvas(mArtwork); - canvas.drawColor(Color.WHITE); - // WHEN the background swatch is computed - Palette.Swatch swatch = MediaNotificationProcessor.findBackgroundSwatch(mArtwork); - // THEN the swatch color is white - assertCloseColors(swatch.getRgb(), Color.WHITE); - } - - @Test - public void findBackgroundSwatch_red() { - // Given artwork that is completely red. - mArtwork = Bitmap.createBitmap(BITMAP_WIDTH, BITMAP_HEIGHT, Bitmap.Config.ARGB_8888); - Canvas canvas = new Canvas(mArtwork); - canvas.drawColor(Color.RED); - // WHEN the background swatch is computed - Palette.Swatch swatch = MediaNotificationProcessor.findBackgroundSwatch(mArtwork); - // THEN the swatch color is red - assertCloseColors(swatch.getRgb(), Color.RED); - } - - static void assertCloseColors(int expected, int actual) { - assertThat((float) Color.red(expected)).isWithin(COLOR_TOLERANCE).of(Color.red(actual)); - assertThat((float) Color.green(expected)).isWithin(COLOR_TOLERANCE).of(Color.green(actual)); - assertThat((float) Color.blue(expected)).isWithin(COLOR_TOLERANCE).of(Color.blue(actual)); - } -} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java index 700de5305778..8344cd143c5d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java @@ -18,7 +18,9 @@ package com.android.systemui.statusbar.phone; import static com.android.systemui.flags.Flags.ONE_WAY_HAPTICS_API_MIGRATION; import static com.android.systemui.statusbar.phone.BiometricUnlockController.MODE_WAKE_AND_UNLOCK; + import static com.google.common.truth.Truth.assertThat; + import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; @@ -38,7 +40,6 @@ import android.test.suitebuilder.annotation.SmallTest; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper.RunWithLooper; import android.testing.TestableResources; -import android.view.HapticFeedbackConstants; import android.view.ViewRootImpl; import com.android.internal.logging.MetricsLogger; @@ -47,6 +48,7 @@ import com.android.keyguard.KeyguardUpdateMonitor; import com.android.keyguard.logging.BiometricUnlockLogger; import com.android.systemui.SysuiTestCase; import com.android.systemui.biometrics.AuthController; +import com.android.systemui.deviceentry.domain.interactor.DeviceEntryHapticsInteractor; import com.android.systemui.dump.DumpManager; import com.android.systemui.flags.FakeFeatureFlags; import com.android.systemui.keyguard.KeyguardViewMediator; @@ -122,6 +124,8 @@ public class BiometricsUnlockControllerTest extends SysuiTestCase { private BiometricUnlockLogger mLogger; @Mock private ViewRootImpl mViewRootImpl; + @Mock + private DeviceEntryHapticsInteractor mDeviceEntryHapticsInteractor; private final FakeSystemClock mSystemClock = new FakeSystemClock(); private FakeFeatureFlags mFeatureFlags; private BiometricUnlockController mBiometricUnlockController; @@ -158,7 +162,8 @@ public class BiometricsUnlockControllerTest extends SysuiTestCase { mAuthController, mStatusBarStateController, mSessionTracker, mLatencyTracker, mScreenOffAnimationController, mVibratorHelper, mSystemClock, - mFeatureFlags + mFeatureFlags, + mDeviceEntryHapticsInteractor ); biometricUnlockController.setKeyguardViewController(mStatusBarKeyguardViewManager); biometricUnlockController.addListener(mBiometricUnlockEventsListener); @@ -462,145 +467,23 @@ public class BiometricsUnlockControllerTest extends SysuiTestCase { } @Test - public void onSideFingerprintSuccess_recentPowerButtonPress_noHaptic() { - // GIVEN side fingerprint enrolled, last wake reason was power button - when(mAuthController.isSfpsEnrolled(anyInt())).thenReturn(true); - when(mWakefulnessLifecycle.getLastWakeReason()) - .thenReturn(PowerManager.WAKE_REASON_POWER_BUTTON); - - // GIVEN last wake time just occurred - when(mWakefulnessLifecycle.getLastWakeTime()).thenReturn(mSystemClock.uptimeMillis()); - - // WHEN biometric fingerprint succeeds - givenFingerprintModeUnlockCollapsing(); - mBiometricUnlockController.startWakeAndUnlock(BiometricSourceType.FINGERPRINT, - true); - - // THEN DO NOT vibrate the device - verify(mVibratorHelper, never()).vibrateAuthSuccess(anyString()); - } - - @Test - public void onSideFingerprintSuccess_oldPowerButtonPress_playHaptic() { - // GIVEN side fingerprint enrolled, last wake reason was power button - when(mAuthController.isSfpsEnrolled(anyInt())).thenReturn(true); - when(mWakefulnessLifecycle.getLastWakeReason()) - .thenReturn(PowerManager.WAKE_REASON_POWER_BUTTON); - - // GIVEN last wake time was 500ms ago - when(mWakefulnessLifecycle.getLastWakeTime()).thenReturn(mSystemClock.uptimeMillis()); - mSystemClock.advanceTime(500); - - // WHEN biometric fingerprint succeeds - givenFingerprintModeUnlockCollapsing(); - mBiometricUnlockController.startWakeAndUnlock(BiometricSourceType.FINGERPRINT, - true); - - // THEN vibrate the device - verify(mVibratorHelper).vibrateAuthSuccess(anyString()); - } - - @Test - public void onSideFingerprintSuccess_oldPowerButtonPress_playOneWayHaptic() { - // GIVEN oneway haptics is enabled - mFeatureFlags.set(ONE_WAY_HAPTICS_API_MIGRATION, true); - // GIVEN side fingerprint enrolled, last wake reason was power button - when(mAuthController.isSfpsEnrolled(anyInt())).thenReturn(true); - when(mWakefulnessLifecycle.getLastWakeReason()) - .thenReturn(PowerManager.WAKE_REASON_POWER_BUTTON); - - // GIVEN last wake time was 500ms ago - when(mWakefulnessLifecycle.getLastWakeTime()).thenReturn(mSystemClock.uptimeMillis()); - mSystemClock.advanceTime(500); - + public void onFingerprintSuccess_requestSuccessHaptic() { // WHEN biometric fingerprint succeeds givenFingerprintModeUnlockCollapsing(); mBiometricUnlockController.startWakeAndUnlock(BiometricSourceType.FINGERPRINT, true); - // THEN vibrate the device - verify(mVibratorHelper).performHapticFeedback( - any(), - eq(HapticFeedbackConstants.CONFIRM) - ); - } - - @Test - public void onSideFingerprintSuccess_recentGestureWakeUp_playHaptic() { - // GIVEN side fingerprint enrolled, wakeup just happened - when(mAuthController.isSfpsEnrolled(anyInt())).thenReturn(true); - when(mWakefulnessLifecycle.getLastWakeTime()).thenReturn(mSystemClock.uptimeMillis()); - - // GIVEN last wake reason was from a gesture - when(mWakefulnessLifecycle.getLastWakeReason()) - .thenReturn(PowerManager.WAKE_REASON_GESTURE); - - // WHEN biometric fingerprint succeeds - givenFingerprintModeUnlockCollapsing(); - mBiometricUnlockController.startWakeAndUnlock(BiometricSourceType.FINGERPRINT, - true); - - // THEN vibrate the device - verify(mVibratorHelper).vibrateAuthSuccess(anyString()); - } - - @Test - public void onSideFingerprintSuccess_recentGestureWakeUp_playOnewayHaptic() { - //GIVEN oneway haptics is enabled - mFeatureFlags.set(ONE_WAY_HAPTICS_API_MIGRATION, true); - // GIVEN side fingerprint enrolled, wakeup just happened - when(mAuthController.isSfpsEnrolled(anyInt())).thenReturn(true); - when(mWakefulnessLifecycle.getLastWakeTime()).thenReturn(mSystemClock.uptimeMillis()); - - // GIVEN last wake reason was from a gesture - when(mWakefulnessLifecycle.getLastWakeReason()) - .thenReturn(PowerManager.WAKE_REASON_GESTURE); - - // WHEN biometric fingerprint succeeds - givenFingerprintModeUnlockCollapsing(); - mBiometricUnlockController.startWakeAndUnlock(BiometricSourceType.FINGERPRINT, - true); - - // THEN vibrate the device - verify(mVibratorHelper).performHapticFeedback( - any(), - eq(HapticFeedbackConstants.CONFIRM) - ); - } - - @Test - public void onSideFingerprintFail_alwaysPlaysHaptic() { - // GIVEN side fingerprint enrolled, last wake reason was recent power button - when(mAuthController.isSfpsEnrolled(anyInt())).thenReturn(true); - when(mWakefulnessLifecycle.getLastWakeReason()) - .thenReturn(PowerManager.WAKE_REASON_POWER_BUTTON); - when(mWakefulnessLifecycle.getLastWakeTime()).thenReturn(mSystemClock.uptimeMillis()); - - // WHEN biometric fingerprint fails - mBiometricUnlockController.onBiometricAuthFailed(BiometricSourceType.FINGERPRINT); - // THEN always vibrate the device - verify(mVibratorHelper).vibrateAuthError(anyString()); + verify(mDeviceEntryHapticsInteractor).vibrateSuccess(); } @Test - public void onSideFingerprintFail_alwaysPlaysOneWayHaptic() { - // GIVEN oneway haptics is enabled - mFeatureFlags.set(ONE_WAY_HAPTICS_API_MIGRATION, true); - // GIVEN side fingerprint enrolled, last wake reason was recent power button - when(mAuthController.isSfpsEnrolled(anyInt())).thenReturn(true); - when(mWakefulnessLifecycle.getLastWakeReason()) - .thenReturn(PowerManager.WAKE_REASON_POWER_BUTTON); - when(mWakefulnessLifecycle.getLastWakeTime()).thenReturn(mSystemClock.uptimeMillis()); - + public void onFingerprintFail_requestErrorHaptic() { // WHEN biometric fingerprint fails mBiometricUnlockController.onBiometricAuthFailed(BiometricSourceType.FINGERPRINT); // THEN always vibrate the device - verify(mVibratorHelper).performHapticFeedback( - any(), - eq(HapticFeedbackConstants.REJECT) - ); + verify(mDeviceEntryHapticsInteractor).vibrateError(); } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryViaTrackerLibTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryViaTrackerLibTest.kt index c2f56654e00a..31263627213d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryViaTrackerLibTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryViaTrackerLibTest.kt @@ -201,11 +201,11 @@ class WifiRepositoryViaTrackerLibTest : SysuiTestCase() { testScope.runTest { val latest by collectLastValue(underTest.isWifiDefault) - val wifiEntry = + val mergedEntry = mock<MergedCarrierEntry>().apply { whenever(this.isDefaultNetwork).thenReturn(true) } - whenever(wifiPickerTracker.connectedWifiEntry).thenReturn(wifiEntry) + whenever(wifiPickerTracker.mergedCarrierEntry).thenReturn(mergedEntry) getCallback().onWifiEntriesChanged() assertThat(latest).isTrue() @@ -229,11 +229,11 @@ class WifiRepositoryViaTrackerLibTest : SysuiTestCase() { testScope.runTest { val latest by collectLastValue(underTest.isWifiDefault) - val wifiEntry = + val mergedEntry = mock<MergedCarrierEntry>().apply { whenever(this.isDefaultNetwork).thenReturn(false) } - whenever(wifiPickerTracker.connectedWifiEntry).thenReturn(wifiEntry) + whenever(wifiPickerTracker.mergedCarrierEntry).thenReturn(mergedEntry) getCallback().onWifiEntriesChanged() assertThat(latest).isFalse() @@ -526,13 +526,14 @@ class WifiRepositoryViaTrackerLibTest : SysuiTestCase() { testScope.runTest { val latest by collectLastValue(underTest.wifiNetwork) - val wifiEntry = + val mergedEntry = mock<MergedCarrierEntry>().apply { whenever(this.isPrimaryNetwork).thenReturn(true) whenever(this.level).thenReturn(3) whenever(this.subscriptionId).thenReturn(567) + whenever(this.isDefaultNetwork).thenReturn(true) } - whenever(wifiPickerTracker.connectedWifiEntry).thenReturn(wifiEntry) + whenever(wifiPickerTracker.mergedCarrierEntry).thenReturn(mergedEntry) getCallback().onWifiEntriesChanged() assertThat(latest is WifiNetworkModel.CarrierMerged).isTrue() @@ -546,11 +547,12 @@ class WifiRepositoryViaTrackerLibTest : SysuiTestCase() { testScope.runTest { val latest by collectLastValue(underTest.wifiNetwork) - val wifiEntry = + val mergedEntry = mock<MergedCarrierEntry>().apply { whenever(this.isPrimaryNetwork).thenReturn(true) + whenever(this.isDefaultNetwork).thenReturn(true) } - whenever(wifiPickerTracker.connectedWifiEntry).thenReturn(wifiEntry) + whenever(wifiPickerTracker.mergedCarrierEntry).thenReturn(mergedEntry) whenever(wifiManager.maxSignalLevel).thenReturn(5) getCallback().onWifiEntriesChanged() @@ -566,12 +568,13 @@ class WifiRepositoryViaTrackerLibTest : SysuiTestCase() { testScope.runTest { val latest by collectLastValue(underTest.wifiNetwork) - val wifiEntry = + val mergedEntry = mock<MergedCarrierEntry>().apply { whenever(this.isPrimaryNetwork).thenReturn(true) whenever(this.subscriptionId).thenReturn(INVALID_SUBSCRIPTION_ID) + whenever(this.isDefaultNetwork).thenReturn(true) } - whenever(wifiPickerTracker.connectedWifiEntry).thenReturn(wifiEntry) + whenever(wifiPickerTracker.mergedCarrierEntry).thenReturn(mergedEntry) getCallback().onWifiEntriesChanged() @@ -628,11 +631,12 @@ class WifiRepositoryViaTrackerLibTest : SysuiTestCase() { testScope.runTest { val latest by collectLastValue(underTest.wifiNetwork) - val wifiEntry = + val mergedEntry = mock<MergedCarrierEntry>().apply { whenever(this.isPrimaryNetwork).thenReturn(false) } - whenever(wifiPickerTracker.connectedWifiEntry).thenReturn(wifiEntry) + whenever(wifiPickerTracker.mergedCarrierEntry).thenReturn(mergedEntry) + whenever(wifiPickerTracker.connectedWifiEntry).thenReturn(null) getCallback().onWifiEntriesChanged() assertThat(latest).isEqualTo(WifiNetworkModel.Inactive) @@ -717,12 +721,14 @@ class WifiRepositoryViaTrackerLibTest : SysuiTestCase() { testScope.runTest { val latest by collectLastValue(underTest.wifiNetwork) - val wifiEntry = + val mergedEntry = mock<MergedCarrierEntry>().apply { whenever(this.isPrimaryNetwork).thenReturn(true) whenever(this.level).thenReturn(3) + whenever(this.isDefaultNetwork).thenReturn(true) } - whenever(wifiPickerTracker.connectedWifiEntry).thenReturn(wifiEntry) + whenever(wifiPickerTracker.connectedWifiEntry).thenReturn(null) + whenever(wifiPickerTracker.mergedCarrierEntry).thenReturn(mergedEntry) getCallback().onWifiEntriesChanged() assertThat(latest is WifiNetworkModel.CarrierMerged).isTrue() @@ -730,6 +736,7 @@ class WifiRepositoryViaTrackerLibTest : SysuiTestCase() { // WHEN we lose our current network whenever(wifiPickerTracker.connectedWifiEntry).thenReturn(null) + whenever(wifiPickerTracker.mergedCarrierEntry).thenReturn(null) getCallback().onWifiEntriesChanged() // THEN we update to no network @@ -767,6 +774,56 @@ class WifiRepositoryViaTrackerLibTest : SysuiTestCase() { } @Test + fun wifiNetwork_carrierMerged_default_usesCarrierMergedInfo() = + testScope.runTest { + val latest by collectLastValue(underTest.wifiNetwork) + + val mergedEntry = + mock<MergedCarrierEntry>().apply { + whenever(this.isPrimaryNetwork).thenReturn(true) + whenever(this.level).thenReturn(3) + whenever(this.isDefaultNetwork).thenReturn(true) + } + val wifiEntry = + mock<WifiEntry>().apply { + whenever(this.isPrimaryNetwork).thenReturn(true) + whenever(this.level).thenReturn(1) + whenever(this.title).thenReturn(TITLE) + } + whenever(wifiPickerTracker.mergedCarrierEntry).thenReturn(mergedEntry) + whenever(wifiPickerTracker.connectedWifiEntry).thenReturn(wifiEntry) + + getCallback().onWifiEntriesChanged() + + assertThat(latest is WifiNetworkModel.CarrierMerged).isTrue() + } + + @Test + fun wifiNetwork_carrierMerged_notDefault_usesConnectedInfo() = + testScope.runTest { + val latest by collectLastValue(underTest.wifiNetwork) + + val mergedEntry = + mock<MergedCarrierEntry>().apply { + whenever(this.isPrimaryNetwork).thenReturn(true) + whenever(this.level).thenReturn(3) + whenever(this.isDefaultNetwork).thenReturn(false) + } + val wifiEntry = + mock<WifiEntry>().apply { + whenever(this.isPrimaryNetwork).thenReturn(true) + whenever(this.level).thenReturn(1) + whenever(this.title).thenReturn(TITLE) + } + whenever(wifiPickerTracker.mergedCarrierEntry).thenReturn(mergedEntry) + whenever(wifiPickerTracker.connectedWifiEntry).thenReturn(wifiEntry) + + getCallback().onWifiEntriesChanged() + + assertThat(latest is WifiNetworkModel.Active).isTrue() + } + + @Test fun secondaryNetworks_activeEntriesEmpty_isEmpty() = testScope.runTest { featureFlags.set(Flags.WIFI_SECONDARY_NETWORKS, true) diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/util/sensors/FakeSensorManager.java b/packages/SystemUI/tests/utils/src/com/android/systemui/util/sensors/FakeSensorManager.java index 197873f15d0d..288dcfe2825d 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/util/sensors/FakeSensorManager.java +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/util/sensors/FakeSensorManager.java @@ -186,7 +186,7 @@ public class FakeSensorManager extends SensorManager { } @Override - protected boolean initDataInjectionImpl(boolean enable) { + protected boolean initDataInjectionImpl(boolean enable, @DataInjectionMode int mode) { return false; } diff --git a/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureManagerService.java b/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureManagerService.java index ee41a69c6f4c..65975e44ee2a 100644 --- a/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureManagerService.java +++ b/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureManagerService.java @@ -1437,6 +1437,10 @@ public class ContentCaptureManagerService extends if (!mDevCfgEnableContentProtectionReceiver) { return false; } + if (mDevCfgContentProtectionRequiredGroups.isEmpty() + && mDevCfgContentProtectionOptionalGroups.isEmpty()) { + return false; + } } return mContentProtectionConsentManager.isConsentGranted(userId) && mContentProtectionBlocklistManager.isAllowed(packageName); diff --git a/services/core/Android.bp b/services/core/Android.bp index e5225f65f22a..1d02e4c88b74 100644 --- a/services/core/Android.bp +++ b/services/core/Android.bp @@ -191,7 +191,7 @@ java_library_static { "ImmutabilityAnnotation", "securebox", "apache-commons-math", - "power_optimization_flags_lib", + "backstage_power_flags_lib", "notification_flags_lib", "camera_platform_flags_core_java_lib", "biometrics_flags_lib", diff --git a/services/core/java/android/content/pm/PackageManagerInternal.java b/services/core/java/android/content/pm/PackageManagerInternal.java index 8df54569cccd..638abdba36ec 100644 --- a/services/core/java/android/content/pm/PackageManagerInternal.java +++ b/services/core/java/android/content/pm/PackageManagerInternal.java @@ -1387,11 +1387,12 @@ public abstract class PackageManagerInternal { @UserIdInt int userId); /** - * Tells PackageManager when a component (except BroadcastReceivers) of the package is used + * Tells PackageManager when a component of the package is used * and the package should get out of stopped state and be enabled. */ public abstract void notifyComponentUsed(@NonNull String packageName, - @UserIdInt int userId, @NonNull String recentCallingPackage, @NonNull String debugInfo); + @UserIdInt int userId, @Nullable String recentCallingPackage, + @NonNull String debugInfo); /** @deprecated For legacy shell command only. */ @Deprecated diff --git a/services/core/java/com/android/server/am/BroadcastQueueImpl.java b/services/core/java/com/android/server/am/BroadcastQueueImpl.java index 127c5b389d79..3c56752d08e3 100644 --- a/services/core/java/com/android/server/am/BroadcastQueueImpl.java +++ b/services/core/java/com/android/server/am/BroadcastQueueImpl.java @@ -1440,10 +1440,9 @@ public class BroadcastQueueImpl extends BroadcastQueue { r.curComponent.getPackageName(), r.userId, Event.APP_COMPONENT_USED); } - // Broadcast is being executed, its package can't be stopped. try { - mService.mPackageManagerInt.setPackageStoppedState( - r.curComponent.getPackageName(), false, r.userId); + mService.mPackageManagerInt.notifyComponentUsed( + r.curComponent.getPackageName(), r.userId, r.callerPackage, r.toString()); } catch (IllegalArgumentException e) { Slog.w(TAG, "Failed trying to unstop package " + r.curComponent.getPackageName() + ": " + e); diff --git a/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java b/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java index d19eae5b0709..b48169788180 100644 --- a/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java +++ b/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java @@ -1982,8 +1982,8 @@ class BroadcastQueueModernImpl extends BroadcastQueue { mService.notifyPackageUse(receiverPackageName, PackageManager.NOTIFY_PACKAGE_USE_BROADCAST_RECEIVER); - mService.mPackageManagerInt.setPackageStoppedState( - receiverPackageName, false, r.userId); + mService.mPackageManagerInt.notifyComponentUsed( + receiverPackageName, r.userId, r.callerPackage, r.toString()); } private void reportUsageStatsBroadcastDispatched(@NonNull ProcessRecord app, diff --git a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java index 16e3fdf2a6ab..2d231b3cf42e 100644 --- a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java +++ b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java @@ -152,6 +152,7 @@ public class SettingsToPropertiesMapper { "preload_safety", "responsible_apis", "rust", + "safety_center", "system_performance", "test_suites", "text", @@ -159,7 +160,14 @@ public class SettingsToPropertiesMapper { "tv_system_ui", "vibrator", "virtual_devices", + "wear_calling_messaging", + "wear_connectivity", + "wear_esim_carriers", "wear_frameworks", + "wear_health_services", + "wear_media", + "wear_offload", + "wear_security", "wear_system_health", "wear_systems", "window_surfaces", diff --git a/services/core/java/com/android/server/am/flags.aconfig b/services/core/java/com/android/server/am/flags.aconfig index 26d99d843c7e..bb9ea285385b 100644 --- a/services/core/java/com/android/server/am/flags.aconfig +++ b/services/core/java/com/android/server/am/flags.aconfig @@ -2,7 +2,7 @@ package: "com.android.server.am" flag { name: "oomadjuster_correctness_rewrite" - namespace: "android_platform_power_optimization" + namespace: "backstage_power" description: "Utilize new OomAdjuster implementation" bug: "298055811" is_fixed_read_only: true diff --git a/services/core/java/com/android/server/audio/MusicFxHelper.java b/services/core/java/com/android/server/audio/MusicFxHelper.java index 6c0fef5f628d..5f4e4c3bc4e0 100644 --- a/services/core/java/com/android/server/audio/MusicFxHelper.java +++ b/services/core/java/com/android/server/audio/MusicFxHelper.java @@ -157,7 +157,8 @@ public class MusicFxHelper { Log.w(TAG, " inside handle MSG_EFFECT_CLIENT_GONE"); // Once the uid is no longer running, close all remain audio session(s) for this UID if (mClientUidSessionMap.get(Integer.valueOf(uid)) != null) { - final List<Integer> sessions = mClientUidSessionMap.get(Integer.valueOf(uid)); + final List<Integer> sessions = + new ArrayList(mClientUidSessionMap.get(Integer.valueOf(uid))); Log.i(TAG, "UID " + uid + " gone, closing " + sessions.size() + " sessions"); for (Integer session : sessions) { Intent intent = new Intent( diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java index 35a4f475c878..5b87eeac938d 100644 --- a/services/core/java/com/android/server/display/DisplayManagerService.java +++ b/services/core/java/com/android/server/display/DisplayManagerService.java @@ -758,7 +758,8 @@ public final class DisplayManagerService extends SystemService { mContext.registerReceiver(mIdleModeReceiver, filter); - mSmallAreaDetectionController = SmallAreaDetectionController.create(mContext); + mSmallAreaDetectionController = (mFlags.isSmallAreaDetectionEnabled()) + ? SmallAreaDetectionController.create(mContext) : null; } @VisibleForTesting diff --git a/services/core/java/com/android/server/display/SmallAreaDetectionController.java b/services/core/java/com/android/server/display/SmallAreaDetectionController.java index adaa5390cb9b..bf384b02d95e 100644 --- a/services/core/java/com/android/server/display/SmallAreaDetectionController.java +++ b/services/core/java/com/android/server/display/SmallAreaDetectionController.java @@ -20,6 +20,7 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.content.Context; import android.content.pm.PackageManagerInternal; +import android.os.UserHandle; import android.provider.DeviceConfig; import android.provider.DeviceConfigInterface; import android.util.ArrayMap; @@ -30,15 +31,14 @@ import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.os.BackgroundThread; import com.android.server.LocalServices; -import com.android.server.pm.UserManagerInternal; +import com.android.server.pm.pkg.PackageStateInternal; import java.io.PrintWriter; -import java.util.Arrays; import java.util.Map; final class SmallAreaDetectionController { - private static native void nativeUpdateSmallAreaDetection(int[] uids, float[] thresholds); - private static native void nativeSetSmallAreaDetectionThreshold(int uid, float threshold); + private static native void nativeUpdateSmallAreaDetection(int[] appIds, float[] thresholds); + private static native void nativeSetSmallAreaDetectionThreshold(int appId, float threshold); // TODO(b/281720315): Move this to DeviceConfig once server side ready. private static final String KEY_SMALL_AREA_DETECTION_ALLOWLIST = @@ -47,12 +47,8 @@ final class SmallAreaDetectionController { private final Object mLock = new Object(); private final Context mContext; private final PackageManagerInternal mPackageManager; - private final UserManagerInternal mUserManager; @GuardedBy("mLock") private final Map<String, Float> mAllowPkgMap = new ArrayMap<>(); - // TODO(b/298722189): Update allowlist when user changes - @GuardedBy("mLock") - private int[] mUserIds; static SmallAreaDetectionController create(@NonNull Context context) { final SmallAreaDetectionController controller = @@ -67,7 +63,6 @@ final class SmallAreaDetectionController { SmallAreaDetectionController(Context context, DeviceConfigInterface deviceConfig) { mContext = context; mPackageManager = LocalServices.getService(PackageManagerInternal.class); - mUserManager = LocalServices.getService(UserManagerInternal.class); deviceConfig.addOnPropertiesChangedListener(DeviceConfig.NAMESPACE_DISPLAY_MANAGER, BackgroundThread.getExecutor(), new SmallAreaDetectionController.OnPropertiesChangedListener()); @@ -76,6 +71,7 @@ final class SmallAreaDetectionController { @VisibleForTesting void updateAllowlist(@Nullable String property) { + final Map<String, Float> allowPkgMap = new ArrayMap<>(); synchronized (mLock) { mAllowPkgMap.clear(); if (property != null) { @@ -86,8 +82,11 @@ final class SmallAreaDetectionController { .getStringArray(R.array.config_smallAreaDetectionAllowlist); for (String defaultMapString : defaultMapStrings) putToAllowlist(defaultMapString); } - updateSmallAreaDetection(); + + if (mAllowPkgMap.isEmpty()) return; + allowPkgMap.putAll(mAllowPkgMap); } + updateSmallAreaDetection(allowPkgMap); } @GuardedBy("mLock") @@ -105,43 +104,32 @@ final class SmallAreaDetectionController { } } - @GuardedBy("mLock") - private void updateUidListForAllUsers(SparseArray<Float> list, String pkg, float threshold) { - for (int i = 0; i < mUserIds.length; i++) { - final int userId = mUserIds[i]; - final int uid = mPackageManager.getPackageUid(pkg, 0, userId); - if (uid > 0) list.put(uid, threshold); - } - } - - @GuardedBy("mLock") - private void updateSmallAreaDetection() { - if (mAllowPkgMap.isEmpty()) return; - - mUserIds = mUserManager.getUserIds(); - - final SparseArray<Float> uidThresholdList = new SparseArray<>(); - for (String pkg : mAllowPkgMap.keySet()) { - final float threshold = mAllowPkgMap.get(pkg); - updateUidListForAllUsers(uidThresholdList, pkg, threshold); + private void updateSmallAreaDetection(Map<String, Float> allowPkgMap) { + final SparseArray<Float> appIdThresholdList = new SparseArray(allowPkgMap.size()); + for (String pkg : allowPkgMap.keySet()) { + final float threshold = allowPkgMap.get(pkg); + final PackageStateInternal stage = mPackageManager.getPackageStateInternal(pkg); + if (stage != null) { + appIdThresholdList.put(stage.getAppId(), threshold); + } } - final int[] uids = new int[uidThresholdList.size()]; - final float[] thresholds = new float[uidThresholdList.size()]; - for (int i = 0; i < uidThresholdList.size(); i++) { - uids[i] = uidThresholdList.keyAt(i); - thresholds[i] = uidThresholdList.valueAt(i); + final int[] appIds = new int[appIdThresholdList.size()]; + final float[] thresholds = new float[appIdThresholdList.size()]; + for (int i = 0; i < appIdThresholdList.size(); i++) { + appIds[i] = appIdThresholdList.keyAt(i); + thresholds[i] = appIdThresholdList.valueAt(i); } - updateSmallAreaDetection(uids, thresholds); + updateSmallAreaDetection(appIds, thresholds); } @VisibleForTesting - void updateSmallAreaDetection(int[] uids, float[] thresholds) { - nativeUpdateSmallAreaDetection(uids, thresholds); + void updateSmallAreaDetection(int[] appIds, float[] thresholds) { + nativeUpdateSmallAreaDetection(appIds, thresholds); } - void setSmallAreaDetectionThreshold(int uid, float threshold) { - nativeSetSmallAreaDetectionThreshold(uid, threshold); + void setSmallAreaDetectionThreshold(int appId, float threshold) { + nativeSetSmallAreaDetectionThreshold(appId, threshold); } void dump(PrintWriter pw) { @@ -151,7 +139,6 @@ final class SmallAreaDetectionController { for (String pkg : mAllowPkgMap.keySet()) { pw.println(" " + pkg + " threshold = " + mAllowPkgMap.get(pkg)); } - pw.println(" mUserIds=" + Arrays.toString(mUserIds)); } } @@ -167,11 +154,15 @@ final class SmallAreaDetectionController { private final class PackageReceiver implements PackageManagerInternal.PackageListObserver { @Override public void onPackageAdded(@NonNull String packageName, int uid) { + float threshold = 0.0f; synchronized (mLock) { if (mAllowPkgMap.containsKey(packageName)) { - setSmallAreaDetectionThreshold(uid, mAllowPkgMap.get(packageName)); + threshold = mAllowPkgMap.get(packageName); } } + if (threshold > 0.0f) { + setSmallAreaDetectionThreshold(UserHandle.getAppId(uid), threshold); + } } } } 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 fae8383bb62e..d953e8e52365 100644 --- a/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java +++ b/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java @@ -71,6 +71,10 @@ public class DisplayManagerFlags { Flags.FLAG_ENABLE_POWER_THROTTLING_CLAMPER, Flags::enablePowerThrottlingClamper); + private final FlagState mSmallAreaDetectionFlagState = new FlagState( + Flags.FLAG_ENABLE_SMALL_AREA_DETECTION, + Flags::enableSmallAreaDetection); + /** Returns whether connected display management is enabled or not. */ public boolean isConnectedDisplayManagementEnabled() { return mConnectedDisplayManagementFlagState.isEnabled(); @@ -147,6 +151,10 @@ public class DisplayManagerFlags { return mBackUpSmoothDisplayAndForcePeakRefreshRateFlagState.isEnabled(); } + public boolean isSmallAreaDetectionEnabled() { + return mSmallAreaDetectionFlagState.isEnabled(); + } + private static class FlagState { private final String mName; 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 9ab9c9def61b..9141814da6fc 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 @@ -104,3 +104,12 @@ flag { bug: "211737588" is_fixed_read_only: true } + +flag { + name: "enable_small_area_detection" + namespace: "display_manager" + description: "Feature flag for SmallAreaDetection" + bug: "298722189" + is_fixed_read_only: true +} + 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 ca23844044ca..d023913c9694 100644 --- a/services/core/java/com/android/server/display/mode/DisplayModeDirector.java +++ b/services/core/java/com/android/server/display/mode/DisplayModeDirector.java @@ -723,7 +723,9 @@ public class DisplayModeDirector { if (mode.getPhysicalWidth() > maxAllowedWidth || mode.getPhysicalHeight() > maxAllowedHeight || mode.getPhysicalWidth() < outSummary.minWidth - || mode.getPhysicalHeight() < outSummary.minHeight) { + || mode.getPhysicalHeight() < outSummary.minHeight + || mode.getRefreshRate() < outSummary.minPhysicalRefreshRate + || mode.getRefreshRate() > outSummary.maxPhysicalRefreshRate) { continue; } diff --git a/services/core/java/com/android/server/media/projection/MediaProjectionSessionIdGenerator.java b/services/core/java/com/android/server/media/projection/MediaProjectionSessionIdGenerator.java new file mode 100644 index 000000000000..ff70cb35f9dc --- /dev/null +++ b/services/core/java/com/android/server/media/projection/MediaProjectionSessionIdGenerator.java @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.media.projection; + +import android.content.Context; +import android.content.SharedPreferences; +import android.os.Environment; + +import com.android.internal.annotations.GuardedBy; +import com.android.internal.annotations.VisibleForTesting; + +import java.io.File; + +public class MediaProjectionSessionIdGenerator { + + private static final String PREFERENCES_FILE_NAME = "media_projection_session_id"; + private static final String SESSION_ID_PREF_KEY = "media_projection_session_id_key"; + private static final int SESSION_ID_DEFAULT_VALUE = 0; + + private static final Object sInstanceLock = new Object(); + + @GuardedBy("sInstanceLock") + private static MediaProjectionSessionIdGenerator sInstance; + + private final Object mSessionIdLock = new Object(); + + @GuardedBy("mSessionIdLock") + private final SharedPreferences mSharedPreferences; + + /** Creates or returns an existing instance of {@link MediaProjectionSessionIdGenerator}. */ + public static MediaProjectionSessionIdGenerator getInstance(Context context) { + synchronized (sInstanceLock) { + if (sInstance == null) { + File preferencesFile = + new File(Environment.getDataSystemDirectory(), PREFERENCES_FILE_NAME); + SharedPreferences preferences = + context.getSharedPreferences(preferencesFile, Context.MODE_PRIVATE); + sInstance = new MediaProjectionSessionIdGenerator(preferences); + } + return sInstance; + } + } + + @VisibleForTesting + public MediaProjectionSessionIdGenerator(SharedPreferences sharedPreferences) { + this.mSharedPreferences = sharedPreferences; + } + + /** Returns the current session ID. This value is persisted across reboots. */ + public int getCurrentSessionId() { + synchronized (mSessionIdLock) { + return getCurrentSessionIdInternal(); + } + } + + /** + * Creates and returns a new session ID. This value will be persisted as the new current session + * ID, and will be persisted across reboots. + */ + public int createAndGetNewSessionId() { + synchronized (mSessionIdLock) { + int newSessionId = getCurrentSessionId() + 1; + setSessionIdInternal(newSessionId); + return newSessionId; + } + } + + @GuardedBy("mSessionIdLock") + private void setSessionIdInternal(int value) { + mSharedPreferences.edit().putInt(SESSION_ID_PREF_KEY, value).apply(); + } + + @GuardedBy("mSessionIdLock") + private int getCurrentSessionIdInternal() { + return mSharedPreferences.getInt(SESSION_ID_PREF_KEY, SESSION_ID_DEFAULT_VALUE); + } +} diff --git a/services/core/java/com/android/server/pm/PackageArchiver.java b/services/core/java/com/android/server/pm/PackageArchiver.java index f59188e9fd93..0fb1f7a0780c 100644 --- a/services/core/java/com/android/server/pm/PackageArchiver.java +++ b/services/core/java/com/android/server/pm/PackageArchiver.java @@ -196,8 +196,12 @@ public class PackageArchiver { for (int i = 0, size = mainActivities.length; i < size; ++i) { var mainActivity = mainActivities[i]; Path iconPath = storeIconForParcel(packageName, mainActivity, userId, i); - ArchiveActivityInfo activityInfo = new ArchiveActivityInfo( - mainActivity.title, iconPath, null); + ArchiveActivityInfo activityInfo = + new ArchiveActivityInfo( + mainActivity.title, + mainActivity.originalComponentName, + iconPath, + null); archiveActivityInfos.add(activityInfo); } @@ -215,8 +219,12 @@ public class PackageArchiver { for (int i = 0, size = mainActivities.size(); i < size; i++) { LauncherActivityInfo mainActivity = mainActivities.get(i); Path iconPath = storeIcon(packageName, mainActivity, userId, i); - ArchiveActivityInfo activityInfo = new ArchiveActivityInfo( - mainActivity.getLabel().toString(), iconPath, null); + ArchiveActivityInfo activityInfo = + new ArchiveActivityInfo( + mainActivity.getLabel().toString(), + mainActivity.getComponentName(), + iconPath, + null); archiveActivityInfos.add(activityInfo); } @@ -593,6 +601,7 @@ public class PackageArchiver { } var archivedActivity = new ArchivedActivityParcel(); archivedActivity.title = info.getTitle(); + archivedActivity.originalComponentName = info.getOriginalComponentName(); archivedActivity.iconBitmap = bytesFromBitmapFile(info.getIconBitmap()); archivedActivity.monochromeIconBitmap = bytesFromBitmapFile( info.getMonochromeIconBitmap()); @@ -624,6 +633,7 @@ public class PackageArchiver { } var archivedActivity = new ArchivedActivityParcel(); archivedActivity.title = info.getLabel().toString(); + archivedActivity.originalComponentName = info.getComponentName(); archivedActivity.iconBitmap = info.getActivityInfo().getIconResource() == 0 ? null : bytesFromBitmap( drawableToBitmap(info.getIcon(/* density= */ 0))); diff --git a/services/core/java/com/android/server/pm/PackageManagerInternalBase.java b/services/core/java/com/android/server/pm/PackageManagerInternalBase.java index 651845e71924..e7499680b9a2 100644 --- a/services/core/java/com/android/server/pm/PackageManagerInternalBase.java +++ b/services/core/java/com/android/server/pm/PackageManagerInternalBase.java @@ -747,7 +747,7 @@ abstract class PackageManagerInternalBase extends PackageManagerInternal { @Override public void notifyComponentUsed(@NonNull String packageName, @UserIdInt int userId, - @NonNull String recentCallingPackage, @NonNull String debugInfo) { + @Nullable String recentCallingPackage, @NonNull String debugInfo) { mService.notifyComponentUsed(snapshot(), packageName, userId, recentCallingPackage, debugInfo); } diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java index abeabc96e969..839b6998bd8b 100644 --- a/services/core/java/com/android/server/pm/PackageManagerService.java +++ b/services/core/java/com/android/server/pm/PackageManagerService.java @@ -36,6 +36,7 @@ import static android.os.storage.StorageManager.FLAG_STORAGE_CE; import static android.os.storage.StorageManager.FLAG_STORAGE_DE; import static android.os.storage.StorageManager.FLAG_STORAGE_EXTERNAL; import static android.provider.DeviceConfig.NAMESPACE_PACKAGE_MANAGER_SERVICE; +import static android.util.FeatureFlagUtils.SETTINGS_TREAT_PAUSE_AS_QUARANTINE; import static com.android.internal.annotations.VisibleForTesting.Visibility; import static com.android.internal.util.FrameworkStatsLog.BOOT_TIME_EVENT_DURATION__EVENT__OTA_PACKAGE_MANAGER_INIT_TIME; @@ -164,6 +165,7 @@ import android.util.ArraySet; import android.util.DisplayMetrics; import android.util.EventLog; import android.util.ExceptionUtils; +import android.util.FeatureFlagUtils; import android.util.Log; import android.util.Pair; import android.util.Slog; @@ -4576,7 +4578,7 @@ public class PackageManagerService implements PackageSender, TestUtilityService } void notifyComponentUsed(@NonNull Computer snapshot, @NonNull String packageName, - @UserIdInt int userId, @NonNull String recentCallingPackage, + @UserIdInt int userId, @Nullable String recentCallingPackage, @NonNull String debugInfo) { synchronized (mLock) { final PackageSetting pkgSetting = mSettings.getPackageLPr(packageName); @@ -6133,8 +6135,16 @@ public class PackageManagerService implements PackageSender, TestUtilityService final Computer snapshot = snapshotComputer(); enforceCanSetPackagesSuspendedAsUser(snapshot, callingPackage, callingUid, userId, "setPackagesSuspendedAsUser"); - boolean quarantined = ((flags & PackageManager.FLAG_SUSPEND_QUARANTINED) != 0) - && Flags.quarantinedEnabled(); + boolean quarantined = false; + if (Flags.quarantinedEnabled()) { + if ((flags & PackageManager.FLAG_SUSPEND_QUARANTINED) != 0) { + quarantined = true; + } else if (FeatureFlagUtils.isEnabled(mContext, + SETTINGS_TREAT_PAUSE_AS_QUARANTINE)) { + final String wellbeingPkg = mContext.getString(R.string.config_systemWellbeing); + quarantined = callingPackage.equals(wellbeingPkg); + } + } return mSuspendPackageHelper.setPackagesSuspended(snapshot, packageNames, suspended, appExtras, launcherExtras, dialogInfo, callingPackage, userId, callingUid, false /* forQuietMode */, quarantined); diff --git a/services/core/java/com/android/server/pm/PackageSetting.java b/services/core/java/com/android/server/pm/PackageSetting.java index 9f4e86d527ce..3ca933a66656 100644 --- a/services/core/java/com/android/server/pm/PackageSetting.java +++ b/services/core/java/com/android/server/pm/PackageSetting.java @@ -1228,6 +1228,9 @@ public class PackageSetting extends SettingBase implements PackageStateInternal long activityInfoToken = proto.start( PackageProto.UserInfoProto.ArchiveState.ACTIVITY_INFOS); proto.write(ArchiveActivityInfo.TITLE, activityInfo.getTitle()); + proto.write( + ArchiveActivityInfo.ORIGINAL_COMPONENT_NAME, + activityInfo.getOriginalComponentName().flattenToString()); if (activityInfo.getIconBitmap() != null) { proto.write(ArchiveActivityInfo.ICON_BITMAP_PATH, activityInfo.getIconBitmap().toAbsolutePath().toString()); diff --git a/services/core/java/com/android/server/pm/Settings.java b/services/core/java/com/android/server/pm/Settings.java index 397a841db604..e726d91c30a0 100644 --- a/services/core/java/com/android/server/pm/Settings.java +++ b/services/core/java/com/android/server/pm/Settings.java @@ -368,6 +368,7 @@ public final class Settings implements Watchable, Snappable, ResilientAtomicFile private static final String ATTR_VALUE = "value"; private static final String ATTR_FIRST_INSTALL_TIME = "first-install-time"; private static final String ATTR_ARCHIVE_ACTIVITY_TITLE = "activity-title"; + private static final String ATTR_ARCHIVE_ORIGINAL_COMPONENT_NAME = "original-component-name"; private static final String ATTR_ARCHIVE_INSTALLER_TITLE = "installer-title"; private static final String ATTR_ARCHIVE_ICON_PATH = "icon-path"; private static final String ATTR_ARCHIVE_MONOCHROME_ICON_PATH = "monochrome-icon-path"; @@ -2079,6 +2080,8 @@ public final class Settings implements Watchable, Snappable, ResilientAtomicFile if (tagName.equals(TAG_ARCHIVE_ACTIVITY_INFO)) { String title = parser.getAttributeValue(null, ATTR_ARCHIVE_ACTIVITY_TITLE); + String originalComponentName = + parser.getAttributeValue(null, ATTR_ARCHIVE_ORIGINAL_COMPONENT_NAME); String iconAttribute = parser.getAttributeValue(null, ATTR_ARCHIVE_ICON_PATH); Path iconPath = iconAttribute == null ? null : Path.of(iconAttribute); @@ -2087,17 +2090,27 @@ public final class Settings implements Watchable, Snappable, ResilientAtomicFile Path monochromeIconPath = monochromeAttribute == null ? null : Path.of( monochromeAttribute); - if (title == null || iconPath == null) { - Slog.wtf(TAG, - TextUtils.formatSimple("Missing attributes in tag %s. %s: %s, %s: %s", - TAG_ARCHIVE_ACTIVITY_INFO, ATTR_ARCHIVE_ACTIVITY_TITLE, title, + if (title == null || originalComponentName == null || iconPath == null) { + Slog.wtf( + TAG, + TextUtils.formatSimple( + "Missing attributes in tag %s. %s: %s, %s: %s, %s: %s", + TAG_ARCHIVE_ACTIVITY_INFO, + ATTR_ARCHIVE_ACTIVITY_TITLE, + title, + ATTR_ARCHIVE_ORIGINAL_COMPONENT_NAME, + originalComponentName, ATTR_ARCHIVE_ICON_PATH, iconPath)); continue; } activityInfos.add( - new ArchiveState.ArchiveActivityInfo(title, iconPath, monochromeIconPath)); + new ArchiveState.ArchiveActivityInfo( + title, + ComponentName.unflattenFromString(originalComponentName), + iconPath, + monochromeIconPath)); } } return activityInfos; @@ -2469,6 +2482,10 @@ public final class Settings implements Watchable, Snappable, ResilientAtomicFile for (ArchiveState.ArchiveActivityInfo activityInfo : archiveState.getActivityInfos()) { serializer.startTag(null, TAG_ARCHIVE_ACTIVITY_INFO); serializer.attribute(null, ATTR_ARCHIVE_ACTIVITY_TITLE, activityInfo.getTitle()); + serializer.attribute( + null, + ATTR_ARCHIVE_ORIGINAL_COMPONENT_NAME, + activityInfo.getOriginalComponentName().flattenToString()); if (activityInfo.getIconBitmap() != null) { serializer.attribute(null, ATTR_ARCHIVE_ICON_PATH, activityInfo.getIconBitmap().toAbsolutePath().toString()); diff --git a/services/core/java/com/android/server/pm/pkg/ArchiveState.java b/services/core/java/com/android/server/pm/pkg/ArchiveState.java index 4916a4a6d72a..1e40d44bd4ca 100644 --- a/services/core/java/com/android/server/pm/pkg/ArchiveState.java +++ b/services/core/java/com/android/server/pm/pkg/ArchiveState.java @@ -16,9 +16,11 @@ package com.android.server.pm.pkg; +import android.content.ComponentName; import android.annotation.NonNull; import android.annotation.Nullable; +import com.android.internal.util.AnnotationValidations; import com.android.internal.util.DataClass; import java.nio.file.Path; @@ -56,6 +58,10 @@ public class ArchiveState { @NonNull private final String mTitle; + /** The component name of the original activity (pre-archival). */ + @NonNull + private final ComponentName mOriginalComponentName; + /** * The path to the stored icon of the activity in the app's locale. Null if the app does * not define any icon (default icon would be shown on the launcher). @@ -96,11 +102,13 @@ public class ArchiveState { @DataClass.Generated.Member public ArchiveActivityInfo( @NonNull String title, + @NonNull ComponentName originalComponentName, @Nullable Path iconBitmap, @Nullable Path monochromeIconBitmap) { this.mTitle = title; - com.android.internal.util.AnnotationValidations.validate( - NonNull.class, null, mTitle); + this.mOriginalComponentName = originalComponentName; + AnnotationValidations.validate(NonNull.class, null, mTitle); + AnnotationValidations.validate(NonNull.class, null, mOriginalComponentName); this.mIconBitmap = iconBitmap; this.mMonochromeIconBitmap = monochromeIconBitmap; @@ -116,6 +124,14 @@ public class ArchiveState { } /** + * The component name of the original activity (pre-archival). + */ + @DataClass.Generated.Member + public @NonNull ComponentName getOriginalComponentName() { + return mOriginalComponentName; + } + + /** * The path to the stored icon of the activity in the app's locale. Null if the app does * not define any icon (default icon would be shown on the launcher). */ @@ -140,6 +156,7 @@ public class ArchiveState { return "ArchiveActivityInfo { " + "title = " + mTitle + ", " + + "originalComponentName = " + mOriginalComponentName + ", " + "iconBitmap = " + mIconBitmap + ", " + "monochromeIconBitmap = " + mMonochromeIconBitmap + " }"; @@ -159,6 +176,7 @@ public class ArchiveState { //noinspection PointlessBooleanExpression return true && java.util.Objects.equals(mTitle, that.mTitle) + && java.util.Objects.equals(mOriginalComponentName, that.mOriginalComponentName) && java.util.Objects.equals(mIconBitmap, that.mIconBitmap) && java.util.Objects.equals(mMonochromeIconBitmap, that.mMonochromeIconBitmap); } @@ -171,6 +189,7 @@ public class ArchiveState { int _hash = 1; _hash = 31 * _hash + java.util.Objects.hashCode(mTitle); + _hash = 31* _hash + java.util.Objects.hashCode(mOriginalComponentName); _hash = 31 * _hash + java.util.Objects.hashCode(mIconBitmap); _hash = 31 * _hash + java.util.Objects.hashCode(mMonochromeIconBitmap); return _hash; @@ -180,7 +199,8 @@ public class ArchiveState { time = 1693590309015L, codegenVersion = "1.0.23", sourceFile = "frameworks/base/services/core/java/com/android/server/pm/pkg/ArchiveState.java", - inputSignatures = "private final @android.annotation.NonNull java.lang.String mTitle\nprivate final @android.annotation.Nullable java.nio.file.Path mIconBitmap\nprivate final @android.annotation.Nullable java.nio.file.Path mMonochromeIconBitmap\nclass ArchiveActivityInfo extends java.lang.Object implements []\n@com.android.internal.util.DataClass(genEqualsHashCode=true, genToString=true)") + inputSignatures = + "private final @android.annotation.NonNull java.lang.String mTitle\nprivate final @android.annotation.NonNull android.content.ComponentName mOriginalComponentName\nprivate final @android.annotation.Nullable java.nio.file.Path mIconBitmap\nprivate final @android.annotation.Nullable java.nio.file.Path mMonochromeIconBitmap\nclass ArchiveActivityInfo extends java.lang.Object implements []\n@com.android.internal.util.DataClass(genEqualsHashCode=true, genToString=true)") @Deprecated private void __metadata() {} @@ -224,11 +244,9 @@ public class ArchiveState { @NonNull List<ArchiveActivityInfo> activityInfos, @NonNull String installerTitle) { this.mActivityInfos = activityInfos; - com.android.internal.util.AnnotationValidations.validate( - NonNull.class, null, mActivityInfos); + AnnotationValidations.validate(NonNull.class, null, mActivityInfos); this.mInstallerTitle = installerTitle; - com.android.internal.util.AnnotationValidations.validate( - NonNull.class, null, mInstallerTitle); + AnnotationValidations.validate(NonNull.class, null, mInstallerTitle); // onConstructed(); // You can define this method to get a callback } diff --git a/services/core/java/com/android/server/power/Android.bp b/services/core/java/com/android/server/power/Android.bp index 1da9dd770ba6..607d435b5410 100644 --- a/services/core/java/com/android/server/power/Android.bp +++ b/services/core/java/com/android/server/power/Android.bp @@ -1,5 +1,5 @@ aconfig_declarations { - name: "power_optimization_flags", + name: "backstage_power_flags", package: "com.android.server.power.optimization", srcs: [ "stats/*.aconfig", @@ -7,6 +7,6 @@ aconfig_declarations { } java_aconfig_library { - name: "power_optimization_flags_lib", - aconfig_declarations: "power_optimization_flags", + name: "backstage_power_flags_lib", + aconfig_declarations: "backstage_power_flags", } diff --git a/services/core/java/com/android/server/power/stats/flags.aconfig b/services/core/java/com/android/server/power/stats/flags.aconfig index add806febe67..0f135715ebc3 100644 --- a/services/core/java/com/android/server/power/stats/flags.aconfig +++ b/services/core/java/com/android/server/power/stats/flags.aconfig @@ -2,14 +2,14 @@ package: "com.android.server.power.optimization" flag { name: "power_monitor_api" - namespace: "power_optimization" + namespace: "backstage_power" description: "Feature flag for ODPM API" bug: "295027807" } flag { name: "streamlined_battery_stats" - namespace: "power_optimization" + namespace: "backstage_power" description: "Feature flag for streamlined battery stats" bug: "285646152" } diff --git a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java index 0718f2f284a5..4200fbf39ade 100644 --- a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java +++ b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java @@ -3982,7 +3982,9 @@ public class WallpaperManagerService extends IWallpaperManager.Stub if (wallpaper == null) { // common case, this is the first lookup post-boot of the system or // unified lock, so we bring up the saved state lazily now and recheck. - int whichLoad = (which == FLAG_LOCK) ? FLAG_LOCK : FLAG_SYSTEM; + // if we're loading the system wallpaper for the first time, also load the lock + // wallpaper to determine if the system wallpaper is system+lock or system only. + int whichLoad = (which == FLAG_LOCK) ? FLAG_LOCK : FLAG_SYSTEM | FLAG_LOCK; loadSettingsLocked(userId, false, whichLoad); wallpaper = whichSet.get(userId); if (wallpaper == null) { diff --git a/services/core/java/com/android/server/wm/BackNavigationController.java b/services/core/java/com/android/server/wm/BackNavigationController.java index 42376685498a..6d59b297de48 100644 --- a/services/core/java/com/android/server/wm/BackNavigationController.java +++ b/services/core/java/com/android/server/wm/BackNavigationController.java @@ -276,6 +276,10 @@ class BackNavigationController { // activity, we won't close the activity. backType = BackNavigationInfo.TYPE_DIALOG_CLOSE; removedWindowContainer = window; + } else if (!currentActivity.occludesParent() || currentActivity.showWallpaper()) { + // skip if current activity is translucent + backType = BackNavigationInfo.TYPE_CALLBACK; + removedWindowContainer = window; } else if (prevActivity != null) { if (!isOccluded || prevActivity.canShowWhenLocked()) { // We have another Activity in the same currentTask to go to diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java index 016b0ff55826..4922e9028ed9 100644 --- a/services/core/java/com/android/server/wm/Task.java +++ b/services/core/java/com/android/server/wm/Task.java @@ -489,10 +489,6 @@ class Task extends TaskFragment { private boolean mForceShowForAllUsers; - /** 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; - private int mForceHiddenFlags = 0; private boolean mForceTranslucent = false; // The display category name for this task. @@ -4495,20 +4491,13 @@ class Task extends TaskFragment { * Sets/unsets the forced-hidden state flag for this task depending on {@param set}. * @return Whether the force hidden state changed */ - boolean setForceHidden(int flags, boolean set) { - int newFlags = mForceHiddenFlags; - if (set) { - newFlags |= flags; - } else { - newFlags &= ~flags; - } - if (mForceHiddenFlags == newFlags) { - return false; - } - + @Override + boolean setForceHidden(@FlagForceHidden int flags, boolean set) { final boolean wasHidden = isForceHidden(); final boolean wasVisible = isVisible(); - mForceHiddenFlags = newFlags; + if (!super.setForceHidden(flags, set)) { + return false; + } final boolean nowHidden = isForceHidden(); if (wasHidden != nowHidden) { final String reason = "setForceHidden"; @@ -4539,11 +4528,6 @@ class Task extends TaskFragment { return super.isAlwaysOnTop(); } - @Override - protected boolean isForceHidden() { - return mForceHiddenFlags != 0; - } - boolean isForceHiddenForPinnedTask() { return (mForceHiddenFlags & FLAG_FORCE_HIDDEN_FOR_PINNED_TASK) != 0; } @@ -5651,6 +5635,8 @@ class Task extends TaskFragment { if (noAnimation) { mDisplayContent.prepareAppTransition(TRANSIT_NONE); mTaskSupervisor.mNoAnimActivities.add(top); + mTransitionController.collect(top); + mTransitionController.setNoAnimation(top); ActivityOptions.abort(options); } else { updateTransitLocked(TRANSIT_TO_FRONT, options); diff --git a/services/core/java/com/android/server/wm/TaskFragment.java b/services/core/java/com/android/server/wm/TaskFragment.java index 7c5bc6e3bb28..906b3b55e015 100644 --- a/services/core/java/com/android/server/wm/TaskFragment.java +++ b/services/core/java/com/android/server/wm/TaskFragment.java @@ -362,6 +362,19 @@ class TaskFragment extends WindowContainer<WindowContainer> { */ private boolean mIsolatedNav; + /** 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; + static final int FLAG_FORCE_HIDDEN_FOR_TASK_FRAGMENT_ORG = 1 << 2; + + @IntDef(prefix = {"FLAG_FORCE_HIDDEN_"}, value = { + FLAG_FORCE_HIDDEN_FOR_PINNED_TASK, + FLAG_FORCE_HIDDEN_FOR_TASK_ORG, + FLAG_FORCE_HIDDEN_FOR_TASK_FRAGMENT_ORG, + }, flag = true) + @interface FlagForceHidden {} + protected int mForceHiddenFlags = 0; + final Point mLastSurfaceSize = new Point(); private final Rect mTmpBounds = new Rect(); @@ -845,7 +858,25 @@ class TaskFragment extends WindowContainer<WindowContainer> { * Returns whether this TaskFragment is currently forced to be hidden for any reason. */ protected boolean isForceHidden() { - return false; + return mForceHiddenFlags != 0; + } + + /** + * Sets/unsets the forced-hidden state flag for this task depending on {@param set}. + * @return Whether the force hidden state changed + */ + boolean setForceHidden(@FlagForceHidden int flags, boolean set) { + int newFlags = mForceHiddenFlags; + if (set) { + newFlags |= flags; + } else { + newFlags &= ~flags; + } + if (mForceHiddenFlags == newFlags) { + return false; + } + mForceHiddenFlags = newFlags; + return true; } protected boolean isForceTranslucent() { diff --git a/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java b/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java index 04164c20a372..ff766beee337 100644 --- a/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java +++ b/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java @@ -51,7 +51,6 @@ import android.window.ITaskFragmentOrganizer; import android.window.ITaskFragmentOrganizerController; import android.window.TaskFragmentInfo; import android.window.TaskFragmentOperation; -import android.window.TaskFragmentOrganizerToken; import android.window.TaskFragmentParentInfo; import android.window.TaskFragmentTransaction; import android.window.WindowContainerTransaction; @@ -745,9 +744,9 @@ public class TaskFragmentOrganizerController extends ITaskFragmentOrganizerContr } } - boolean isSystemOrganizer(@NonNull TaskFragmentOrganizerToken token) { + boolean isSystemOrganizer(@NonNull IBinder organizerToken) { final TaskFragmentOrganizerState state = - mTaskFragmentOrganizerState.get(token.asBinder()); + mTaskFragmentOrganizerState.get(organizerToken); return state != null && state.mIsSystemOrganizer; } diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java index 93db1caf5fdf..7d65c61193b5 100644 --- a/services/core/java/com/android/server/wm/Transition.java +++ b/services/core/java/com/android/server/wm/Transition.java @@ -3321,8 +3321,8 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { mFrozen.add(wc); final ChangeInfo changeInfo = Objects.requireNonNull(mChanges.get(wc)); changeInfo.mSnapshot = snapshotSurface; - if (isDisplayRotation) { - // This isn't cheap, so only do it for display rotations. + if (changeInfo.mRotation != wc.mDisplayContent.getRotation()) { + // This isn't cheap, so only do it for rotation change. changeInfo.mSnapshotLuma = TransitionAnimation.getBorderLuma( buffer, screenshotBuffer.getColorSpace()); } diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java index 9663f3aba7f1..88f72f9dbc90 100644 --- a/services/core/java/com/android/server/wm/WindowManagerService.java +++ b/services/core/java/com/android/server/wm/WindowManagerService.java @@ -8461,16 +8461,18 @@ public class WindowManagerService extends IWindowManager.Stub return true; } // For a task session, find the activity identified by the launch cookie. - final WindowContainerToken wct = getTaskWindowContainerTokenForLaunchCookie( + final WindowContainerInfo wci = getTaskWindowContainerInfoForLaunchCookie( incomingSession.getTokenToRecord()); - if (wct == null) { + if (wci == null) { Slog.w(TAG, "Handling a new recording session; unable to find the " + "WindowContainerToken"); return false; } // Replace the launch cookie in the session details with the task's // WindowContainerToken. - incomingSession.setTokenToRecord(wct.asBinder()); + incomingSession.setTokenToRecord(wci.getToken().asBinder()); + // Also replace the UNKNOWN target UID with the actual UID. + incomingSession.setTargetUid(wci.getUid()); mContentRecordingController.setContentRecordingSessionLocked(incomingSession, WindowManagerService.this); return true; @@ -8798,21 +8800,41 @@ public class WindowManagerService extends IWindowManager.Stub mAtmService.setFocusedTask(task.mTaskId, touchedActivity); } + @VisibleForTesting + static class WindowContainerInfo { + private final int mUid; + @NonNull private final WindowContainerToken mToken; + + private WindowContainerInfo(int uid, @NonNull WindowContainerToken token) { + this.mUid = uid; + this.mToken = token; + } + + public int getUid() { + return mUid; + } + + @NonNull + public WindowContainerToken getToken() { + return mToken; + } + } + /** - * Retrieve the {@link WindowContainerToken} of the task that contains the activity started - * with the given launch cookie. + * Retrieve the {@link WindowContainerInfo} of the task that contains the activity started with + * the given launch cookie. * * @param launchCookie the launch cookie set on the {@link ActivityOptions} when starting an - * activity + * activity * @return a token representing the task containing the activity started with the given launch - * cookie, or {@code null} if the token couldn't be found. + * cookie, or {@code null} if the token couldn't be found. */ @VisibleForTesting @Nullable - WindowContainerToken getTaskWindowContainerTokenForLaunchCookie(@NonNull IBinder launchCookie) { + WindowContainerInfo getTaskWindowContainerInfoForLaunchCookie(@NonNull IBinder launchCookie) { // Find the activity identified by the launch cookie. - final ActivityRecord targetActivity = mRoot.getActivity( - activity -> activity.mLaunchCookie == launchCookie); + final ActivityRecord targetActivity = + mRoot.getActivity(activity -> activity.mLaunchCookie == launchCookie); if (targetActivity == null) { Slog.w(TAG, "Unable to find the activity for this launch cookie"); return null; @@ -8827,7 +8849,7 @@ public class WindowManagerService extends IWindowManager.Stub Slog.w(TAG, "Unable to find the WindowContainerToken for " + targetActivity.getName()); return null; } - return taskWindowContainerToken; + return new WindowContainerInfo(targetActivity.getUid(), taskWindowContainerToken); } /** diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java index dd9a88f72bde..5ed6caffe1fb 100644 --- a/services/core/java/com/android/server/wm/WindowOrganizerController.java +++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java @@ -24,7 +24,9 @@ import static android.view.Display.DEFAULT_DISPLAY; import static android.window.TaskFragmentOperation.OP_TYPE_CLEAR_ADJACENT_TASK_FRAGMENTS; import static android.window.TaskFragmentOperation.OP_TYPE_CREATE_TASK_FRAGMENT; import static android.window.TaskFragmentOperation.OP_TYPE_DELETE_TASK_FRAGMENT; +import static android.window.TaskFragmentOperation.OP_TYPE_REORDER_TO_BOTTOM_OF_TASK; import static android.window.TaskFragmentOperation.OP_TYPE_REORDER_TO_FRONT; +import static android.window.TaskFragmentOperation.OP_TYPE_REORDER_TO_TOP_OF_TASK; import static android.window.TaskFragmentOperation.OP_TYPE_REPARENT_ACTIVITY_TO_TASK_FRAGMENT; import static android.window.TaskFragmentOperation.OP_TYPE_REQUEST_FOCUS_ON_TASK_FRAGMENT; import static android.window.TaskFragmentOperation.OP_TYPE_SET_ADJACENT_TASK_FRAGMENTS; @@ -34,6 +36,8 @@ import static android.window.TaskFragmentOperation.OP_TYPE_SET_ISOLATED_NAVIGATI 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; +import static android.window.WindowContainerTransaction.Change.CHANGE_FOCUSABLE; +import static android.window.WindowContainerTransaction.Change.CHANGE_HIDDEN; import static android.window.WindowContainerTransaction.Change.CHANGE_RELATIVE_BOUNDS; import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_ADD_INSETS_FRAME_PROVIDER; import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_ADD_TASK_FRAGMENT_OPERATION; @@ -61,6 +65,7 @@ import static com.android.server.wm.ActivityTaskSupervisor.PRESERVE_WINDOWS; import static com.android.server.wm.Task.FLAG_FORCE_HIDDEN_FOR_PINNED_TASK; import static com.android.server.wm.Task.FLAG_FORCE_HIDDEN_FOR_TASK_ORG; import static com.android.server.wm.TaskFragment.EMBEDDING_ALLOWED; +import static com.android.server.wm.TaskFragment.FLAG_FORCE_HIDDEN_FOR_TASK_FRAGMENT_ORG; import static com.android.server.wm.WindowContainer.POSITION_BOTTOM; import static com.android.server.wm.WindowContainer.POSITION_TOP; @@ -821,6 +826,7 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub return TRANSACT_EFFECTS_NONE; } + int effects = TRANSACT_EFFECTS_NONE; // When the TaskFragment is resized, we may want to create a change transition for it, for // which we want to defer the surface update until we determine whether or not to start // change transition. @@ -843,7 +849,14 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub c.getConfiguration().windowConfiguration.setBounds(absBounds); taskFragment.setRelativeEmbeddedBounds(relBounds); } - final int effects = applyChanges(taskFragment, c); + if ((c.getChangeMask() & WindowContainerTransaction.Change.CHANGE_HIDDEN) != 0) { + if (taskFragment.setForceHidden( + FLAG_FORCE_HIDDEN_FOR_TASK_FRAGMENT_ORG, c.getHidden())) { + effects |= TRANSACT_EFFECTS_LIFECYCLE; + } + } + effects |= applyChanges(taskFragment, c); + if (taskFragment.shouldStartChangeTransition(mTmpBounds0, mTmpBounds1)) { taskFragment.initializeChangeTransition(mTmpBounds0); } @@ -1393,6 +1406,24 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub taskFragment.setIsolatedNav(isolatedNav); break; } + case OP_TYPE_REORDER_TO_BOTTOM_OF_TASK: { + final Task task = taskFragment.getTask(); + if (task != null) { + task.mChildren.remove(taskFragment); + task.mChildren.add(0, taskFragment); + effects |= TRANSACT_EFFECTS_LIFECYCLE; + } + break; + } + case OP_TYPE_REORDER_TO_TOP_OF_TASK: { + final Task task = taskFragment.getTask(); + if (task != null) { + task.mChildren.remove(taskFragment); + task.mChildren.add(taskFragment); + effects |= TRANSACT_EFFECTS_LIFECYCLE; + } + break; + } } return effects; } @@ -1420,6 +1451,18 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub return false; } + if ((opType == OP_TYPE_REORDER_TO_BOTTOM_OF_TASK + || opType == OP_TYPE_REORDER_TO_TOP_OF_TASK) + && !mTaskFragmentOrganizerController.isSystemOrganizer(organizer.asBinder())) { + final Throwable exception = new SecurityException( + "Only a system organizer can perform OP_TYPE_REORDER_TO_BOTTOM_OF_TASK or " + + "OP_TYPE_REORDER_TO_TOP_OF_TASK." + ); + sendTaskFragmentOperationFailure(organizer, errorCallbackToken, taskFragment, + opType, exception); + return false; + } + final IBinder secondaryFragmentToken = operation.getSecondaryFragmentToken(); return secondaryFragmentToken == null || validateTaskFragment(mLaunchTaskFragments.get(secondaryFragmentToken), opType, @@ -1920,6 +1963,11 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub * For config change on {@link TaskFragment}, we only support the following operations: * {@link WindowContainerTransaction#setRelativeBounds(WindowContainerToken, Rect)}, * {@link WindowContainerTransaction#setWindowingMode(WindowContainerToken, int)}. + * + * For a system organizer, we additionally support + * {@link WindowContainerTransaction#setHidden(WindowContainerToken, boolean)}, and + * {@link WindowContainerTransaction#setFocusable(WindowContainerToken, boolean)}. See + * {@link TaskFragmentOrganizerController#registerOrganizer(ITaskFragmentOrganizer, boolean)} */ private void enforceTaskFragmentConfigChangeAllowed(@NonNull String func, @Nullable WindowContainer wc, @NonNull WindowContainerTransaction.Change change, @@ -1938,31 +1986,49 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub throw new SecurityException(msg); } - final int changeMask = change.getChangeMask(); - final int configSetMask = change.getConfigSetMask(); - final int windowSetMask = change.getWindowSetMask(); - if (changeMask == 0 && configSetMask == 0 && windowSetMask == 0 - && change.getWindowingMode() >= 0) { - // The change contains only setWindowingMode, which is allowed. - return; + final int originalChangeMask = change.getChangeMask(); + final int originalConfigSetMask = change.getConfigSetMask(); + final int originalWindowSetMask = change.getWindowSetMask(); + + int changeMaskToBeChecked = originalChangeMask; + int configSetMaskToBeChecked = originalConfigSetMask; + int windowSetMaskToBeChecked = originalWindowSetMask; + + if (mTaskFragmentOrganizerController.isSystemOrganizer(organizer.asBinder())) { + // System organizer is allowed to update the hidden and focusable state. + // We unset the CHANGE_HIDDEN and CHANGE_FOCUSABLE bits because they are checked here. + changeMaskToBeChecked &= ~CHANGE_HIDDEN; + changeMaskToBeChecked &= ~CHANGE_FOCUSABLE; } - if (changeMask != CHANGE_RELATIVE_BOUNDS - || configSetMask != ActivityInfo.CONFIG_WINDOW_CONFIGURATION - || windowSetMask != WindowConfiguration.WINDOW_CONFIG_BOUNDS) { - // None of the change should be requested from a TaskFragment organizer except - // setRelativeBounds and setWindowingMode. - // For setRelativeBounds, we don't need to check whether it is outside of the Task + + // setRelativeBounds is allowed. + if ((changeMaskToBeChecked & CHANGE_RELATIVE_BOUNDS) != 0 + && (configSetMaskToBeChecked & ActivityInfo.CONFIG_WINDOW_CONFIGURATION) != 0 + && (windowSetMaskToBeChecked & WindowConfiguration.WINDOW_CONFIG_BOUNDS) != 0) { + // For setRelativeBounds, we don't need to check whether it is outside the Task // bounds, because it is possible that the Task is also resizing, for which we don't // want to throw an exception. The bounds will be adjusted in // TaskFragment#translateRelativeBoundsToAbsoluteBounds. - String msg = "Permission Denial: " + func + " from pid=" - + Binder.getCallingPid() + ", uid=" + Binder.getCallingUid() - + " trying to apply changes of changeMask=" + changeMask - + " configSetMask=" + configSetMask + " windowSetMask=" + windowSetMask - + " to TaskFragment=" + tf + " TaskFragmentOrganizer=" + organizer; - Slog.w(TAG, msg); - throw new SecurityException(msg); + changeMaskToBeChecked &= ~CHANGE_RELATIVE_BOUNDS; + configSetMaskToBeChecked &= ~ActivityInfo.CONFIG_WINDOW_CONFIGURATION; + windowSetMaskToBeChecked &= ~WindowConfiguration.WINDOW_CONFIG_BOUNDS; } + + if (changeMaskToBeChecked == 0 && configSetMaskToBeChecked == 0 + && windowSetMaskToBeChecked == 0) { + // All the changes have been checked. + // Note that setWindowingMode is always allowed, so we don't need to check the mask. + return; + } + + final String msg = "Permission Denial: " + func + " from pid=" + + Binder.getCallingPid() + ", uid=" + Binder.getCallingUid() + + " trying to apply changes of changeMask=" + originalChangeMask + + " configSetMask=" + originalConfigSetMask + + " windowSetMask=" + originalWindowSetMask + + " to TaskFragment=" + tf + " TaskFragmentOrganizer=" + organizer; + Slog.w(TAG, msg); + throw new SecurityException(msg); } private void createTaskFragment(@NonNull TaskFragmentCreationParams creationParams, @@ -2019,7 +2085,7 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub TaskFragmentOrganizerToken organizerToken = creationParams.getOrganizer(); taskFragment.setTaskFragmentOrganizer(organizerToken, ownerActivity.getUid(), ownerActivity.info.processName, - mTaskFragmentOrganizerController.isSystemOrganizer(organizerToken)); + mTaskFragmentOrganizerController.isSystemOrganizer(organizerToken.asBinder())); final int position; if (creationParams.getPairedPrimaryFragmentToken() != null) { // When there is a paired primary TaskFragment, we want to place the new TaskFragment diff --git a/services/core/java/com/android/server/wm/WindowToken.java b/services/core/java/com/android/server/wm/WindowToken.java index e434f296bbb5..7d21dbf85a66 100644 --- a/services/core/java/com/android/server/wm/WindowToken.java +++ b/services/core/java/com/android/server/wm/WindowToken.java @@ -569,6 +569,7 @@ class WindowToken extends WindowContainer<WindowState> { && asActivityRecord() != null && isVisible()) { // Trigger an activity level rotation transition. mTransitionController.requestTransitionIfNeeded(WindowManager.TRANSIT_CHANGE, this); + mTransitionController.collectVisibleChange(this); mTransitionController.setReady(this); } final int originalRotation = getWindowConfiguration().getRotation(); diff --git a/services/core/jni/Android.bp b/services/core/jni/Android.bp index 709d5e39463c..24ee16389fd2 100644 --- a/services/core/jni/Android.bp +++ b/services/core/jni/Android.bp @@ -189,7 +189,7 @@ cc_defaults { "android.hardware.thermal@1.0", "android.hardware.thermal-V1-ndk", "android.hardware.tv.input@1.0", - "android.hardware.tv.input-V1-ndk", + "android.hardware.tv.input-V2-ndk", "android.hardware.vibrator-V2-cpp", "android.hardware.vibrator@1.0", "android.hardware.vibrator@1.1", @@ -244,5 +244,5 @@ filegroup { filegroup { name: "lib_oomConnection_native", - srcs: ["com_android_server_am_OomConnection.cpp",], + srcs: ["com_android_server_am_OomConnection.cpp"], } diff --git a/services/core/jni/com_android_server_display_SmallAreaDetectionController.cpp b/services/core/jni/com_android_server_display_SmallAreaDetectionController.cpp index b256f168f2af..1844d3063cd8 100644 --- a/services/core/jni/com_android_server_display_SmallAreaDetectionController.cpp +++ b/services/core/jni/com_android_server_display_SmallAreaDetectionController.cpp @@ -24,33 +24,33 @@ #include "utils/Log.h" namespace android { -static void nativeUpdateSmallAreaDetection(JNIEnv* env, jclass clazz, jintArray juids, +static void nativeUpdateSmallAreaDetection(JNIEnv* env, jclass clazz, jintArray jappIds, jfloatArray jthresholds) { - if (juids == nullptr || jthresholds == nullptr) return; + if (jappIds == nullptr || jthresholds == nullptr) return; - ScopedIntArrayRO uids(env, juids); + ScopedIntArrayRO appIds(env, jappIds); ScopedFloatArrayRO thresholds(env, jthresholds); - if (uids.size() != thresholds.size()) { - ALOGE("uids size exceeds thresholds size!"); + if (appIds.size() != thresholds.size()) { + ALOGE("appIds size exceeds thresholds size!"); return; } - std::vector<int32_t> uidVector; + std::vector<int32_t> appIdVector; std::vector<float> thresholdVector; - size_t size = uids.size(); - uidVector.reserve(size); + size_t size = appIds.size(); + appIdVector.reserve(size); thresholdVector.reserve(size); for (int i = 0; i < size; i++) { - uidVector.push_back(static_cast<int32_t>(uids[i])); + appIdVector.push_back(static_cast<int32_t>(appIds[i])); thresholdVector.push_back(static_cast<float>(thresholds[i])); } - SurfaceComposerClient::updateSmallAreaDetection(uidVector, thresholdVector); + SurfaceComposerClient::updateSmallAreaDetection(appIdVector, thresholdVector); } -static void nativeSetSmallAreaDetectionThreshold(JNIEnv* env, jclass clazz, jint uid, +static void nativeSetSmallAreaDetectionThreshold(JNIEnv* env, jclass clazz, jint appId, jfloat threshold) { - SurfaceComposerClient::setSmallAreaDetectionThreshold(uid, threshold); + SurfaceComposerClient::setSmallAreaDetectionThreshold(appId, threshold); } static const JNINativeMethod gMethods[] = { diff --git a/services/core/jni/tvinput/JTvInputHal.cpp b/services/core/jni/tvinput/JTvInputHal.cpp index c7366173ecda..dc05462c1a1c 100644 --- a/services/core/jni/tvinput/JTvInputHal.cpp +++ b/services/core/jni/tvinput/JTvInputHal.cpp @@ -368,12 +368,20 @@ JTvInputHal::TvInputEventWrapper JTvInputHal::TvInputEventWrapper::createEventWr } JTvInputHal::TvMessageEventWrapper JTvInputHal::TvMessageEventWrapper::createEventWrapper( - const AidlTvMessageEvent& aidlTvMessageEvent) { + const AidlTvMessageEvent& aidlTvMessageEvent, bool isLegacyMessage) { + auto messageList = aidlTvMessageEvent.messages; TvMessageEventWrapper event; - event.messages.insert(event.messages.begin(), std::begin(aidlTvMessageEvent.messages) + 1, - std::end(aidlTvMessageEvent.messages)); + // Handle backwards compatibility for V1 + if (isLegacyMessage) { + event.deviceId = messageList[0].groupId; + event.messages.insert(event.messages.begin(), std::begin(messageList) + 1, + std::end(messageList)); + } else { + event.deviceId = aidlTvMessageEvent.deviceId; + event.messages.insert(event.messages.begin(), std::begin(messageList), + std::end(messageList)); + } event.streamId = aidlTvMessageEvent.streamId; - event.deviceId = aidlTvMessageEvent.messages[0].groupId; event.type = aidlTvMessageEvent.type; return event; } @@ -449,15 +457,30 @@ JTvInputHal::TvInputCallback::TvInputCallback(JTvInputHal* hal) { ::ndk::ScopedAStatus JTvInputHal::TvInputCallback::notifyTvMessageEvent( const AidlTvMessageEvent& event) { const std::string DEVICE_ID_SUBTYPE = "device_id"; - if (event.messages.size() > 1 && event.messages[0].subType == DEVICE_ID_SUBTYPE) { - mHal->mLooper - ->sendMessage(new NotifyTvMessageHandler(mHal, - TvMessageEventWrapper::createEventWrapper( - event)), - static_cast<int>(event.type)); + ::ndk::ScopedAStatus status = ::ndk::ScopedAStatus::ok(); + int32_t aidlVersion = 0; + if (mHal->mTvInput->getAidlInterfaceVersion(&aidlVersion).isOk() && event.messages.size() > 0) { + bool validLegacyMessage = aidlVersion == 1 && + event.messages[0].subType == DEVICE_ID_SUBTYPE && event.messages.size() > 1; + bool validTvMessage = aidlVersion > 1 && event.messages.size() > 0; + if (validLegacyMessage || validTvMessage) { + mHal->mLooper->sendMessage( + new NotifyTvMessageHandler(mHal, + TvMessageEventWrapper:: + createEventWrapper(event, + validLegacyMessage)), + static_cast<int>(event.type)); + } else { + status = ::ndk::ScopedAStatus::fromExceptionCode(EX_ILLEGAL_ARGUMENT); + ALOGE("The TVMessage event was malformed for HAL version: %d", aidlVersion); + } + } else { + status = ::ndk::ScopedAStatus::fromExceptionCode(EX_ILLEGAL_ARGUMENT); + ALOGE("The TVMessage event was empty or the HAL version (version: %d) could not " + "be inferred.", + aidlVersion); } - - return ::ndk::ScopedAStatus::ok(); + return status; } JTvInputHal::ITvInputWrapper::ITvInputWrapper(std::shared_ptr<AidlITvInput>& aidlTvInput) @@ -521,4 +544,12 @@ JTvInputHal::ITvInputWrapper::ITvInputWrapper(std::shared_ptr<AidlITvInput>& aid } } +::ndk::ScopedAStatus JTvInputHal::ITvInputWrapper::getAidlInterfaceVersion(int32_t* _aidl_return) { + if (mIsHidl) { + return ::ndk::ScopedAStatus::fromExceptionCode(EX_UNSUPPORTED_OPERATION); + } else { + return mAidlTvInput->getInterfaceVersion(_aidl_return); + } +} + } // namespace android diff --git a/services/core/jni/tvinput/JTvInputHal.h b/services/core/jni/tvinput/JTvInputHal.h index 1d8d1629f67d..6026a107c67f 100644 --- a/services/core/jni/tvinput/JTvInputHal.h +++ b/services/core/jni/tvinput/JTvInputHal.h @@ -138,7 +138,7 @@ private: TvMessageEventWrapper() {} static TvMessageEventWrapper createEventWrapper( - const AidlTvMessageEvent& aidlTvMessageEvent); + const AidlTvMessageEvent& aidlTvMessageEvent, bool isLegacyMessage); int streamId; int deviceId; @@ -195,6 +195,7 @@ private: ::ndk::ScopedAStatus getTvMessageQueueDesc( MQDescriptor<int8_t, SynchronizedReadWrite>* out_queue, int32_t in_deviceId, int32_t in_streamId); + ::ndk::ScopedAStatus getAidlInterfaceVersion(int32_t* _aidl_return); private: ::ndk::ScopedAStatus hidlSetCallback(const std::shared_ptr<TvInputCallback>& in_callback); diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java index c26aee8af83e..924e2f8ec654 100644 --- a/services/java/com/android/server/SystemServer.java +++ b/services/java/com/android/server/SystemServer.java @@ -338,6 +338,8 @@ public final class SystemServer implements Dumpable { "com.android.clockwork.modes.ModeManagerService"; private static final String WEAR_DISPLAY_SERVICE_CLASS = "com.android.clockwork.display.WearDisplayService"; + private static final String WEAR_DEBUG_SERVICE_CLASS = + "com.android.clockwork.debug.WearDebugService"; private static final String WEAR_TIME_SERVICE_CLASS = "com.android.clockwork.time.WearTimeService"; private static final String WEAR_SETTINGS_SERVICE_CLASS = @@ -2636,6 +2638,12 @@ public final class SystemServer implements Dumpable { mSystemServiceManager.startService(WEAR_DISPLAY_SERVICE_CLASS); t.traceEnd(); + if (Build.IS_DEBUGGABLE) { + t.traceBegin("StartWearDebugService"); + mSystemServiceManager.startService(WEAR_DEBUG_SERVICE_CLASS); + t.traceEnd(); + } + t.traceBegin("StartWearTimeService"); mSystemServiceManager.startService(WEAR_TIME_SERVICE_CLASS); t.traceEnd(); diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageManagerSettingsTests.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageManagerSettingsTests.java index 952cfc48b583..cbedcaf97358 100644 --- a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageManagerSettingsTests.java +++ b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageManagerSettingsTests.java @@ -43,6 +43,7 @@ import static org.mockito.Mockito.when; import android.annotation.NonNull; import android.app.PropertyInvalidatedCache; +import android.content.ComponentName; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.pm.SuspendDialogInfo; @@ -873,12 +874,20 @@ public class PackageManagerSettingsTests { .setUid(packageSetting.getAppId()) .hideAsFinal()); - ArchiveState archiveState = new ArchiveState( - List.of(new ArchiveState.ArchiveActivityInfo("title1", Path.of("/path1"), - Path.of("/monochromePath1")), - new ArchiveState.ArchiveActivityInfo("title2", Path.of("/path2"), - Path.of("/monochromePath2"))), - "installerTitle"); + ArchiveState archiveState = + new ArchiveState( + List.of( + new ArchiveState.ArchiveActivityInfo( + "title1", + new ComponentName("pkg1", "class1"), + Path.of("/path1"), + Path.of("/monochromePath1")), + new ArchiveState.ArchiveActivityInfo( + "title2", + new ComponentName("pkg2", "class2"), + Path.of("/path2"), + Path.of("/monochromePath2"))), + "installerTitle"); packageSetting.modifyUserState(UserHandle.SYSTEM.getIdentifier()).setArchiveState( archiveState); settings.mPackages.put(PACKAGE_NAME_1, packageSetting); diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageUserStateTest.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageUserStateTest.java index 58ae7406580e..87a297b0e86f 100644 --- a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageUserStateTest.java +++ b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageUserStateTest.java @@ -24,6 +24,7 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; +import android.content.ComponentName; import android.content.pm.PackageManager; import android.content.pm.SuspendDialogInfo; import android.content.pm.overlay.OverlayPaths; @@ -192,8 +193,8 @@ public class PackageUserStateTest { return new SuspendParams(dialogInfo, appExtras, launcherExtras); } - private static PersistableBundle createPersistableBundle(String lKey, long lValue, String sKey, - String sValue, String dKey, double dValue) { + private static PersistableBundle createPersistableBundle( + String lKey, long lValue, String sKey, String sValue, String dKey, double dValue) { final PersistableBundle result = new PersistableBundle(3); if (lKey != null) { result.putLong("com.unit_test." + lKey, lValue); @@ -320,6 +321,7 @@ public class PackageUserStateTest { assertEquals(0L, state.getLastPackageUsageTimeInMills()[i]); } } + private static void assertLastPackageUsageSet( PackageStateUnserialized state, int reason, long value) throws Exception { for (int i = state.getLastPackageUsageTimeInMills().length - 1; i >= 0; --i) { @@ -330,6 +332,7 @@ public class PackageUserStateTest { } } } + @Test public void testPackageUseReasons() throws Exception { PackageSetting packageSetting = Mockito.mock(PackageSetting.class); @@ -377,6 +380,7 @@ public class PackageUserStateTest { assertTrue(testState.setOverlayPaths(new OverlayPaths.Builder().build())); assertFalse(testState.setOverlayPaths(null)); } + @Test public void testSharedLibOverlayPaths() { final PackageUserStateImpl testState = new PackageUserStateImpl(); @@ -401,8 +405,12 @@ public class PackageUserStateTest { @Test public void archiveState() { PackageUserStateImpl packageUserState = new PackageUserStateImpl(); - ArchiveState.ArchiveActivityInfo archiveActivityInfo = new ArchiveState.ArchiveActivityInfo( - "appTitle", Path.of("/path1"), Path.of("/path2")); + ArchiveState.ArchiveActivityInfo archiveActivityInfo = + new ArchiveState.ArchiveActivityInfo( + "appTitle", + new ComponentName("pkg", "class"), + Path.of("/path1"), + Path.of("/path2")); ArchiveState archiveState = new ArchiveState(List.of(archiveActivityInfo), "installerTitle"); packageUserState.setArchiveState(archiveState); diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java index d021f1d5aaea..16d72e40fbb5 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java @@ -117,7 +117,6 @@ import com.android.server.display.feature.DisplayManagerFlags; import com.android.server.display.notifications.DisplayNotificationManager; import com.android.server.input.InputManagerInternal; import com.android.server.lights.LightsManager; -import com.android.server.pm.UserManagerInternal; import com.android.server.sensors.SensorManagerInternal; import com.android.server.wm.WindowManagerInternal; @@ -312,7 +311,6 @@ public class DisplayManagerServiceTest { @Mock SensorManager mSensorManager; @Mock DisplayDeviceConfig mMockDisplayDeviceConfig; @Mock PackageManagerInternal mMockPackageManagerInternal; - @Mock UserManagerInternal mMockUserManagerInternal; @Captor ArgumentCaptor<ContentRecordingSession> mContentRecordingSessionCaptor; @@ -336,8 +334,6 @@ public class DisplayManagerServiceTest { VirtualDeviceManagerInternal.class, mMockVirtualDeviceManagerInternal); LocalServices.removeServiceForTest(PackageManagerInternal.class); LocalServices.addService(PackageManagerInternal.class, mMockPackageManagerInternal); - LocalServices.removeServiceForTest(UserManagerInternal.class); - LocalServices.addService(UserManagerInternal.class, mMockUserManagerInternal); // TODO: b/287945043 mContext = spy(new ContextWrapper(ApplicationProvider.getApplicationContext())); mResources = Mockito.spy(mContext.getResources()); diff --git a/services/tests/displayservicetests/src/com/android/server/display/mode/DisplayModeDirectorTest.java b/services/tests/displayservicetests/src/com/android/server/display/mode/DisplayModeDirectorTest.java index c4f72b307eb7..6a95d5c57024 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/mode/DisplayModeDirectorTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/mode/DisplayModeDirectorTest.java @@ -102,6 +102,9 @@ import com.android.server.sensors.SensorManagerInternal.ProximityActiveListener; import com.android.server.statusbar.StatusBarManagerInternal; import com.android.server.testutils.FakeDeviceConfigInterface; +import junitparams.JUnitParamsRunner; +import junitparams.Parameters; + import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -121,26 +124,28 @@ import java.util.concurrent.Executor; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; -import junitparams.JUnitParamsRunner; -import junitparams.Parameters; - @SmallTest @RunWith(JUnitParamsRunner.class) public class DisplayModeDirectorTest { public static Collection<Object[]> getAppRequestedSizeTestCases() { var appRequestedSizeTestCases = Arrays.asList(new Object[][] { - {DEFAULT_MODE_75.getModeId(), Float.POSITIVE_INFINITY, - DEFAULT_MODE_75.getRefreshRate(), Map.of()}, - {APP_MODE_HIGH_90.getModeId(), Float.POSITIVE_INFINITY, - APP_MODE_HIGH_90.getRefreshRate(), - Map.of( + {/*expectedBaseModeId*/ DEFAULT_MODE_75.getModeId(), + /*expectedPhysicalRefreshRate*/ Float.POSITIVE_INFINITY, + /*expectedAppRequestedRefreshRate*/ DEFAULT_MODE_75.getRefreshRate(), + /*votesWithPriorities*/ Map.of()}, + {/*expectedBaseModeId*/ APP_MODE_HIGH_90.getModeId(), + /*expectedPhysicalRefreshRate*/ Float.POSITIVE_INFINITY, + /*expectedAppRequestedRefreshRate*/ APP_MODE_HIGH_90.getRefreshRate(), + /*votesWithPriorities*/ Map.of( Vote.PRIORITY_APP_REQUEST_SIZE, Vote.forSize(APP_MODE_HIGH_90.getPhysicalWidth(), APP_MODE_HIGH_90.getPhysicalHeight()), Vote.PRIORITY_APP_REQUEST_BASE_MODE_REFRESH_RATE, Vote.forBaseModeRefreshRate(APP_MODE_HIGH_90.getRefreshRate()))}, - {LIMIT_MODE_70.getModeId(), Float.POSITIVE_INFINITY, Float.POSITIVE_INFINITY, - Map.of( + {/*expectedBaseModeId*/ LIMIT_MODE_70.getModeId(), + /*expectedPhysicalRefreshRate*/ Float.POSITIVE_INFINITY, + /*expectedAppRequestedRefreshRate*/ Float.POSITIVE_INFINITY, + /*votesWithPriorities*/ Map.of( Vote.PRIORITY_APP_REQUEST_SIZE, Vote.forSize(APP_MODE_HIGH_90.getPhysicalWidth(), APP_MODE_HIGH_90.getPhysicalHeight()), @@ -149,9 +154,10 @@ public class DisplayModeDirectorTest { Vote.PRIORITY_LOW_POWER_MODE, Vote.forSize(LIMIT_MODE_70.getPhysicalWidth(), LIMIT_MODE_70.getPhysicalHeight()))}, - {LIMIT_MODE_70.getModeId(), LIMIT_MODE_70.getRefreshRate(), - LIMIT_MODE_70.getRefreshRate(), - Map.of( + {/*expectedBaseModeId*/ LIMIT_MODE_70.getModeId(), + /*expectedPhysicalRefreshRate*/ LIMIT_MODE_70.getRefreshRate(), + /*expectedAppRequestedRefreshRate*/ LIMIT_MODE_70.getRefreshRate(), + /*votesWithPriorities*/ Map.of( Vote.PRIORITY_APP_REQUEST_SIZE, Vote.forSize(APP_MODE_65.getPhysicalWidth(), APP_MODE_65.getPhysicalHeight()), @@ -160,9 +166,10 @@ public class DisplayModeDirectorTest { Vote.PRIORITY_LOW_POWER_MODE, Vote.forSize(LIMIT_MODE_70.getPhysicalWidth(), LIMIT_MODE_70.getPhysicalHeight()))}, - {LIMIT_MODE_70.getModeId(), LIMIT_MODE_70.getRefreshRate(), - LIMIT_MODE_70.getRefreshRate(), - Map.of( + {/*expectedBaseModeId*/ LIMIT_MODE_70.getModeId(), + /*expectedPhysicalRefreshRate*/ LIMIT_MODE_70.getRefreshRate(), + /*expectedAppRequestedRefreshRate*/ LIMIT_MODE_70.getRefreshRate(), + /*votesWithPriorities*/ Map.of( Vote.PRIORITY_APP_REQUEST_SIZE, Vote.forSize(APP_MODE_65.getPhysicalWidth(), APP_MODE_65.getPhysicalHeight()), @@ -173,10 +180,12 @@ public class DisplayModeDirectorTest { 0, 0, LIMIT_MODE_70.getPhysicalWidth(), LIMIT_MODE_70.getPhysicalHeight(), - 0, Float.POSITIVE_INFINITY)), false}, - {APP_MODE_65.getModeId(), APP_MODE_65.getRefreshRate(), - APP_MODE_65.getRefreshRate(), - Map.of( + 0, Float.POSITIVE_INFINITY)), + /*displayResolutionRangeVotingEnabled*/ false}, + {/*expectedBaseModeId*/ APP_MODE_65.getModeId(), + /*expectedPhysicalRefreshRate*/ APP_MODE_65.getRefreshRate(), + /*expectedAppRequestedRefreshRate*/ APP_MODE_65.getRefreshRate(), + /*votesWithPriorities*/ Map.of( Vote.PRIORITY_APP_REQUEST_SIZE, Vote.forSize(APP_MODE_65.getPhysicalWidth(), APP_MODE_65.getPhysicalHeight()), @@ -187,7 +196,40 @@ public class DisplayModeDirectorTest { 0, 0, LIMIT_MODE_70.getPhysicalWidth(), LIMIT_MODE_70.getPhysicalHeight(), - 0, Float.POSITIVE_INFINITY)), true}}); + 0, Float.POSITIVE_INFINITY)), + /*displayResolutionRangeVotingEnabled*/ true}, + {/*expectedBaseModeId*/ DEFAULT_MODE_75.getModeId(), + /*expectedPhysicalRefreshRate*/ APP_MODE_65.getRefreshRate(), + /*expectedAppRequestedRefreshRate*/ APP_MODE_HIGH_90.getRefreshRate(), + /*votesWithPriorities*/ Map.of( + Vote.PRIORITY_APP_REQUEST_SIZE, + Vote.forSize(APP_MODE_HIGH_90.getPhysicalWidth(), + APP_MODE_HIGH_90.getPhysicalHeight()), + Vote.PRIORITY_APP_REQUEST_BASE_MODE_REFRESH_RATE, + Vote.forBaseModeRefreshRate(APP_MODE_HIGH_90.getRefreshRate()), + Vote.PRIORITY_LOW_POWER_MODE, + Vote.forSizeAndPhysicalRefreshRatesRange( + 0, 0, + LIMIT_MODE_70.getPhysicalWidth(), + LIMIT_MODE_70.getPhysicalHeight(), + 0, APP_MODE_65.getRefreshRate())), + /*displayResolutionRangeVotingEnabled*/ false}, + {/*expectedBaseModeId*/ DEFAULT_MODE_60.getModeId(), // Resolution == APP_MODE_65 + /*expectedPhysicalRefreshRate*/ APP_MODE_65.getRefreshRate(), + /*expectedAppRequestedRefreshRate*/ APP_MODE_65.getRefreshRate(), + /*votesWithPriorities*/ Map.of( + Vote.PRIORITY_APP_REQUEST_SIZE, + Vote.forSize(APP_MODE_HIGH_90.getPhysicalWidth(), + APP_MODE_HIGH_90.getPhysicalHeight()), + Vote.PRIORITY_APP_REQUEST_BASE_MODE_REFRESH_RATE, + Vote.forBaseModeRefreshRate(APP_MODE_HIGH_90.getRefreshRate()), + Vote.PRIORITY_LOW_POWER_MODE, + Vote.forSizeAndPhysicalRefreshRatesRange( + 0, 0, + LIMIT_MODE_70.getPhysicalWidth(), + LIMIT_MODE_70.getPhysicalHeight(), + 0, APP_MODE_65.getRefreshRate())), + /*displayResolutionRangeVotingEnabled*/ true}}); final var res = new ArrayList<Object[]>(appRequestedSizeTestCases.size() * 2); @@ -218,6 +260,8 @@ public class DisplayModeDirectorTest { private static final boolean DEBUG = false; private static final float FLOAT_TOLERANCE = 0.01f; + private static final Display.Mode DEFAULT_MODE_60 = new Display.Mode( + /*modeId=*/60, /*width=*/1900, /*height=*/1900, 60); private static final Display.Mode APP_MODE_65 = new Display.Mode( /*modeId=*/65, /*width=*/1900, /*height=*/1900, 65); private static final Display.Mode LIMIT_MODE_70 = new Display.Mode( @@ -227,8 +271,7 @@ public class DisplayModeDirectorTest { private static final Display.Mode APP_MODE_HIGH_90 = new Display.Mode( /*modeId=*/90, /*width=*/3000, /*height=*/3000, 90); private static final Display.Mode[] TEST_MODES = new Display.Mode[] { - new Display.Mode( - /*modeId=*/60, /*width=*/1900, /*height=*/1900, 60), + DEFAULT_MODE_60, APP_MODE_65, LIMIT_MODE_70, DEFAULT_MODE_75, diff --git a/services/tests/mockingservicestests/src/com/android/server/am/AsyncProcessStartTest.java b/services/tests/mockingservicestests/src/com/android/server/am/AsyncProcessStartTest.java index 596a3f3d0400..b3605ccfc25d 100644 --- a/services/tests/mockingservicestests/src/com/android/server/am/AsyncProcessStartTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/am/AsyncProcessStartTest.java @@ -280,7 +280,9 @@ public class AsyncProcessStartTest { 0, 0); // Sleep until timeout should have triggered - SystemClock.sleep(ActivityManagerService.PROC_START_TIMEOUT + 1000); + if (wedge) { + SystemClock.sleep(ActivityManagerService.PROC_START_TIMEOUT + 1000); + } return app; } diff --git a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java index 410ae35aa790..367e14b37180 100644 --- a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java @@ -228,7 +228,7 @@ public class BroadcastQueueTest { LocalServices.removeServiceForTest(AlarmManagerInternal.class); LocalServices.addService(AlarmManagerInternal.class, mAlarmManagerInt); doReturn(new ComponentName("", "")).when(mPackageManagerInt).getSystemUiServiceComponent(); - doNothing().when(mPackageManagerInt).setPackageStoppedState(any(), anyBoolean(), anyInt()); + doNothing().when(mPackageManagerInt).notifyComponentUsed(any(), anyInt(), any(), any()); doAnswer((invocation) -> { return getUidForPackage(invocation.getArgument(0)); }).when(mPackageManagerInt).getPackageUid(any(), anyLong(), eq(UserHandle.USER_SYSTEM)); @@ -1014,8 +1014,9 @@ public class BroadcastQueueTest { eq(PackageManager.NOTIFY_PACKAGE_USE_BROADCAST_RECEIVER)); // Confirm that we unstopped manifest receivers - verify(mAms.mPackageManagerInt, atLeastOnce()).setPackageStoppedState( - eq(receiverApp.info.packageName), eq(false), eq(UserHandle.USER_SYSTEM)); + verify(mAms.mPackageManagerInt, atLeastOnce()).notifyComponentUsed( + eq(receiverApp.info.packageName), eq(UserHandle.USER_SYSTEM), + eq(callerApp.info.packageName), any()); } // Confirm that we've reported expected usage events diff --git a/services/tests/mockingservicestests/src/com/android/server/display/SmallAreaDetectionControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/display/SmallAreaDetectionControllerTest.java index 1ce79a5b596b..05ac5b5720e6 100644 --- a/services/tests/mockingservicestests/src/com/android/server/display/SmallAreaDetectionControllerTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/display/SmallAreaDetectionControllerTest.java @@ -16,8 +16,6 @@ package com.android.server.display; -import static android.os.Process.INVALID_UID; - import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doNothing; @@ -35,7 +33,7 @@ import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; import com.android.server.LocalServices; -import com.android.server.pm.UserManagerInternal; +import com.android.server.pm.pkg.PackageStateInternal; import org.junit.Before; import org.junit.Rule; @@ -55,7 +53,10 @@ public class SmallAreaDetectionControllerTest { @Mock private PackageManagerInternal mMockPackageManagerInternal; @Mock - private UserManagerInternal mMockUserManagerInternal; + private PackageStateInternal mMockPkgStateA; + @Mock + private PackageStateInternal mMockPkgStateB; + private SmallAreaDetectionController mSmallAreaDetectionController; @@ -64,29 +65,18 @@ public class SmallAreaDetectionControllerTest { private static final String PKG_NOT_INSTALLED = "com.not.installed"; private static final float THRESHOLD_A = 0.05f; private static final float THRESHOLD_B = 0.07f; - private static final int USER_1 = 110; - private static final int USER_2 = 111; - private static final int UID_A_1 = 11011111; - private static final int UID_A_2 = 11111111; - private static final int UID_B_1 = 11022222; - private static final int UID_B_2 = 11122222; + private static final int APP_ID_A = 11111; + private static final int APP_ID_B = 22222; @Before public void setup() { LocalServices.removeServiceForTest(PackageManagerInternal.class); LocalServices.addService(PackageManagerInternal.class, mMockPackageManagerInternal); - LocalServices.removeServiceForTest(UserManagerInternal.class); - LocalServices.addService(UserManagerInternal.class, mMockUserManagerInternal); - - when(mMockUserManagerInternal.getUserIds()).thenReturn(new int[]{USER_1, USER_2}); - when(mMockPackageManagerInternal.getPackageUid(PKG_A, 0, USER_1)).thenReturn(UID_A_1); - when(mMockPackageManagerInternal.getPackageUid(PKG_A, 0, USER_2)).thenReturn(UID_A_2); - when(mMockPackageManagerInternal.getPackageUid(PKG_B, 0, USER_1)).thenReturn(UID_B_1); - when(mMockPackageManagerInternal.getPackageUid(PKG_B, 0, USER_2)).thenReturn(UID_B_2); - when(mMockPackageManagerInternal.getPackageUid(PKG_NOT_INSTALLED, 0, USER_1)).thenReturn( - INVALID_UID); - when(mMockPackageManagerInternal.getPackageUid(PKG_NOT_INSTALLED, 0, USER_2)).thenReturn( - INVALID_UID); + + when(mMockPackageManagerInternal.getPackageStateInternal(PKG_A)).thenReturn(mMockPkgStateA); + when(mMockPackageManagerInternal.getPackageStateInternal(PKG_B)).thenReturn(mMockPkgStateB); + when(mMockPkgStateA.getAppId()).thenReturn(APP_ID_A); + when(mMockPkgStateB.getAppId()).thenReturn(APP_ID_B); mSmallAreaDetectionController = spy(new SmallAreaDetectionController( new ContextWrapper(ApplicationProvider.getApplicationContext()), @@ -99,9 +89,9 @@ public class SmallAreaDetectionControllerTest { final String property = PKG_A + ":" + THRESHOLD_A + "," + PKG_B + ":" + THRESHOLD_B; mSmallAreaDetectionController.updateAllowlist(property); - final int[] resultUidArray = {UID_A_1, UID_B_1, UID_A_2, UID_B_2}; - final float[] resultThresholdArray = {THRESHOLD_A, THRESHOLD_B, THRESHOLD_A, THRESHOLD_B}; - verify(mSmallAreaDetectionController).updateSmallAreaDetection(eq(resultUidArray), + final int[] resultAppIdArray = {APP_ID_A, APP_ID_B}; + final float[] resultThresholdArray = {THRESHOLD_A, THRESHOLD_B}; + verify(mSmallAreaDetectionController).updateSmallAreaDetection(eq(resultAppIdArray), eq(resultThresholdArray)); } @@ -110,9 +100,9 @@ public class SmallAreaDetectionControllerTest { final String property = PKG_A + "," + PKG_B + ":" + THRESHOLD_B; mSmallAreaDetectionController.updateAllowlist(property); - final int[] resultUidArray = {UID_B_1, UID_B_2}; - final float[] resultThresholdArray = {THRESHOLD_B, THRESHOLD_B}; - verify(mSmallAreaDetectionController).updateSmallAreaDetection(eq(resultUidArray), + final int[] resultAppIdArray = {APP_ID_B}; + final float[] resultThresholdArray = {THRESHOLD_B}; + verify(mSmallAreaDetectionController).updateSmallAreaDetection(eq(resultAppIdArray), eq(resultThresholdArray)); } @@ -122,9 +112,9 @@ public class SmallAreaDetectionControllerTest { PKG_A + ":" + THRESHOLD_A + "," + PKG_NOT_INSTALLED + ":" + THRESHOLD_B; mSmallAreaDetectionController.updateAllowlist(property); - final int[] resultUidArray = {UID_A_1, UID_A_2}; - final float[] resultThresholdArray = {THRESHOLD_A, THRESHOLD_A}; - verify(mSmallAreaDetectionController).updateSmallAreaDetection(eq(resultUidArray), + final int[] resultAppIdArray = {APP_ID_A}; + final float[] resultThresholdArray = {THRESHOLD_A}; + verify(mSmallAreaDetectionController).updateSmallAreaDetection(eq(resultAppIdArray), eq(resultThresholdArray)); } 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 eb50556821eb..610ea903767e 100644 --- a/services/tests/mockingservicestests/src/com/android/server/pm/PackageArchiverTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/pm/PackageArchiverTest.java @@ -35,6 +35,7 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.app.AppOpsManager; +import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.IntentSender; @@ -427,6 +428,7 @@ public class PackageArchiverTest { for (LauncherActivityInfo mainActivity : createLauncherActivities()) { ArchiveState.ArchiveActivityInfo activityInfo = new ArchiveState.ArchiveActivityInfo( mainActivity.getLabel().toString(), + mainActivity.getComponentName(), ICON_PATH, null); activityInfos.add(activityInfo); } @@ -437,9 +439,11 @@ public class PackageArchiverTest { ActivityInfo activityInfo = mock(ActivityInfo.class); LauncherActivityInfo activity1 = mock(LauncherActivityInfo.class); when(activity1.getLabel()).thenReturn("activity1"); + when(activity1.getComponentName()).thenReturn(new ComponentName("pkg1", "class1")); when(activity1.getActivityInfo()).thenReturn(activityInfo); LauncherActivityInfo activity2 = mock(LauncherActivityInfo.class); when(activity2.getLabel()).thenReturn("activity2"); + when(activity2.getComponentName()).thenReturn(new ComponentName("pkg2", "class2")); when(activity2.getActivityInfo()).thenReturn(activityInfo); return List.of(activity1, activity2); } 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 e8cbcf9a6874..a3d415e4918f 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 @@ -337,11 +337,7 @@ public class VirtualDeviceManagerServiceTest { LocalServices.removeServiceForTest(DisplayManagerInternal.class); LocalServices.addService(DisplayManagerInternal.class, mDisplayManagerInternalMock); - mSetFlagsRule.disableFlags(Flags.FLAG_VDM_PUBLIC_APIS); - mSetFlagsRule.disableFlags(Flags.FLAG_DYNAMIC_POLICY); - mSetFlagsRule.disableFlags(Flags.FLAG_STREAM_PERMISSIONS); - mSetFlagsRule.disableFlags(Flags.FLAG_VDM_CUSTOM_HOME); - mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_NATIVE_VDM); + mSetFlagsRule.initAllFlagsToReleaseConfigDefault(); doReturn(true).when(mInputManagerInternalMock).setVirtualMousePointerDisplayId(anyInt()); doNothing().when(mInputManagerInternalMock).setPointerAcceleration(anyFloat(), anyInt()); diff --git a/services/tests/servicestests/src/com/android/server/contentcapture/ContentCaptureManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/contentcapture/ContentCaptureManagerServiceTest.java index 12d6161eb718..5cc84b197e03 100644 --- a/services/tests/servicestests/src/com/android/server/contentcapture/ContentCaptureManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/contentcapture/ContentCaptureManagerServiceTest.java @@ -59,6 +59,7 @@ import org.mockito.Mock; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; +import java.util.Collections; import java.util.List; /** @@ -103,6 +104,10 @@ public class ContentCaptureManagerServiceTest { private boolean mDevCfgEnableContentProtectionReceiver; + private List<List<String>> mDevCfgContentProtectionRequiredGroups = List.of(List.of("a")); + + private List<List<String>> mDevCfgContentProtectionOptionalGroups = Collections.emptyList(); + private int mContentProtectionBlocklistManagersCreated; private int mContentProtectionServiceInfosCreated; @@ -374,7 +379,21 @@ public class ContentCaptureManagerServiceTest { } @Test - public void isContentProtectionReceiverEnabled_withoutManagers() { + public void isContentProtectionReceiverEnabled_true() { + when(mMockContentProtectionConsentManager.isConsentGranted(USER_ID)).thenReturn(true); + when(mMockContentProtectionBlocklistManager.isAllowed(PACKAGE_NAME)).thenReturn(true); + mDevCfgEnableContentProtectionReceiver = true; + mContentCaptureManagerService = new TestContentCaptureManagerService(); + + boolean actual = + mContentCaptureManagerService.mGlobalContentCaptureOptions.isWhitelisted( + USER_ID, PACKAGE_NAME); + + assertThat(actual).isTrue(); + } + + @Test + public void isContentProtectionReceiverEnabled_false_withoutManagers() { boolean actual = mContentCaptureManagerService.mGlobalContentCaptureOptions.isWhitelisted( USER_ID, PACKAGE_NAME); @@ -385,7 +404,7 @@ public class ContentCaptureManagerServiceTest { } @Test - public void isContentProtectionReceiverEnabled_disabledWithFlag() { + public void isContentProtectionReceiverEnabled_false_disabledWithFlag() { mDevCfgEnableContentProtectionReceiver = true; mContentCaptureManagerService = new TestContentCaptureManagerService(); mContentCaptureManagerService.mDevCfgEnableContentProtectionReceiver = false; @@ -400,6 +419,22 @@ public class ContentCaptureManagerServiceTest { } @Test + public void isContentProtectionReceiverEnabled_false_emptyGroups() { + mDevCfgEnableContentProtectionReceiver = true; + mDevCfgContentProtectionRequiredGroups = Collections.emptyList(); + mDevCfgContentProtectionOptionalGroups = Collections.emptyList(); + mContentCaptureManagerService = new TestContentCaptureManagerService(); + + boolean actual = + mContentCaptureManagerService.mGlobalContentCaptureOptions.isWhitelisted( + USER_ID, PACKAGE_NAME); + + assertThat(actual).isFalse(); + verify(mMockContentProtectionConsentManager, never()).isConsentGranted(anyInt()); + verify(mMockContentProtectionBlocklistManager, never()).isAllowed(anyString()); + } + + @Test public void onLoginDetected_disabledAfterConstructor() { mDevCfgEnableContentProtectionReceiver = true; mContentCaptureManagerService = new TestContentCaptureManagerService(); @@ -525,6 +560,10 @@ public class ContentCaptureManagerServiceTest { super(sContext); this.mDevCfgEnableContentProtectionReceiver = ContentCaptureManagerServiceTest.this.mDevCfgEnableContentProtectionReceiver; + this.mDevCfgContentProtectionRequiredGroups = + ContentCaptureManagerServiceTest.this.mDevCfgContentProtectionRequiredGroups; + this.mDevCfgContentProtectionOptionalGroups = + ContentCaptureManagerServiceTest.this.mDevCfgContentProtectionOptionalGroups; } @Override diff --git a/services/tests/servicestests/src/com/android/server/media/projection/MediaProjectionSessionIdGeneratorTest.java b/services/tests/servicestests/src/com/android/server/media/projection/MediaProjectionSessionIdGeneratorTest.java new file mode 100644 index 000000000000..07cdf4df47ae --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/media/projection/MediaProjectionSessionIdGeneratorTest.java @@ -0,0 +1,103 @@ +/* + * 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.projection; + +import static com.google.common.truth.Truth.assertThat; + +import android.content.Context; +import android.content.SharedPreferences; +import android.platform.test.annotations.Presubmit; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; +import androidx.test.platform.app.InstrumentationRegistry; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.io.File; + +/** + * Tests for the {@link MediaProjectionSessionIdGenerator} class. + * + * <p>Build/Install/Run: atest FrameworksServicesTests:MediaProjectionSessionIdGeneratorTest + */ +@SmallTest +@Presubmit +@RunWith(AndroidJUnit4.class) +public class MediaProjectionSessionIdGeneratorTest { + + private static final String TEST_PREFS_FILE = "media-projection-session-id-test"; + + private final Context mContext = + InstrumentationRegistry.getInstrumentation().getTargetContext(); + private final File mSharedPreferencesFile = new File(mContext.getCacheDir(), TEST_PREFS_FILE); + private final SharedPreferences mSharedPreferences = createSharePreferences(); + private final MediaProjectionSessionIdGenerator mGenerator = + createGenerator(mSharedPreferences); + + @Before + public void setUp() { + mSharedPreferences.edit().clear().commit(); + } + + @After + public void tearDown() { + mSharedPreferences.edit().clear().commit(); + mSharedPreferencesFile.delete(); + } + + @Test + public void getCurrentSessionId_byDefault_returns0() { + assertThat(mGenerator.getCurrentSessionId()).isEqualTo(0); + } + + @Test + public void getCurrentSessionId_multipleTimes_returnsSameValue() { + assertThat(mGenerator.getCurrentSessionId()).isEqualTo(0); + assertThat(mGenerator.getCurrentSessionId()).isEqualTo(0); + assertThat(mGenerator.getCurrentSessionId()).isEqualTo(0); + } + + @Test + public void createAndGetNewSessionId_returnsIncrementedId() { + int previousValue = mGenerator.getCurrentSessionId(); + + int newValue = mGenerator.createAndGetNewSessionId(); + + assertThat(newValue).isEqualTo(previousValue + 1); + } + + @Test + public void createAndGetNewSessionId_persistsNewValue() { + int newValue = mGenerator.createAndGetNewSessionId(); + + MediaProjectionSessionIdGenerator newInstance = createGenerator(createSharePreferences()); + + assertThat(newInstance.getCurrentSessionId()).isEqualTo(newValue); + } + + private SharedPreferences createSharePreferences() { + return mContext.getSharedPreferences(mSharedPreferencesFile, Context.MODE_PRIVATE); + } + + private MediaProjectionSessionIdGenerator createGenerator(SharedPreferences sharedPreferences) { + return new MediaProjectionSessionIdGenerator(sharedPreferences); + } +} diff --git a/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java index 8e7ba7030e82..dd7dec0bdb2b 100644 --- a/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java @@ -130,12 +130,22 @@ public class BackNavigationControllerTests extends WindowTestsBase { // verify if back animation would start. assertTrue("Animation scheduled", backNavigationInfo.isPrepareRemoteAnimation()); - // reset drawing status + // reset drawing status to test translucent activity backNavigationInfo.onBackNavigationFinished(false); mBackNavigationController.clearBackAnimations(); - topTask.forAllWindows(w -> { - makeWindowVisibleAndDrawn(w); - }, true); + final ActivityRecord topActivity = topTask.getTopMostActivity(); + makeWindowVisibleAndDrawn(topActivity.findMainWindow()); + // simulate translucent + topActivity.setOccludesParent(false); + backNavigationInfo = startBackNavigation(); + assertThat(typeToString(backNavigationInfo.getType())) + .isEqualTo(typeToString(BackNavigationInfo.TYPE_CALLBACK)); + + // reset drawing status to test keyguard occludes + topActivity.setOccludesParent(true); + backNavigationInfo.onBackNavigationFinished(false); + mBackNavigationController.clearBackAnimations(); + makeWindowVisibleAndDrawn(topActivity.findMainWindow()); setupKeyguardOccluded(); backNavigationInfo = startBackNavigation(); assertThat(typeToString(backNavigationInfo.getType())) @@ -201,9 +211,7 @@ public class BackNavigationControllerTests extends WindowTestsBase { // reset drawing status backNavigationInfo.onBackNavigationFinished(false); mBackNavigationController.clearBackAnimations(); - testCase.recordFront.forAllWindows(w -> { - makeWindowVisibleAndDrawn(w); - }, true); + makeWindowVisibleAndDrawn(testCase.recordFront.findMainWindow()); setupKeyguardOccluded(); backNavigationInfo = startBackNavigation(); assertThat(typeToString(backNavigationInfo.getType())) diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java index 2bf13857e537..6235b3b67145 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java @@ -26,7 +26,9 @@ import static android.view.WindowManager.TRANSIT_NONE; import static android.view.WindowManager.TRANSIT_OPEN; import static android.window.TaskFragmentOperation.OP_TYPE_CREATE_TASK_FRAGMENT; import static android.window.TaskFragmentOperation.OP_TYPE_DELETE_TASK_FRAGMENT; +import static android.window.TaskFragmentOperation.OP_TYPE_REORDER_TO_BOTTOM_OF_TASK; import static android.window.TaskFragmentOperation.OP_TYPE_REORDER_TO_FRONT; +import static android.window.TaskFragmentOperation.OP_TYPE_REORDER_TO_TOP_OF_TASK; import static android.window.TaskFragmentOperation.OP_TYPE_REPARENT_ACTIVITY_TO_TASK_FRAGMENT; import static android.window.TaskFragmentOperation.OP_TYPE_SET_ADJACENT_TASK_FRAGMENTS; import static android.window.TaskFragmentOperation.OP_TYPE_SET_ANIMATION_PARAMS; @@ -1655,6 +1657,127 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase { assertEquals(frontMostTaskFragment, tf0); } + @Test + public void testApplyTransaction_reorderToBottomOfTask() { + mController.unregisterOrganizer(mIOrganizer); + mController.registerOrganizerInternal(mIOrganizer, true /* isSystemOrganizer */); + final Task task = createTask(mDisplayContent); + // Create a non-embedded Activity at the bottom. + final ActivityRecord bottomActivity = new ActivityBuilder(mAtm) + .setTask(task) + .build(); + final TaskFragment tf0 = createTaskFragment(task); + final TaskFragment tf1 = createTaskFragment(task); + // Create a non-embedded Activity at the top. + final ActivityRecord topActivity = new ActivityBuilder(mAtm) + .setTask(task) + .build(); + + // Ensure correct order of the children before the operation + assertEquals(topActivity, task.getChildAt(3).asActivityRecord()); + assertEquals(tf1, task.getChildAt(2).asTaskFragment()); + assertEquals(tf0, task.getChildAt(1).asTaskFragment()); + assertEquals(bottomActivity, task.getChildAt(0).asActivityRecord()); + + // Reorder TaskFragment to bottom + final TaskFragmentOperation operation = new TaskFragmentOperation.Builder( + OP_TYPE_REORDER_TO_BOTTOM_OF_TASK).build(); + mTransaction.addTaskFragmentOperation(tf1.getFragmentToken(), operation); + assertApplyTransactionAllowed(mTransaction); + + // Ensure correct order of the children after the operation + assertEquals(topActivity, task.getChildAt(3).asActivityRecord()); + assertEquals(tf0, task.getChildAt(2).asTaskFragment()); + assertEquals(bottomActivity, task.getChildAt(1).asActivityRecord()); + assertEquals(tf1, task.getChildAt(0).asTaskFragment()); + } + + @Test + public void testApplyTransaction_reorderToTopOfTask() { + mController.unregisterOrganizer(mIOrganizer); + mController.registerOrganizerInternal(mIOrganizer, true /* isSystemOrganizer */); + final Task task = createTask(mDisplayContent); + // Create a non-embedded Activity at the bottom. + final ActivityRecord bottomActivity = new ActivityBuilder(mAtm) + .setTask(task) + .build(); + final TaskFragment tf0 = createTaskFragment(task); + final TaskFragment tf1 = createTaskFragment(task); + // Create a non-embedded Activity at the top. + final ActivityRecord topActivity = new ActivityBuilder(mAtm) + .setTask(task) + .build(); + + // Ensure correct order of the children before the operation + assertEquals(topActivity, task.getChildAt(3).asActivityRecord()); + assertEquals(tf1, task.getChildAt(2).asTaskFragment()); + assertEquals(tf0, task.getChildAt(1).asTaskFragment()); + assertEquals(bottomActivity, task.getChildAt(0).asActivityRecord()); + + // Reorder TaskFragment to top + final TaskFragmentOperation operation = new TaskFragmentOperation.Builder( + OP_TYPE_REORDER_TO_TOP_OF_TASK).build(); + mTransaction.addTaskFragmentOperation(tf0.getFragmentToken(), operation); + assertApplyTransactionAllowed(mTransaction); + + // Ensure correct order of the children after the operation + assertEquals(tf0, task.getChildAt(3).asTaskFragment()); + assertEquals(topActivity, task.getChildAt(2).asActivityRecord()); + assertEquals(tf1, task.getChildAt(1).asTaskFragment()); + assertEquals(bottomActivity, task.getChildAt(0).asActivityRecord()); + } + + @Test + public void testApplyTransaction_reorderToBottomOfTask_failsIfNotSystemOrganizer() { + testApplyTransaction_reorder_failsIfNotSystemOrganizer_common( + OP_TYPE_REORDER_TO_BOTTOM_OF_TASK); + } + + @Test + public void testApplyTransaction_reorderToTopOfTask_failsIfNotSystemOrganizer() { + testApplyTransaction_reorder_failsIfNotSystemOrganizer_common( + OP_TYPE_REORDER_TO_TOP_OF_TASK); + } + + private void testApplyTransaction_reorder_failsIfNotSystemOrganizer_common( + @TaskFragmentOperation.OperationType int opType) { + final Task task = createTask(mDisplayContent); + // Create a non-embedded Activity at the bottom. + final ActivityRecord bottomActivity = new ActivityBuilder(mAtm) + .setTask(task) + .build(); + final TaskFragment tf0 = createTaskFragment(task); + final TaskFragment tf1 = createTaskFragment(task); + // Create a non-embedded Activity at the top. + final ActivityRecord topActivity = new ActivityBuilder(mAtm) + .setTask(task) + .build(); + + // Ensure correct order of the children before the operation + assertEquals(topActivity, task.getChildAt(3).asActivityRecord()); + assertEquals(tf1, task.getChildAt(2).asTaskFragment()); + assertEquals(tf0, task.getChildAt(1).asTaskFragment()); + assertEquals(bottomActivity, task.getChildAt(0).asActivityRecord()); + + // Apply reorder transaction, which is expected to fail for non-system organizer. + final TaskFragmentOperation operation = new TaskFragmentOperation.Builder( + opType).build(); + mTransaction + .addTaskFragmentOperation(tf0.getFragmentToken(), operation) + .setErrorCallbackToken(mErrorToken); + assertApplyTransactionAllowed(mTransaction); + // The pending event will be dispatched on the handler (from requestTraversal). + waitHandlerIdle(mWm.mAnimationHandler); + + assertTaskFragmentErrorTransaction(opType, SecurityException.class); + + // Ensure no change to the order of the children after the operation + assertEquals(topActivity, task.getChildAt(3).asActivityRecord()); + assertEquals(tf1, task.getChildAt(2).asTaskFragment()); + assertEquals(tf0, task.getChildAt(1).asTaskFragment()); + assertEquals(bottomActivity, task.getChildAt(0).asActivityRecord()); + } + /** * Creates a {@link TaskFragment} with the {@link WindowContainerTransaction}. Calls * {@link WindowOrganizerController#applyTransaction(WindowContainerTransaction)} to apply the @@ -1782,6 +1905,19 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase { assertEquals(activityToken, change.getActivityToken()); } + /** Setups an embedded TaskFragment. */ + private TaskFragment createTaskFragment(Task task) { + final IBinder token = new Binder(); + TaskFragment taskFragment = new TaskFragmentBuilder(mAtm) + .setParentTask(task) + .setFragmentToken(token) + .setOrganizer(mOrganizer) + .createActivityCount(1) + .build(); + mWindowOrganizerController.mLaunchTaskFragments.put(token, taskFragment); + return taskFragment; + } + /** Setups an embedded TaskFragment in a PIP Task. */ private void setupTaskFragmentInPip() { mTaskFragment = new TaskFragmentBuilder(mAtm) 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 e86fc366a631..eaeb8049b81a 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java @@ -100,6 +100,9 @@ import androidx.test.platform.app.InstrumentationRegistry; import com.android.compatibility.common.util.AdoptShellPermissionsRule; import com.android.internal.os.IResultReceiver; import com.android.server.LocalServices; +import com.android.server.wm.WindowManagerService.WindowContainerInfo; + +import com.google.common.truth.Expect; import org.junit.Rule; import org.junit.Test; @@ -125,6 +128,9 @@ public class WindowManagerServiceTests extends WindowTestsBase { InstrumentationRegistry.getInstrumentation().getUiAutomation(), ADD_TRUSTED_DISPLAY); + @Rule + public Expect mExpect = Expect.create(); + @Test public void testIsRequestedOrientationMapped() { mWm.setOrientationRequestPolicy(/* isIgnoreOrientationRequestDisabled*/ true, @@ -674,64 +680,68 @@ public class WindowManagerServiceTests extends WindowTestsBase { @Test public void testGetTaskWindowContainerTokenForLaunchCookie_nullCookie() { - WindowContainerToken wct = mWm.getTaskWindowContainerTokenForLaunchCookie(null); - assertThat(wct).isNull(); + WindowContainerInfo wci = mWm.getTaskWindowContainerInfoForLaunchCookie(null); + assertThat(wci).isNull(); } @Test public void testGetTaskWindowContainerTokenForLaunchCookie_invalidCookie() { Binder cookie = new Binder("test cookie"); - WindowContainerToken wct = mWm.getTaskWindowContainerTokenForLaunchCookie(cookie); - assertThat(wct).isNull(); + WindowContainerInfo wci = mWm.getTaskWindowContainerInfoForLaunchCookie(cookie); + assertThat(wci).isNull(); final ActivityRecord testActivity = new ActivityBuilder(mAtm) .setCreateTask(true) .build(); - wct = mWm.getTaskWindowContainerTokenForLaunchCookie(cookie); - assertThat(wct).isNull(); + wci = mWm.getTaskWindowContainerInfoForLaunchCookie(cookie); + assertThat(wci).isNull(); } @Test public void testGetTaskWindowContainerTokenForLaunchCookie_validCookie() { final Binder cookie = new Binder("ginger cookie"); final WindowContainerToken launchRootTask = mock(WindowContainerToken.class); - setupActivityWithLaunchCookie(cookie, launchRootTask); + final int uid = 123; + setupActivityWithLaunchCookie(cookie, launchRootTask, uid); - WindowContainerToken wct = mWm.getTaskWindowContainerTokenForLaunchCookie(cookie); - assertThat(wct).isEqualTo(launchRootTask); + WindowContainerInfo wci = mWm.getTaskWindowContainerInfoForLaunchCookie(cookie); + mExpect.that(wci.getToken()).isEqualTo(launchRootTask); + mExpect.that(wci.getUid()).isEqualTo(uid); } @Test public void testGetTaskWindowContainerTokenForLaunchCookie_multipleCookies() { final Binder cookie1 = new Binder("ginger cookie"); final WindowContainerToken launchRootTask1 = mock(WindowContainerToken.class); - setupActivityWithLaunchCookie(cookie1, launchRootTask1); + final int uid1 = 123; + setupActivityWithLaunchCookie(cookie1, launchRootTask1, uid1); setupActivityWithLaunchCookie(new Binder("choc chip cookie"), - mock(WindowContainerToken.class)); + mock(WindowContainerToken.class), /* uid= */ 456); setupActivityWithLaunchCookie(new Binder("peanut butter cookie"), - mock(WindowContainerToken.class)); + mock(WindowContainerToken.class), /* uid= */ 789); - WindowContainerToken wct = mWm.getTaskWindowContainerTokenForLaunchCookie(cookie1); - assertThat(wct).isEqualTo(launchRootTask1); + WindowContainerInfo wci = mWm.getTaskWindowContainerInfoForLaunchCookie(cookie1); + mExpect.that(wci.getToken()).isEqualTo(launchRootTask1); + mExpect.that(wci.getUid()).isEqualTo(uid1); } @Test public void testGetTaskWindowContainerTokenForLaunchCookie_multipleCookies_noneValid() { setupActivityWithLaunchCookie(new Binder("ginger cookie"), - mock(WindowContainerToken.class)); + mock(WindowContainerToken.class), /* uid= */ 123); setupActivityWithLaunchCookie(new Binder("choc chip cookie"), - mock(WindowContainerToken.class)); + mock(WindowContainerToken.class), /* uid= */ 456); setupActivityWithLaunchCookie(new Binder("peanut butter cookie"), - mock(WindowContainerToken.class)); + mock(WindowContainerToken.class), /* uid= */ 789); - WindowContainerToken wct = mWm.getTaskWindowContainerTokenForLaunchCookie( + WindowContainerInfo wci = mWm.getTaskWindowContainerInfoForLaunchCookie( new Binder("some other cookie")); - assertThat(wct).isNull(); + assertThat(wci).isNull(); } @Test @@ -778,17 +788,18 @@ public class WindowManagerServiceTests extends WindowTestsBase { } @Test - public void setContentRecordingSession_matchingTask_mutatesSessionWithWindowContainerToken() { + public void setContentRecordingSession_matchingTask_mutatesSessionWithWindowContainerInfo() { WindowManagerInternal wmInternal = LocalServices.getService(WindowManagerInternal.class); Task task = createTask(mDefaultDisplay); ActivityRecord activityRecord = createActivityRecord(task); - ContentRecordingSession session = ContentRecordingSession.createTaskSession( - activityRecord.mLaunchCookie); + ContentRecordingSession session = + ContentRecordingSession.createTaskSession(activityRecord.mLaunchCookie); wmInternal.setContentRecordingSession(session); - assertThat(session.getTokenToRecord()).isEqualTo( - task.mRemoteToken.toWindowContainerToken().asBinder()); + mExpect.that(session.getTokenToRecord()) + .isEqualTo(task.mRemoteToken.toWindowContainerToken().asBinder()); + mExpect.that(session.getTargetUid()).isEqualTo(activityRecord.getUid()); } @Test @@ -1010,12 +1021,12 @@ public class WindowManagerServiceTests extends WindowTestsBase { } } - private void setupActivityWithLaunchCookie(IBinder launchCookie, WindowContainerToken wct) { + private void setupActivityWithLaunchCookie( + IBinder launchCookie, WindowContainerToken wct, int uid) { final WindowContainer.RemoteToken remoteToken = mock(WindowContainer.RemoteToken.class); when(remoteToken.toWindowContainerToken()).thenReturn(wct); - final ActivityRecord testActivity = new ActivityBuilder(mAtm) - .setCreateTask(true) - .build(); + final ActivityRecord testActivity = + new ActivityBuilder(mAtm).setCreateTask(true).setUid(uid).build(); testActivity.mLaunchCookie = launchCookie; testActivity.getTask().mRemoteToken = remoteToken; } diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java index 7168670f9652..0b77fd828745 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java @@ -40,6 +40,7 @@ import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; import static com.android.dx.mockito.inline.extended.ExtendedMockito.times; import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; import static com.android.dx.mockito.inline.extended.ExtendedMockito.when; +import static com.android.server.wm.testing.Assert.assertThrows; import static com.android.server.wm.ActivityRecord.State.RESUMED; import static com.android.server.wm.WindowContainer.POSITION_TOP; import static com.android.server.wm.WindowContainer.SYNC_STATE_READY; @@ -58,6 +59,7 @@ import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.clearInvocations; +import android.annotation.NonNull; import android.app.ActivityManager; import android.app.ActivityManager.RunningTaskInfo; import android.app.ActivityOptions; @@ -77,11 +79,13 @@ import android.util.Rational; import android.view.Display; import android.view.SurfaceControl; import android.view.WindowInsets; +import android.window.ITaskFragmentOrganizer; import android.window.ITaskOrganizer; import android.window.IWindowContainerTransactionCallback; import android.window.StartingWindowInfo; import android.window.StartingWindowRemovalInfo; import android.window.TaskAppearedInfo; +import android.window.TaskFragmentOrganizer; import android.window.WindowContainerToken; import android.window.WindowContainerTransaction; @@ -579,6 +583,87 @@ public class WindowOrganizerTests extends WindowTestsBase { } @Test + public void testTaskFragmentHiddenAndFocusableChanges() { + removeGlobalMinSizeRestriction(); + final Task rootTask = new TaskBuilder(mSupervisor).setCreateActivity(true) + .setWindowingMode(WINDOWING_MODE_FULLSCREEN).build(); + + final WindowContainerTransaction t = new WindowContainerTransaction(); + final TaskFragmentOrganizer organizer = + createTaskFragmentOrganizer(t, true /* isSystemOrganizer */); + + final IBinder token = new Binder(); + final TaskFragment taskFragment = new TaskFragmentBuilder(mAtm) + .setParentTask(rootTask) + .setFragmentToken(token) + .setOrganizer(organizer) + .createActivityCount(1) + .build(); + + // Should be visible and focusable initially. + assertTrue(rootTask.shouldBeVisible(null)); + assertTrue(taskFragment.shouldBeVisible(null)); + assertTrue(taskFragment.isFocusable()); + assertTrue(taskFragment.isTopActivityFocusable()); + + // Apply transaction to the TaskFragment hidden and not focusable. + t.setHidden(taskFragment.mRemoteToken.toWindowContainerToken(), true); + t.setFocusable(taskFragment.mRemoteToken.toWindowContainerToken(), false); + mWm.mAtmService.mWindowOrganizerController.applyTaskFragmentTransactionLocked( + t, TaskFragmentOrganizer.TASK_FRAGMENT_TRANSIT_CHANGE, + false /* shouldApplyIndependently */); + + // Should be not visible and not focusable after the transaction. + assertFalse(taskFragment.shouldBeVisible(null)); + assertFalse(taskFragment.isFocusable()); + assertFalse(taskFragment.isTopActivityFocusable()); + + // Apply transaction to the TaskFragment not hidden and focusable. + t.setHidden(taskFragment.mRemoteToken.toWindowContainerToken(), false); + t.setFocusable(taskFragment.mRemoteToken.toWindowContainerToken(), true); + mWm.mAtmService.mWindowOrganizerController.applyTaskFragmentTransactionLocked( + t, TaskFragmentOrganizer.TASK_FRAGMENT_TRANSIT_CHANGE, + false /* shouldApplyIndependently */); + + // Should be visible and focusable after the transaction. + assertTrue(taskFragment.shouldBeVisible(null)); + assertTrue(taskFragment.isFocusable()); + assertTrue(taskFragment.isTopActivityFocusable()); + } + + @Test + public void testTaskFragmentHiddenAndFocusableChanges_throwsWhenNotSystemOrganizer() { + removeGlobalMinSizeRestriction(); + final Task rootTask = new TaskBuilder(mSupervisor).setCreateActivity(true) + .setWindowingMode(WINDOWING_MODE_FULLSCREEN).build(); + + final WindowContainerTransaction t = new WindowContainerTransaction(); + final TaskFragmentOrganizer organizer = + createTaskFragmentOrganizer(t, false /* isSystemOrganizer */); + + final IBinder token = new Binder(); + final TaskFragment taskFragment = new TaskFragmentBuilder(mAtm) + .setParentTask(rootTask) + .setFragmentToken(token) + .setOrganizer(organizer) + .createActivityCount(1) + .build(); + + assertTrue(rootTask.shouldBeVisible(null)); + assertTrue(taskFragment.shouldBeVisible(null)); + + t.setHidden(taskFragment.mRemoteToken.toWindowContainerToken(), true); + t.setFocusable(taskFragment.mRemoteToken.toWindowContainerToken(), false); + + // Non-system organizers are not allow to update the hidden and focusable states. + assertThrows(SecurityException.class, () -> + mWm.mAtmService.mWindowOrganizerController.applyTaskFragmentTransactionLocked( + t, TaskFragmentOrganizer.TASK_FRAGMENT_TRANSIT_CHANGE, + false /* shouldApplyIndependently */) + ); + } + + @Test public void testContainerTranslucentChanges() { removeGlobalMinSizeRestriction(); final Task rootTask = new TaskBuilder(mSupervisor).setCreateActivity(true) @@ -1600,4 +1685,20 @@ public class WindowOrganizerTests extends WindowTestsBase { assertTrue(taskIds.contains(expectedTasks[i].mTaskId)); } } + + @NonNull + private TaskFragmentOrganizer createTaskFragmentOrganizer( + @NonNull WindowContainerTransaction t, boolean isSystemOrganizer) { + final TaskFragmentOrganizer organizer = new TaskFragmentOrganizer(Runnable::run); + final ITaskFragmentOrganizer organizerInterface = + ITaskFragmentOrganizer.Stub.asInterface(organizer.getOrganizerToken().asBinder()); + mWm.mAtmService.mWindowOrganizerController.mTaskFragmentOrganizerController + .registerOrganizerInternal( + ITaskFragmentOrganizer.Stub.asInterface( + organizer.getOrganizerToken().asBinder()), + isSystemOrganizer); + t.setTaskFragmentOrganizer(organizerInterface); + + return organizer; + } } diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java index 138e575a6872..1c689d0d5ce3 100644 --- a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java +++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java @@ -58,6 +58,7 @@ import android.os.Handler; import android.os.IBinder; import android.os.Parcel; import android.os.ParcelFileDescriptor; +import android.os.PermissionEnforcer; import android.os.PersistableBundle; import android.os.RemoteCallback; import android.os.RemoteCallbackList; @@ -67,6 +68,7 @@ import android.os.SharedMemory; import android.os.ShellCallback; import android.os.Trace; import android.os.UserHandle; +import android.permission.flags.Flags; import android.provider.Settings; import android.service.voice.IMicrophoneHotwordDetectionVoiceInteractionCallback; import android.service.voice.IVisualQueryDetectionVoiceInteractionCallback; @@ -1286,6 +1288,17 @@ public class VoiceInteractionManagerService extends SystemService { } } + // Enforce permissions that are flag controlled. The flag value decides if the permission + // should be enforced. + private void initAndVerifyDetector_enforcePermissionWithFlags() { + PermissionEnforcer enforcer = mContext.getSystemService(PermissionEnforcer.class); + if (Flags.voiceActivationPermissionApis()) { + enforcer.enforcePermission( + android.Manifest.permission.RECEIVE_SANDBOX_TRIGGER_AUDIO, + getCallingPid(), getCallingUid()); + } + } + @android.annotation.EnforcePermission(android.Manifest.permission.MANAGE_HOTWORD_DETECTION) @Override public void initAndVerifyDetector( @@ -1295,7 +1308,13 @@ public class VoiceInteractionManagerService extends SystemService { @NonNull IBinder token, IHotwordRecognitionStatusCallback callback, int detectorType) { + // TODO(b/305787465): Remove the MANAGE_HOTWORD_DETECTION permission enforcement on the + // {@link #initAndVerifyDetector(Identity, PersistableBundle, ShareMemory, IBinder, + // IHotwordRecognitionStatusCallback, int)} + // and replace with the permission RECEIVE_SANDBOX_TRIGGER_AUDIO when it is fully + // launched. super.initAndVerifyDetector_enforcePermission(); + initAndVerifyDetector_enforcePermissionWithFlags(); synchronized (this) { enforceIsCurrentVoiceInteractionService(); diff --git a/tools/aapt2/integration-tests/AutoVersionTest/Android.bp b/tools/aapt2/integration-tests/AutoVersionTest/Android.bp index bfd35083366e..c901efa707f4 100644 --- a/tools/aapt2/integration-tests/AutoVersionTest/Android.bp +++ b/tools/aapt2/integration-tests/AutoVersionTest/Android.bp @@ -26,4 +26,5 @@ package { android_test { name: "AaptAutoVersionTest", sdk_version: "current", + use_resource_processor: false, } diff --git a/tools/aapt2/integration-tests/BasicTest/Android.bp b/tools/aapt2/integration-tests/BasicTest/Android.bp index 7db9d2698cc7..d0649ea4ef9c 100644 --- a/tools/aapt2/integration-tests/BasicTest/Android.bp +++ b/tools/aapt2/integration-tests/BasicTest/Android.bp @@ -26,4 +26,5 @@ package { android_test { name: "AaptBasicTest", sdk_version: "current", + use_resource_processor: false, } diff --git a/tools/aapt2/integration-tests/StaticLibTest/App/Android.bp b/tools/aapt2/integration-tests/StaticLibTest/App/Android.bp index 80404eeb8d8e..ebb4e9f479d6 100644 --- a/tools/aapt2/integration-tests/StaticLibTest/App/Android.bp +++ b/tools/aapt2/integration-tests/StaticLibTest/App/Android.bp @@ -24,9 +24,9 @@ package { } android_test { - name: "AaptTestStaticLib_App", sdk_version: "current", + use_resource_processor: false, srcs: ["src/**/*.java"], asset_dirs: [ "assets", diff --git a/tools/aapt2/integration-tests/StaticLibTest/LibOne/Android.bp b/tools/aapt2/integration-tests/StaticLibTest/LibOne/Android.bp index a84da43c70c8..ee12a92906a8 100644 --- a/tools/aapt2/integration-tests/StaticLibTest/LibOne/Android.bp +++ b/tools/aapt2/integration-tests/StaticLibTest/LibOne/Android.bp @@ -26,6 +26,7 @@ package { android_library { name: "AaptTestStaticLib_LibOne", sdk_version: "current", + use_resource_processor: false, srcs: ["src/**/*.java"], resource_dirs: ["res"], } diff --git a/tools/aapt2/integration-tests/StaticLibTest/LibTwo/Android.bp b/tools/aapt2/integration-tests/StaticLibTest/LibTwo/Android.bp index d386c3a35d20..83b2362496fc 100644 --- a/tools/aapt2/integration-tests/StaticLibTest/LibTwo/Android.bp +++ b/tools/aapt2/integration-tests/StaticLibTest/LibTwo/Android.bp @@ -26,6 +26,7 @@ package { android_library { name: "AaptTestStaticLib_LibTwo", sdk_version: "current", + use_resource_processor: false, srcs: ["src/**/*.java"], resource_dirs: ["res"], libs: ["AaptTestStaticLib_LibOne"], diff --git a/tools/aapt2/integration-tests/SymlinkTest/Android.bp b/tools/aapt2/integration-tests/SymlinkTest/Android.bp index 1e8cf86ed811..15a6a207d6d1 100644 --- a/tools/aapt2/integration-tests/SymlinkTest/Android.bp +++ b/tools/aapt2/integration-tests/SymlinkTest/Android.bp @@ -26,4 +26,5 @@ package { android_test { name: "AaptSymlinkTest", sdk_version: "current", + use_resource_processor: false, } |